diff --git a/package.json b/package.json index d4fa2a380c..0278f0fbb0 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "private": true, + "packageManager": "pnpm@9.1.0", "scripts": { "build": "tsc -b", "watch": "npm run build && (npm run watch:base & npm run watch:vue)", diff --git a/packages/language-core/lib/codegen/common.ts b/packages/language-core/lib/codegen/common.ts index 58389a1931..1869f634b8 100644 --- a/packages/language-core/lib/codegen/common.ts +++ b/packages/language-core/lib/codegen/common.ts @@ -1,5 +1,5 @@ import type * as ts from 'typescript'; -import { getNodeText } from '../parsers/scriptSetupRanges'; +import { getNodeText } from '../utils/parseBindings'; import type { Code, SfcBlock, VueCodeInformation } from '../types'; export const newLine = '\n'; diff --git a/packages/language-core/lib/codegen/script/context.ts b/packages/language-core/lib/codegen/script/context.ts index a78d05a82d..9d3960002f 100644 --- a/packages/language-core/lib/codegen/script/context.ts +++ b/packages/language-core/lib/codegen/script/context.ts @@ -108,9 +108,10 @@ export function createScriptCodegenContext(options: ScriptCodegenOptions) { generatedPropsType: false, scriptSetupGeneratedOffset: undefined as number | undefined, bypassDefineComponent: options.lang === 'js' || options.lang === 'jsx', + // TODO: maybe not using substring to get names? bindingNames: new Set([ - ...options.scriptRanges?.bindings.map(range => options.sfc.script!.content.substring(range.start, range.end)) ?? [], - ...options.scriptSetupRanges?.bindings.map(range => options.sfc.scriptSetup!.content.substring(range.start, range.end)) ?? [], + ...options.scriptRanges?.bindings.bindingRanges.map(range => options.sfc.script!.content.substring(range.start, range.end)) ?? [], + ...options.scriptSetupRanges?.bindings.bindingRanges.map(range => options.sfc.scriptSetup!.content.substring(range.start, range.end)) ?? [], ]), helperTypes, generateHelperTypes, diff --git a/packages/language-core/lib/codegen/script/internalComponent.ts b/packages/language-core/lib/codegen/script/internalComponent.ts index c5a0d600e1..85788cc703 100644 --- a/packages/language-core/lib/codegen/script/internalComponent.ts +++ b/packages/language-core/lib/codegen/script/internalComponent.ts @@ -21,9 +21,9 @@ export function* generateInternalComponent( // bindings const templateUsageVars = getTemplateUsageVars(options, ctx); for (const [content, bindings] of [ - [options.sfc.scriptSetup.content, options.scriptSetupRanges.bindings] as const, + [options.sfc.scriptSetup.content, options.scriptSetupRanges.bindings.bindingRanges] as const, options.sfc.script && options.scriptRanges - ? [options.sfc.script.content, options.scriptRanges.bindings] as const + ? [options.sfc.script.content, options.scriptRanges.bindings.bindingRanges] as const : ['', []] as const, ]) { for (const expose of bindings) { diff --git a/packages/language-core/lib/codegen/template/context.ts b/packages/language-core/lib/codegen/template/context.ts index e8673496f2..bb0911a240 100644 --- a/packages/language-core/lib/codegen/template/context.ts +++ b/packages/language-core/lib/codegen/template/context.ts @@ -1,5 +1,6 @@ import type * as CompilerDOM from '@vue/compiler-dom'; import type { Code, VueCodeInformation } from '../../types'; +import type { BindingTypes } from '../../utils/parseBindings'; import { endOfLine, newLine, wrapWith } from '../common'; const _codeFeatures = { @@ -96,6 +97,7 @@ export function createTemplateCodegenContext() { const blockConditions: string[] = []; const usedComponentCtxVars = new Set(); const scopedClasses: { className: string, offset: number; }[] = []; + let bindingTypes: Map | undefined; return { slots, @@ -106,6 +108,9 @@ export function createTemplateCodegenContext() { blockConditions, usedComponentCtxVars, scopedClasses, + get bindingTypes() { + return bindingTypes; + }, accessGlobalVariable(name: string, offset?: number) { let arr = accessGlobalVariables.get(name); if (!arr) { @@ -127,6 +132,9 @@ export function createTemplateCodegenContext() { getInternalVariable: () => { return `__VLS_${variableId++}`; }, + setBindingTypes: (types: Map) => { + bindingTypes = types; + }, ignoreError: function* (): Generator { if (!ignoredError) { ignoredError = true; diff --git a/packages/language-core/lib/codegen/template/element.ts b/packages/language-core/lib/codegen/template/element.ts index e516c44d89..f3f77c8111 100644 --- a/packages/language-core/lib/codegen/template/element.ts +++ b/packages/language-core/lib/codegen/template/element.ts @@ -2,6 +2,7 @@ import * as CompilerDOM from '@vue/compiler-dom'; import { camelize, capitalize } from '@vue/shared'; import type { Code, VueCodeInformation } from '../../types'; import { hyphenateTag } from '../../utils/shared'; +import { BindingTypes } from '../../utils/parseBindings'; import { collectVars, createTsAst, endOfLine, newLine, variableNameRegex, wrapWith } from '../common'; import { generateCamelized } from './camelized'; import type { TemplateCodegenContext } from './context'; @@ -30,7 +31,12 @@ export function* generateComponent( : [startTagOffset]; const propsFailedExps: CompilerDOM.SimpleExpressionNode[] = []; const possibleOriginalNames = getPossibleOriginalComponentNames(node.tag, true); - const matchImportName = possibleOriginalNames.find(name => options.scriptSetupImportComponentNames.has(name)); + const matchImportName = possibleOriginalNames.find(name => { + const bindingType = ctx.bindingTypes?.get(name); + if (bindingType) { + return bindingType & BindingTypes.Component; + } + }); const var_originalComponent = matchImportName ?? ctx.getInternalVariable(); const var_functionalComponent = ctx.getInternalVariable(); const var_componentInstance = ctx.getInternalVariable(); diff --git a/packages/language-core/lib/codegen/template/index.ts b/packages/language-core/lib/codegen/template/index.ts index 0e655f6c28..a876cbffca 100644 --- a/packages/language-core/lib/codegen/template/index.ts +++ b/packages/language-core/lib/codegen/template/index.ts @@ -1,6 +1,7 @@ import * as CompilerDOM from '@vue/compiler-dom'; import type * as ts from 'typescript'; import type { Code, Sfc, VueCompilerOptions } from '../../types'; +import type { BindingTypes } from '../../utils/parseBindings'; import { endOfLine, newLine, wrapWith } from '../common'; import { createTemplateCodegenContext } from './context'; import { getCanonicalComponentName, getPossibleOriginalComponentNames } from './element'; @@ -15,14 +16,17 @@ export interface TemplateCodegenOptions { template: NonNullable; shouldGenerateScopedClasses?: boolean; stylesScopedClasses: Set; - scriptSetupImportComponentNames: Set; hasDefineSlots?: boolean; slotsAssignName?: string; propsAssignName?: string; + bindingTypes?: Map; } export function* generateTemplate(options: TemplateCodegenOptions) { const ctx = createTemplateCodegenContext(); + if (options.bindingTypes) { + ctx.setBindingTypes(options.bindingTypes); + }; let hasSlot = false; diff --git a/packages/language-core/lib/codegen/template/interpolation.ts b/packages/language-core/lib/codegen/template/interpolation.ts index 6f90c64757..134e12f701 100644 --- a/packages/language-core/lib/codegen/template/interpolation.ts +++ b/packages/language-core/lib/codegen/template/interpolation.ts @@ -1,6 +1,6 @@ -import { isGloballyWhitelisted } from '@vue/shared'; +import { isGloballyAllowed } from '@vue/shared'; import type * as ts from 'typescript'; -import { getNodeText, getStartEnd } from '../../parsers/scriptSetupRanges'; +import { BindingTypes, getNodeText, getStartEnd } from '../../utils/parseBindings'; import type { Code, VueCodeInformation, VueCompilerOptions } from '../../types'; import { collectVars, createTsAst } from '../common'; import type { TemplateCodegenContext } from './context'; @@ -89,9 +89,10 @@ export function* forEachInterpolationSegment( if ( ctx.hasLocalVariable(text) || // https://github.com/vuejs/core/blob/245230e135152900189f13a4281302de45fdcfaa/packages/compiler-core/src/transforms/transformExpression.ts#L342-L352 - isGloballyWhitelisted(text) || + isGloballyAllowed(text) || text === 'require' || - text.startsWith('__VLS_') + text.startsWith('__VLS_') || + ((ctx.bindingTypes?.get(text) ?? 0) & BindingTypes.NoUnref) ) { // localVarOffsets.push(localVar.getStart(ast)); } diff --git a/packages/language-core/lib/parsers/scriptRanges.ts b/packages/language-core/lib/parsers/scriptRanges.ts index c31ca6e161..3a331d7683 100644 --- a/packages/language-core/lib/parsers/scriptRanges.ts +++ b/packages/language-core/lib/parsers/scriptRanges.ts @@ -1,11 +1,14 @@ import type { TextRange } from '../types'; import type * as ts from 'typescript'; -import { getNodeText, getStartEnd, parseBindingRanges } from './scriptSetupRanges'; +import { BindingTypes, getNodeText, getStartEnd, parseBindings } from '../utils/parseBindings'; +import { injectTscApiShim } from '../utils/tscApiShim'; export interface ScriptRanges extends ReturnType { } export function parseScriptRanges(ts: typeof import('typescript'), ast: ts.SourceFile, hasScriptSetup: boolean, withNode: boolean) { + ts = injectTscApiShim(ts); + let exportDefault: (TextRange & { expression: TextRange, args: TextRange, @@ -15,14 +18,16 @@ export function parseScriptRanges(ts: typeof import('typescript'), ast: ts.Sourc nameOption: TextRange | undefined, }) | undefined; - const bindings = hasScriptSetup ? parseBindingRanges(ts, ast) : []; + const bindings = hasScriptSetup + ? parseBindings(ts, ast) + : { bindingRanges: [], bindingTypes: new Map() }; ts.forEachChild(ast, raw => { if (ts.isExportAssignment(raw)) { let node: ts.AsExpression | ts.ExportAssignment | ts.ParenthesizedExpression = raw; - while (isAsExpression(node.expression) || ts.isParenthesizedExpression(node.expression)) { // fix https://github.com/vuejs/language-tools/issues/1882 + while (ts.isAsExpression(node.expression) || ts.isParenthesizedExpression(node.expression)) { // fix https://github.com/vuejs/language-tools/issues/1882 node = node.expression; } @@ -71,9 +76,4 @@ export function parseScriptRanges(ts: typeof import('typescript'), ast: ts.Sourc function _getStartEnd(node: ts.Node) { return getStartEnd(ts, node, ast); } - - // isAsExpression is missing in tsc - function isAsExpression(node: ts.Node): node is ts.AsExpression { - return node.kind === ts.SyntaxKind.AsExpression; - } } diff --git a/packages/language-core/lib/parsers/scriptSetupRanges.ts b/packages/language-core/lib/parsers/scriptSetupRanges.ts index fc3696efd8..0ebdc7e930 100644 --- a/packages/language-core/lib/parsers/scriptSetupRanges.ts +++ b/packages/language-core/lib/parsers/scriptSetupRanges.ts @@ -1,5 +1,6 @@ import type * as ts from 'typescript'; import type { VueCompilerOptions, TextRange } from '../types'; +import { getNodeText, getStartEnd, parseBindings } from '../utils/parseBindings'; export interface ScriptSetupRanges extends ReturnType { } @@ -46,10 +47,9 @@ export function parseScriptSetupRanges( required: boolean; isModel?: boolean; }[] = []; - const bindings = parseBindingRanges(ts, ast); + const bindings = parseBindings(ts, ast, vueCompilerOptions); const text = ast.text; const leadingCommentEndOffset = ts.getLeadingCommentRanges(text, 0)?.reverse()[0].end ?? 0; - const importComponentNames = new Set(); ts.forEachChild(ast, node => { const isTypeExport = (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) && node.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword); @@ -71,17 +71,6 @@ export function parseScriptSetupRanges( } foundNonImportExportNode = true; } - - if ( - ts.isImportDeclaration(node) - && node.importClause?.name - && !node.importClause.isTypeOnly - ) { - const moduleName = getNodeText(ts, node.moduleSpecifier, ast).slice(1, -1); - if (vueCompilerOptions.extensions.some(ext => moduleName.endsWith(ext))) { - importComponentNames.add(getNodeText(ts, node.importClause.name, ast)); - } - } }); ts.forEachChild(ast, child => visitNode(child, [ast])); @@ -89,7 +78,6 @@ export function parseScriptSetupRanges( leadingCommentEndOffset, importSectionEndOffset, bindings, - importComponentNames, props, slots, emits, @@ -267,108 +255,3 @@ export function parseScriptSetupRanges( }); } } - -export function parseBindingRanges(ts: typeof import('typescript'), sourceFile: ts.SourceFile) { - const bindings: TextRange[] = []; - ts.forEachChild(sourceFile, node => { - if (ts.isVariableStatement(node)) { - for (const node_2 of node.declarationList.declarations) { - const vars = _findBindingVars(node_2.name); - for (const _var of vars) { - bindings.push(_var); - } - } - } - else if (ts.isFunctionDeclaration(node)) { - if (node.name && ts.isIdentifier(node.name)) { - bindings.push(_getStartEnd(node.name)); - } - } - else if (ts.isClassDeclaration(node)) { - if (node.name) { - bindings.push(_getStartEnd(node.name)); - } - } - else if (ts.isEnumDeclaration(node)) { - bindings.push(_getStartEnd(node.name)); - } - - if (ts.isImportDeclaration(node)) { - if (node.importClause && !node.importClause.isTypeOnly) { - if (node.importClause.name) { - bindings.push(_getStartEnd(node.importClause.name)); - } - if (node.importClause.namedBindings) { - if (ts.isNamedImports(node.importClause.namedBindings)) { - for (const element of node.importClause.namedBindings.elements) { - bindings.push(_getStartEnd(element.name)); - } - } - else if (ts.isNamespaceImport(node.importClause.namedBindings)) { - bindings.push(_getStartEnd(node.importClause.namedBindings.name)); - } - } - } - } - }); - return bindings; - function _getStartEnd(node: ts.Node) { - return getStartEnd(ts, node, sourceFile); - } - function _findBindingVars(left: ts.BindingName) { - return findBindingVars(ts, left, sourceFile); - } -} - -export function findBindingVars(ts: typeof import('typescript'), left: ts.BindingName, sourceFile: ts.SourceFile) { - const vars: TextRange[] = []; - worker(left); - return vars; - function worker(_node: ts.Node) { - if (ts.isIdentifier(_node)) { - vars.push(getStartEnd(ts, _node, sourceFile)); - } - // { ? } = ... - // [ ? ] = ... - else if (ts.isObjectBindingPattern(_node) || ts.isArrayBindingPattern(_node)) { - for (const property of _node.elements) { - if (ts.isBindingElement(property)) { - worker(property.name); - } - } - } - // { foo: ? } = ... - else if (ts.isPropertyAssignment(_node)) { - worker(_node.initializer); - } - // { foo } = ... - else if (ts.isShorthandPropertyAssignment(_node)) { - vars.push(getStartEnd(ts, _node.name, sourceFile)); - } - // { ...? } = ... - // [ ...? ] = ... - else if (ts.isSpreadAssignment(_node) || ts.isSpreadElement(_node)) { - worker(_node.expression); - } - } -} - -export function getStartEnd( - ts: typeof import('typescript'), - node: ts.Node, - sourceFile: ts.SourceFile -) { - return { - start: (ts as any).getTokenPosOfNode(node, sourceFile) as number, - end: node.end, - }; -} - -export function getNodeText( - ts: typeof import('typescript'), - node: ts.Node, - sourceFile: ts.SourceFile -) { - const { start, end } = getStartEnd(ts, node, sourceFile); - return sourceFile.text.substring(start, end); -} diff --git a/packages/language-core/lib/plugins/vue-tsx.ts b/packages/language-core/lib/plugins/vue-tsx.ts index cf367a73d9..b64fe98cb6 100644 --- a/packages/language-core/lib/plugins/vue-tsx.ts +++ b/packages/language-core/lib/plugins/vue-tsx.ts @@ -119,10 +119,10 @@ function createTsx( template: _sfc.template, shouldGenerateScopedClasses: shouldGenerateScopedClasses(), stylesScopedClasses: stylesScopedClasses(), - scriptSetupImportComponentNames: scriptSetupImportComponentNames(), hasDefineSlots: hasDefineSlots(), slotsAssignName: slotsAssignName(), propsAssignName: propsAssignName(), + bindingTypes: bindingTypes(), }); let current = codegen.next(); @@ -139,15 +139,9 @@ function createTsx( }; }); const hasDefineSlots = computed(() => !!scriptSetupRanges()?.slots.define); - const scriptSetupImportComponentNames = computed>(oldNames => { - const newNames = scriptSetupRanges()?.importComponentNames ?? new Set(); - if (newNames && oldNames && twoSetsEqual(newNames, oldNames)) { - return oldNames; - } - return newNames; - }); const slotsAssignName = computed(() => scriptSetupRanges()?.slots.name); const propsAssignName = computed(() => scriptSetupRanges()?.props.name); + const bindingTypes = computed(() => scriptSetupRanges()?.bindings.bindingTypes ?? scriptRanges()?.bindings.bindingTypes); const generatedScript = computed(() => { const codes: Code[] = []; const linkedCodeMappings: Mapping[] = []; @@ -190,15 +184,3 @@ function createTsx( generatedTemplate, }; } - -function twoSetsEqual(a: Set, b: Set) { - if (a.size !== b.size) { - return false; - } - for (const file of a) { - if (!b.has(file)) { - return false; - } - } - return true; -} diff --git a/packages/language-core/lib/utils/parseBindings.ts b/packages/language-core/lib/utils/parseBindings.ts new file mode 100644 index 0000000000..20834e0283 --- /dev/null +++ b/packages/language-core/lib/utils/parseBindings.ts @@ -0,0 +1,262 @@ +import type * as ts from 'typescript'; +import type { TextRange, VueCompilerOptions } from '../types'; +import { injectTscApiShim } from './tscApiShim'; + +export enum BindingTypes { + NoUnref = 2 << 0, + NeedUnref = 2 << 1, + DirectAccess = 2 << 2, + Component = 2 << 3, +} + +export function parseBindings( + ts: typeof import('typescript'), + sourceFile: ts.SourceFile, + vueCompilerOptions?: VueCompilerOptions, +) { + ts = injectTscApiShim(ts); + const bindingRanges: TextRange[] = []; + // `bindingTypes` may include some bindings that are not in `bindingRanges`, such as `foo` in `defineProps({ foo: Number })` + const bindingTypes = new Map(); + const vueImportAliases: Record = { + ref: 'ref', + computed: 'computed', + shallowRef: 'shallowRef', + customRef: 'customRef', + toRef: 'toRef', + }; + + ts.forEachChild(sourceFile, node => { + // TODO: User may use package name alias then the import specifier may not be `vue` + if (ts.isImportDeclaration(node) && _getNodeText(node.moduleSpecifier) === vueCompilerOptions?.lib) { + const namedBindings = node.importClause?.namedBindings; + if (namedBindings && ts.isNamedImports(namedBindings)) { + for (const element of namedBindings.elements) { + vueImportAliases[_getNodeText(element.name)] = element.propertyName ? _getNodeText(element.propertyName) : _getNodeText(element.name); + } + } + } + }); + + ts.forEachChild(sourceFile, node => { + if (ts.isVariableStatement(node)) { + for (const decl of node.declarationList.declarations) { + const declList = node.declarationList; + + worker(decl.name, true); + function worker(_node: ts.Node, root = false) { + if (ts.isIdentifier(_node)) { + const name = _getNodeText(_node); + bindingRanges.push(_getStartEnd(_node)); + if (root) { + if (declList.flags & ts.NodeFlags.Const) { + if (decl.initializer) { + if (ts.isCallExpression(decl.initializer)) { + const callee = _getNodeText(decl.initializer.expression); + if ( + callee === vueImportAliases.ref + || callee === vueImportAliases.computed + || callee === vueImportAliases.shallowRef + || callee === vueImportAliases.customRef + || callee === vueImportAliases.toRef + ) { + bindingTypes.set(name, BindingTypes.NeedUnref); + } + else if (vueCompilerOptions?.macros.defineProps.includes(callee)) { + bindingTypes.set(name, BindingTypes.DirectAccess); + if (decl.initializer.typeArguments?.length === 1) { + const typeNode = decl.initializer.typeArguments[0]; + if (ts.isTypeLiteralNode(typeNode)) { + for (const prop of typeNode.members) { + if (ts.isPropertySignature(prop)) { + bindingTypes.set(_getNodeText(prop.name), BindingTypes.NoUnref); + } + } + } + } + else if (decl.initializer.arguments.length === 1) { + const arg = decl.initializer.arguments[0]; + if (ts.isObjectLiteralExpression(arg)) { + for (const prop of arg.properties) { + if (ts.isPropertyAssignment(prop)) { + bindingTypes.set(_getNodeText(prop.name), BindingTypes.NoUnref); + } + } + } + else if (ts.isArrayLiteralExpression(arg)) { + for (const prop of arg.elements) { + if (ts.isStringLiteral(prop)) { + bindingTypes.set(_getNodeText(prop), BindingTypes.NoUnref); + } + } + } + } + } + } + else { + bindingTypes.set( + name, + canNeverBeRef(decl.initializer, vueImportAliases.reactive) ? BindingTypes.NoUnref : BindingTypes.NeedUnref + ); + } + } + else { + bindingTypes.set(name, BindingTypes.NeedUnref); + } + } + // let a = 1; + else { + bindingTypes.set(name, BindingTypes.NeedUnref); + } + } + } + // { ? } = ... + // [ ? ] = ... + else if (ts.isObjectBindingPattern(_node) || ts.isArrayBindingPattern(_node)) { + for (const property of _node.elements) { + if (ts.isBindingElement(property)) { + worker(property.name); + } + } + } + // { foo: ? } = ... + else if (ts.isPropertyAssignment(_node)) { + worker(_node.initializer); + } + // { foo } = ... + else if (ts.isShorthandPropertyAssignment(_node)) { + addBinding(_node.name, BindingTypes.NeedUnref); + } + // { ...? } = ... + // [ ...? ] = ... + else if (ts.isSpreadAssignment(_node) || ts.isSpreadElement(_node)) { + worker(_node.expression); + } + } + } + } + else if ( + ts.isFunctionDeclaration(node) + || ts.isClassDeclaration(node) + || ts.isEnumDeclaration(node) + ) { + if (node.name) { + addBinding(node.name, BindingTypes.NoUnref); + } + } + else if (ts.isImportDeclaration(node)) { + if (node.importClause && !node.importClause.isTypeOnly) { + if (node.importClause.name) { + const name = _getNodeText(node.importClause.name); + bindingRanges.push(_getStartEnd(node.importClause.name)); + if ( + ts.isStringLiteral(node.moduleSpecifier) + && !node.importClause.isTypeOnly + && vueCompilerOptions?.extensions.some(ext => _getNodeText(node.moduleSpecifier).slice(1, -1).endsWith(ext)) + ) { + bindingTypes.set(name, BindingTypes.NoUnref | BindingTypes.Component); + } + else { + bindingTypes.set(name, BindingTypes.NeedUnref); + } + } + if (node.importClause.namedBindings) { + if (ts.isNamedImports(node.importClause.namedBindings)) { + for (const element of node.importClause.namedBindings.elements) { + addBinding(element.name, BindingTypes.NeedUnref); + } + } + else if (ts.isNamespaceImport(node.importClause.namedBindings)) { + addBinding(node.importClause.namedBindings.name, BindingTypes.NoUnref); + } + } + } + } + }); + return { bindingRanges, bindingTypes }; + + function _getStartEnd(node: ts.Node) { + return getStartEnd(ts, node, sourceFile); + } + function _getNodeText(node: ts.Node) { + return getNodeText(ts, node, sourceFile); + } + function addBinding(node: ts.Node, bindingType: BindingTypes) { + bindingRanges.push(_getStartEnd(node)); + bindingTypes.set(_getNodeText(node), bindingType); + } + function getValueNode(node: ts.Node) { + if ( + ts.isAsExpression(node) + || ts.isSatisfiesExpression(node) + || ts.isTypeAssertionExpression(node) + || ts.isParenthesizedExpression(node) + || ts.isNonNullExpression(node) + || ts.isExpressionWithTypeArguments(node) + ) { + return getValueNode(node.expression); + } + else if (ts.isCommaListExpression(node)) { + return getValueNode(node.elements[node.elements.length - 1]); + } + return node; + } + function canNeverBeRef(node: ts.Node, userReactiveImport?: string): boolean { + node = getValueNode(node); + if (isCallOf(node, userReactiveImport)) { + return true; + } + else if ( + ts.isPrefixUnaryExpression(node) + || ts.isPostfixUnaryExpression(node) + || ts.isArrayLiteralExpression(node) + || ts.isObjectLiteralExpression(node) + || ts.isFunctionExpression(node) + || ts.isArrowFunction(node) + || ts.isClassExpression(node) + || ts.isTaggedTemplateExpression(node) + || ts.isNoSubstitutionTemplateLiteral(node) + || ts.isLiteralExpression(node) + || node.kind === ts.SyntaxKind.TrueKeyword + || node.kind === ts.SyntaxKind.FalseKeyword + || node.kind === ts.SyntaxKind.NullKeyword + ) { + return true; + } + else if (ts.isBinaryExpression(node)) { + return canNeverBeRef(node.left, userReactiveImport) && canNeverBeRef(node.right, userReactiveImport); + } + else if (ts.isConditionalExpression(node)) { + return canNeverBeRef(node.whenTrue, userReactiveImport) && canNeverBeRef(node.whenFalse, userReactiveImport); + } + return false; + } + function isCallOf(node: ts.Node, userReactiveImport?: string): boolean { + if (ts.isCallExpression(node)) { + if (ts.isIdentifier(node.expression) && _getNodeText(node.expression) === userReactiveImport) { + return true; + } + } + return false; + } +} + +export function getStartEnd( + ts: typeof import('typescript'), + node: ts.Node, + sourceFile: ts.SourceFile +) { + return { + start: (ts as any).getTokenPosOfNode(node, sourceFile) as number, + end: node.end, + }; +} + +export function getNodeText( + ts: typeof import('typescript'), + node: ts.Node, + sourceFile: ts.SourceFile +) { + const { start, end } = getStartEnd(ts, node, sourceFile); + return sourceFile.text.substring(start, end); +} diff --git a/packages/language-core/lib/utils/tscApiShim.ts b/packages/language-core/lib/utils/tscApiShim.ts new file mode 100644 index 0000000000..3268e40bb8 --- /dev/null +++ b/packages/language-core/lib/utils/tscApiShim.ts @@ -0,0 +1,32 @@ +import type * as ts from 'typescript'; + +/** + * Provide missing functions in tsc context. + */ +export function injectTscApiShim(ts: typeof import('typescript')) { + function isAsExpression(node: ts.Node): node is ts.AsExpression { + return node.kind === ts.SyntaxKind.AsExpression; + } + function isTypeAssertionExpression(node: ts.Node): node is ts.TypeAssertion { + return node.kind === ts.SyntaxKind.TypeAssertionExpression; + } + function isTemplateExpression(node: ts.Node): node is ts.TemplateExpression { + return node.kind === ts.SyntaxKind.TemplateExpression; + } + + return new Proxy(ts, { + get(target, key) { + if (key === 'isAsExpression') { + return isAsExpression; + } + else if (key === 'isTypeAssertionExpression') { + return isTypeAssertionExpression; + } + else if (key === 'isTemplateExpression') { + return isTemplateExpression; + } + + return target[key as keyof typeof target]; + } + }); +}; diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index 50aa71449e..e07c1e84a9 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -451,7 +451,7 @@ export function create( } } - for (const binding of scriptSetupRanges?.bindings ?? []) { + for (const binding of scriptSetupRanges?.bindings.bindingRanges ?? []) { const name = vueCode.sfc.scriptSetup!.content.substring(binding.start, binding.end); if (casing.tag === TagNameCasing.Kebab) { names.add(hyphenateTag(name)); @@ -501,8 +501,8 @@ export function create( return []; } let ctxVars = [ - ..._tsCodegen.scriptRanges()?.bindings.map(binding => vueCode.sfc.script!.content.substring(binding.start, binding.end)) ?? [], - ..._tsCodegen.scriptSetupRanges()?.bindings.map(binding => vueCode.sfc.scriptSetup!.content.substring(binding.start, binding.end)) ?? [], + ..._tsCodegen.scriptRanges()?.bindings.bindingRanges.map(binding => vueCode.sfc.script!.content.substring(binding.start, binding.end)) ?? [], + ..._tsCodegen.scriptSetupRanges()?.bindings.bindingRanges.map(binding => vueCode.sfc.scriptSetup!.content.substring(binding.start, binding.end)) ?? [], ...templateContextProps, ]; ctxVars = [...new Set(ctxVars)];