Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/quick-peas-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@flint.fyi/core": patch
"@flint.fyi/rule-tester": patch
"@flint.fyi/typescript-language": patch
"@flint.fyi/volar-language": minor
---

Introduce Volar.js meta-language.
55 changes: 46 additions & 9 deletions packages/core/src/running/processRuleReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,48 @@ export function processRuleReport(
rule: AnyRule,
ruleReport: RuleReport,
) {
let range = ruleReport.range;
let fix =
ruleReport.fix && !Array.isArray(ruleReport.fix)
? [ruleReport.fix]
: ruleReport.fix;
let suggestions = ruleReport.suggestions;
const { adjustReportRange } = currentFile;
if (adjustReportRange != null) {
const adjustedRange = adjustReportRange(ruleReport.range);
if (adjustedRange == null) {
return null;
}
range = adjustedRange;
fix &&= fix
.map((fix) => {
const range = adjustReportRange(fix.range);
return (
range && {
...fix,
range,
}
);
})
.filter((f) => f != null);

suggestions &&= suggestions
.map((s) => {
if ("files" in s) {
// TODO: support cross-file suggestions
return null;
}
const range = adjustReportRange(s.range);
return (
range && {
...s,
range,
}
);
})
.filter((s) => s != null);
}

return {
...ruleReport,
about: {
Expand All @@ -21,23 +63,18 @@ export function processRuleReport(
? `${rule.about.pluginId}/${rule.about.id}`
: rule.about.id,
},
fix:
ruleReport.fix && !Array.isArray(ruleReport.fix)
? [ruleReport.fix]
: ruleReport.fix,
fix,
message: nullThrows(
rule.messages[ruleReport.message],
`Rule "${rule.about.id}" reported message "${ruleReport.message}" which is not defined in its messages.`,
),
range: {
begin: getColumnAndLineOfPosition(
currentFile.about.sourceText,
ruleReport.range.begin,
),
end: getColumnAndLineOfPosition(
currentFile.about.sourceText,
ruleReport.range.end,
range.begin,
),
end: getColumnAndLineOfPosition(currentFile.about.sourceText, range.end),
},
suggestions,
};
}
9 changes: 6 additions & 3 deletions packages/core/src/running/runLintRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export async function runLintRule(

const ruleRuntime = await rule.setup({
report(ruleReport) {
// TODO: what if report is called asynchronously? maybe we can use AsyncLocalStorage?
if (!currentFile) {
throw new Error(
"`filePath` not provided in a rule report() not called by a visitor.",
Expand All @@ -35,9 +36,11 @@ export async function runLintRule(

log("Adding %s report for file path %s", ruleReport.message, filePath);

reportsByFilePath
.get(filePath)
.push(processRuleReport(currentFile, rule, ruleReport));
const processedReport = processRuleReport(currentFile, rule, ruleReport);
if (processedReport == null) {
return;
}
reportsByFilePath.get(filePath).push(processedReport);
},
});

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/types/languages.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CommentDirective } from "./directives.ts";
import type { LinterHost } from "./host.ts";
import type { CharacterReportRange } from "./ranges.ts";
import type { FileReport } from "./reports.ts";
import type { Rule, RuleAbout, RuleDefinition, RuleRuntime } from "./rules.ts";
import type { AnyOptionalSchema, InferredOutputObject } from "./shapes.ts";
Expand Down Expand Up @@ -138,6 +139,9 @@ export type LanguageFile<FileServices extends object> = Disposable &
*/
export interface LanguageFileBase<FileServices extends object> {
about: FileAboutData;
adjustReportRange?: (
range: CharacterReportRange,
) => CharacterReportRange | null;
directives?: CommentDirective[];
reports?: FileReport[];
services: FileServices;
Expand Down
9 changes: 7 additions & 2 deletions packages/rule-tester/src/runTestCaseRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type AnyLanguageFileFactory,
type AnyOptionalSchema,
type AnyRule,
type FileReport,
type InferredOutputObject,
type NormalizedReport,
normalizePath,
Expand Down Expand Up @@ -60,11 +61,15 @@ export async function runTestCaseRule<
sourceText: code,
});

const reports: NormalizedReport[] = [];
const reports: FileReport[] = [];

const ruleRuntime = await rule.setup({
report(ruleReport) {
reports.push(processRuleReport(file, rule, ruleReport));
const processedReport = processRuleReport(file, rule, ruleReport);
if (processedReport == null) {
return;
}
reports.push(processedReport);
},
});

Expand Down
13 changes: 0 additions & 13 deletions packages/typescript-language/src/getTypeScriptFileDiagnostics.ts

This file was deleted.

146 changes: 135 additions & 11 deletions packages/typescript-language/src/language.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { createLanguage, type FileAboutData } from "@flint.fyi/core";
import {
type AnyOptionalSchema,
createLanguage,
type FileAboutData,
type InferredOutputObject,
type LanguageDiagnostics,
type LanguageFile,
type LanguageFileDefinition,
type RuleRuntime,
} from "@flint.fyi/core";
import { assert } from "@flint.fyi/utils";
import { createProjectService } from "@typescript-eslint/project-service";
import { debugForFile } from "debug-for-file";
import path from "node:path";
import * as ts from "typescript";

import packageJson from "../package.json" with { type: "json" };
import { convertTypeScriptDiagnosticToLanguageFileDiagnostic } from "./convertTypeScriptDiagnosticToLanguageFileDiagnostic.ts";
import { createTypeScriptServerHost } from "./createTypeScriptServerHost.ts";
import { parseDirectivesFromTypeScriptFile } from "./directives/parseDirectivesFromTypeScriptFile.ts";
import { getFirstEnumValues } from "./getFirstEnumValues.ts";
import { getTypeScriptFileCacheImpacts } from "./getTypeScriptFileCacheImpacts.ts";
import { getTypeScriptFileDiagnostics } from "./getTypeScriptFileDiagnostics.ts";
import type { TypeScriptNodesByName, TypeScriptNodeVisitors } from "./nodes.ts";
import type * as AST from "./types/ast.ts";
import type { Checker } from "./types/checker.ts";
Expand All @@ -21,7 +32,52 @@ export interface TypeScriptFileServices {

const log = debugForFile(import.meta.filename);

const NodeSyntaxKinds = getFirstEnumValues(ts.SyntaxKind);
export const NodeSyntaxKinds = getFirstEnumValues(ts.SyntaxKind);

interface GlobalLanguageState {
packageVersion: string;
volarCreateFile: null | VolarCreateFile;
}
type VolarCreateFile = (
data: FileAboutData,
program: ts.Program,
sourceFile: AST.SourceFile,
) => VolarLanguageFileDefinition;

type VolarLanguageFileDefinition = LanguageFileDefinition<object> & {
__volarServices: {
getDiagnostics(): LanguageDiagnostics;
runVisitors(
file: LanguageFile<TypeScriptFileServices>,
options: InferredOutputObject<AnyOptionalSchema | undefined>,
runtime: RuleRuntime<TypeScriptNodeVisitors, TypeScriptFileServices>,
): void;
};
};

const stateSymbol = Symbol.for("@flint.fyi/typescript-language/state");

const globalTyped = globalThis as typeof globalThis & {
[stateSymbol]?: GlobalLanguageState;
};

assert(
globalTyped[stateSymbol] == null,
`Two different versions of ${packageJson.name} are imported: ${packageJson.version} and ${globalTyped[stateSymbol]?.packageVersion}`,
);

const languageState: GlobalLanguageState = (globalTyped[stateSymbol] = {
packageVersion: packageJson.version,
volarCreateFile: null,
});

export function setVolarCreateFile(create: VolarCreateFile) {
assert(
languageState.volarCreateFile == null,
"setVolarCreateFile is expected to be called only once",
);
languageState.volarCreateFile = create;
}

export const typescriptLanguage = createLanguage<
TypeScriptNodeVisitors,
Expand Down Expand Up @@ -67,15 +123,33 @@ export const typescriptLanguage = createLanguage<
`Could not retrieve source file for: ${data.filePathAbsolute}`,
);

const fileExtension = path.extname(data.filePathAbsolute);
if (typeScriptCoreSupportedExtensions.has(fileExtension)) {
return {
...parseDirectivesFromTypeScriptFile(sourceFile as AST.SourceFile),
about: data,
language: typescriptLanguage,
services: {
program,
sourceFile,
typeChecker: program.getTypeChecker(),
},
[Symbol.dispose]() {
service.closeClientFile(data.filePathAbsolute);
},
};
}

if (languageState.volarCreateFile == null) {
throwUnknownLanguageExtension(data.filePathAbsolute);
}

return {
...parseDirectivesFromTypeScriptFile(sourceFile as AST.SourceFile),
about: data,
language: typescriptLanguage,
services: {
...languageState.volarCreateFile(
data,
program,
sourceFile,
typeChecker: program.getTypeChecker(),
},
sourceFile as AST.SourceFile,
),
[Symbol.dispose]() {
service.closeClientFile(data.filePathAbsolute);
},
Expand All @@ -86,12 +160,30 @@ export const typescriptLanguage = createLanguage<
},

getFileCacheImpacts: getTypeScriptFileCacheImpacts,
getFileDiagnostics: getTypeScriptFileDiagnostics,
getFileDiagnostics(file) {
if ("__volarServices" in file) {
return (
file as VolarLanguageFileDefinition
).__volarServices.getDiagnostics();
}
return ts
.getPreEmitDiagnostics(file.services.program, file.services.sourceFile)
.map(convertTypeScriptDiagnosticToLanguageFileDiagnostic);
},
runFileVisitors(file, options, runtime) {
if (!runtime.visitors) {
return;
}

if ("__volarServices" in file) {
(file as VolarLanguageFileDefinition).__volarServices.runVisitors(
file,
options,
runtime,
);
return;
}

const { visitors } = runtime;
const visitorServices = { options, ...file.services };

Expand All @@ -110,3 +202,35 @@ export const typescriptLanguage = createLanguage<
visit(file.services.sourceFile);
},
});

const typeScriptCoreSupportedExtensions: ReadonlySet<string> = new Set([
".cjs",
".cts",
".d.cts",
".d.mts",
".d.ts",
".js",
".json",
".jsx",
".mjs",
".mts",
".ts",
".tsx",
]);

const fileExtToFlintPlugin: Record<string, string> = {
".astro": "@flint.fyi/astro",
".gjs": "@flint.fyi/ember",
".gts": "@flint.fyi/ember",
".mdx": "@flint.fyi/mdx",
".svelte": "@flint.fyi/svelte",
".vue": "@flint.fyi/vue",
};

export function throwUnknownLanguageExtension(filename: string): never {
const pluginName = fileExtToFlintPlugin[path.extname(filename)];
const message = pluginName
? `Did you install & import ${pluginName}?`
: "Unknown extension.";
throw new Error(`Cannot process ${filename}. ${message}`);
}
Loading