From 9c908781ea305a3d2f9e817a591846b2859bf43d Mon Sep 17 00:00:00 2001 From: auvred Date: Mon, 29 Dec 2025 13:39:10 +0300 Subject: [PATCH 01/11] wip --- packages/astro-language/package.json | 37 ++ packages/astro-language/src/index.ts | 428 +++++++++++++++ .../src/rules/anyReturns.test.ts | 40 ++ .../astro-language/src/rules/ruleTester.ts | 14 + packages/astro-language/tsconfig.json | 13 + packages/core/src/index.ts | 1 + .../core/src/ts-patch/install-patch-hooks.ts | 4 +- packages/ts/src/index.ts | 1 + packages/vue-language/package.json | 38 ++ packages/vue-language/src/index.ts | 2 + packages/vue-language/src/language.ts | 492 ++++++++++++++++++ packages/vue-language/src/plugin.ts | 13 + .../vue-language/src/rules/MyComponent.vue | 5 + .../vue-language/src/rules/anyReturns.test.ts | 98 ++++ .../src/rules/asyncComputed.test.ts | 414 +++++++++++++++ .../vue-language/src/rules/asyncComputed.ts | 93 ++++ .../src/rules/debuggerStatements.test.ts | 78 +++ packages/vue-language/src/rules/fixture.ts | 1 + packages/vue-language/src/rules/ruleTester.ts | 14 + .../src/rules/scriptSetupExports.test.ts | 285 ++++++++++ .../src/rules/scriptSetupExports.ts | 79 +++ .../vue-language/src/rules/vForKey.test.ts | 464 +++++++++++++++++ packages/vue-language/src/rules/vForKey.ts | 261 ++++++++++ packages/vue-language/tsconfig.json | 13 + pnpm-lock.yaml | 422 ++++++++++++++- tsconfig.json | 2 + 26 files changed, 3309 insertions(+), 3 deletions(-) create mode 100644 packages/astro-language/package.json create mode 100644 packages/astro-language/src/index.ts create mode 100644 packages/astro-language/src/rules/anyReturns.test.ts create mode 100644 packages/astro-language/src/rules/ruleTester.ts create mode 100644 packages/astro-language/tsconfig.json create mode 100644 packages/vue-language/package.json create mode 100644 packages/vue-language/src/index.ts create mode 100644 packages/vue-language/src/language.ts create mode 100644 packages/vue-language/src/plugin.ts create mode 100644 packages/vue-language/src/rules/MyComponent.vue create mode 100644 packages/vue-language/src/rules/anyReturns.test.ts create mode 100644 packages/vue-language/src/rules/asyncComputed.test.ts create mode 100644 packages/vue-language/src/rules/asyncComputed.ts create mode 100644 packages/vue-language/src/rules/debuggerStatements.test.ts create mode 100644 packages/vue-language/src/rules/fixture.ts create mode 100644 packages/vue-language/src/rules/ruleTester.ts create mode 100644 packages/vue-language/src/rules/scriptSetupExports.test.ts create mode 100644 packages/vue-language/src/rules/scriptSetupExports.ts create mode 100644 packages/vue-language/src/rules/vForKey.test.ts create mode 100644 packages/vue-language/src/rules/vForKey.ts create mode 100644 packages/vue-language/tsconfig.json diff --git a/packages/astro-language/package.json b/packages/astro-language/package.json new file mode 100644 index 000000000..f782d025f --- /dev/null +++ b/packages/astro-language/package.json @@ -0,0 +1,37 @@ +{ + "name": "@flint.fyi/astro-language", + "version": "0.0.1", + "description": "[Experimental] TypeScript language plugin for Flint.", + "repository": { + "type": "git", + "url": "https://github.com/JoshuaKGoldberg/flint", + "directory": "packages/ts" + }, + "license": "MIT", + "author": { + "name": "JoshuaKGoldberg", + "email": "npm@joshuakgoldberg.com" + }, + "type": "module", + "main": "./lib/index.js", + "dependencies": { + "@astrojs/language-server": "^2.16.2", + "@flint.fyi/core": "workspace:", + "@flint.fyi/ts": "workspace:", + "@volar/language-core": "2.4.27", + "@volar/typescript": "2.4.27", + "ts-api-utils": "^2.1.0", + "typescript": ">=5.9.3", + "vscode-uri": "^3.1.0" + }, + "devDependencies": { + "@flint.fyi/rule-tester": "workspace:" + }, + "engines": { + "node": ">=24.0.0" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/astro-language/src/index.ts b/packages/astro-language/src/index.ts new file mode 100644 index 000000000..355f67c42 --- /dev/null +++ b/packages/astro-language/src/index.ts @@ -0,0 +1,428 @@ +import { + LanguagePlugin as VolarLanguagePlugin, + Language as VolarLanguage, + Mapper as VolarMapper, + CodegenContext as VolarCodegenContext, +} from "@volar/language-core"; +import { + AstroVirtualCode, + getAstroLanguagePlugin, +} from "@astrojs/language-server/dist/core/index.js"; +import { + convertTypeScriptDiagnosticToLanguageFileDiagnostic, + extractDirectivesFromTypeScriptFile, + NodeSyntaxKinds, + prepareTypeScriptBasedLanguage, + prepareTypeScriptFile, + TSNodesByName, + TypeScriptBasedLanguageFile, + TypeScriptFileServices, +} from "@flint.fyi/ts"; +// for LanguagePlugin interface augmentation +import "@volar/typescript"; +import { URI } from "vscode-uri"; + +import { + CharacterReportRange, + createLanguage, + DirectivesCollector, + getColumnAndLineOfPosition, + isSuggestionForFiles, + LanguagePreparedDefinition, + NormalizedReport, + NormalizedReportRangeObject, + RuleContext, + setTSExtraSupportedExtensions, + setTSProgramCreationProxy, + SourceFileWithLineMap, +} from "@flint.fyi/core"; +import { proxyCreateProgram } from "@volar/typescript/lib/node/proxyCreateProgram.js"; +import ts from "typescript"; + +type ProxiedTSProgram = ts.Program & { + __flintVolarLanguage?: undefined | VolarLanguage; +}; + +setTSExtraSupportedExtensions([".astro"]); +setTSProgramCreationProxy( + (ts, createProgram) => + new Proxy(function () {} as unknown as typeof createProgram, { + apply(target, thisArg, args) { + let volarLanguage = null as null | VolarLanguage; + const proxied = proxyCreateProgram(ts, createProgram, (ts, options) => { + const { + getLanguageId, + createVirtualCode, + updateVirtualCode, + disposeVirtualCode, + isAssociatedFileOnly, + typescript, + } = getAstroLanguagePlugin(); + const languagePlugin: VolarLanguagePlugin = + { + getLanguageId(scriptId) { + return getLanguageId(URI.file(scriptId)); + }, + createVirtualCode(scriptId, languageId, snapshot) { + // astro never touches ctx + const ctx = {} as VolarCodegenContext; + return createVirtualCode?.( + URI.file(scriptId), + languageId, + snapshot, + ctx, + ); + }, + updateVirtualCode(scriptId, virtualCode, newSnapshot) { + // astro never touches ctx + const ctx = {} as VolarCodegenContext; + return updateVirtualCode?.( + URI.file(scriptId), + virtualCode, + newSnapshot, + ctx, + ); + }, + disposeVirtualCode(scriptId, virtualCode) { + return disposeVirtualCode?.(URI.file(scriptId), virtualCode); + }, + ...(isAssociatedFileOnly != null && { + isAssociatedFileOnly(scriptId, languageId): boolean { + return isAssociatedFileOnly(URI.file(scriptId), languageId); + }, + }), + typescript, + }; + if (languagePlugin.typescript != null) { + const { getServiceScript } = languagePlugin.typescript; + // getExtraServiceScripts() is not available in this use case. + delete languagePlugin.typescript.getExtraServiceScripts; + languagePlugin.typescript.getServiceScript = (root) => { + const script = getServiceScript(root); + if (script == null) { + return script; + } + return { + ...script, + // Leading offset is useful for LanguageService [1], but we don't use it. + // The Vue language plugin doesn't provide preventLeadingOffset [2], so we + // have to provide it ourselves. + // + // [1] https://github.com/volarjs/volar.js/discussions/188 + // [2] https://github.com/vuejs/language-tools/blob/fd05a1c92c9af63e6af1eab926084efddf7c46c3/packages/language-core/lib/languagePlugin.ts#L113-L130 + preventLeadingOffset: true, + }; + }; + } + return { + languagePlugins: [languagePlugin], + setup: (lang) => (volarLanguage = lang), + }; + }); + + const program: ProxiedTSProgram = Reflect.apply(proxied, thisArg, args); + + if (volarLanguage == null) { + throw new Error("Flint bug: volarLanguage is not defined"); + } + + if (program.__flintVolarLanguage != null) { + return program; + } + + program.__flintVolarLanguage = volarLanguage; + return program; + }, + }), +); + +export interface AstroServices extends TypeScriptFileServices { + astroServices?: { + // codegen: VueCodegen; + // map: VolarMapper; + // sfc: RootNode; + // virtualCode: VueVirtualCode; + // // TODO: can we type MessageId? + // reportSfc: RuleReporter; + }; +} + +export const astroLanguage = createLanguage({ + about: { + name: "Astro", + }, + prepare: () => { + const tsLang = prepareTypeScriptBasedLanguage(); + + return { + prepareFromDisk: (filePathAbsolute) => { + return prepareAstroFile( + filePathAbsolute, + tsLang.createFromDisk(filePathAbsolute), + ); + }, + prepareFromVirtual: (filePathAbsolute, sourceText) => { + return prepareAstroFile( + filePathAbsolute, + tsLang.createFromVirtual(filePathAbsolute, sourceText), + ); + }, + }; + }, +}); + +function prepareAstroFile( + filePathAbsolute: string, + tsFile: TypeScriptBasedLanguageFile, +): LanguagePreparedDefinition { + const { program, sourceFile, [Symbol.dispose]: onDispose } = tsFile; + + // @ts-expect-error + const volarLanguage: VolarLanguage = program.__flintVolarLanguage; + + if (volarLanguage == null) { + throw new Error( + "'typescript' package wasn't properly patched. Make sure you don't import 'typescript' before Flint.", + ); + } + + const sourceScript = volarLanguage.scripts.get(filePathAbsolute); + if (sourceScript == null) { + throw new Error("Expected sourceScript to be set"); + } + if (sourceScript.languageId !== "astro") { + return prepareTypeScriptFile({ + program, + sourceFile, + [Symbol.dispose]: onDispose, + }); + } + if (sourceScript.generated == null) { + throw new Error("Expected sourceScript.generated to be set"); + } + if (sourceScript.snapshot == null) { + throw new Error("Expected sourceScript.snapshot to be set"); + } + if (sourceScript.generated.languagePlugin.typescript == null) { + throw new Error( + "Expected sourceScript.generated.languagePlugin.typescript to be set", + ); + } + + const sourceText = sourceScript.snapshot.getText( + 0, + sourceScript.snapshot.getLength(), + ); + const sourceTextWithLineMap: SourceFileWithLineMap = { + text: sourceText, + }; + function normalizeSourceRange( + range: CharacterReportRange, + ): NormalizedReportRangeObject { + return { + begin: getColumnAndLineOfPosition(sourceTextWithLineMap, range.begin), + end: getColumnAndLineOfPosition(sourceTextWithLineMap, range.end), + }; + } + + const serviceScript = + sourceScript.generated.languagePlugin.typescript.getServiceScript( + sourceScript.generated.root, + ); + if (serviceScript == null) { + throw new Error("Expected serviceScript to exist"); + } + + const virtualCode = sourceScript.generated.root as AstroVirtualCode; + // const codegen = tsCodegen.get(virtualCode.sfc); + // if (codegen == null) { + // throw new Error("Expected codegen to exist"); + // } + + const map = volarLanguage.maps.get(serviceScript.code, sourceScript); + const sortedMappings = map.mappings.toSorted( + (a, b) => a.generatedOffsets[0] - b.generatedOffsets[0], + ); + + // const sfcAst = vueParse(sourceText, { + // comments: true, + // expressionPlugins: ["typescript"], + // parseMode: "html", + // // We ignore errors because virtual code already provides them, + // // and it also provides them with sourceText-based locations, + // // so we don't have to remap them. Oh, and it also contains errors from + // // other blocks rather than only