diff --git a/extensions/vscode/lib/reactivityVisualization.ts b/extensions/vscode/lib/reactivityVisualization.ts index c1ee7a0b46..d8f0897be8 100644 --- a/extensions/vscode/lib/reactivityVisualization.ts +++ b/extensions/vscode/lib/reactivityVisualization.ts @@ -1,4 +1,5 @@ import type { getReactiveReferences } from '@vue/typescript-plugin/lib/requests/getReactiveReferences'; +import type * as ts from 'typescript'; import * as vscode from 'vscode'; import { config } from './config'; @@ -111,14 +112,11 @@ export function activate( } } -function getFlatRanges(document: vscode.TextDocument, ranges: { - start: number; - end: number; -}[]) { +function getFlatRanges(document: vscode.TextDocument, ranges: ts.TextRange[]) { const documentRanges = ranges .map(range => new vscode.Range( - document.positionAt(range.start).line, + document.positionAt(range.pos).line, 0, document.positionAt(range.end).line, 0, diff --git a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts index d228d0a1d3..f918fe3796 100644 --- a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts +++ b/packages/typescript-plugin/lib/requests/getReactiveReferences.ts @@ -1,30 +1,12 @@ import { createProxyLanguageService, decorateLanguageServiceHost } from '@volar/typescript'; -import { collectBindingRanges, hyphenateAttr, type Language, type SourceScript } from '@vue/language-core'; +import { type Language, type SourceScript } from '@vue/language-core'; +import { createAnalyzer } from 'laplacenoma'; +import * as rulesVue from 'laplacenoma/rules/vue'; import type * as ts from 'typescript'; -const enum ReactiveAccessType { - ValueProperty, - AnyProperty, - Call, -} - -interface TSNode { - ast: ts.Node; - start: number; - end: number; -} - -interface ReactiveNode { - isDependency: boolean; - isDependent: boolean; - binding?: TSNode & { - accessTypes: ReactiveAccessType[]; - }; - accessor?: TSNode & { - requiredAccess: boolean; - }; - callback?: TSNode; -} +const analyzer = createAnalyzer({ + rules: rulesVue, +}); let currentVersion = -1; let currentFileName = ''; @@ -32,8 +14,6 @@ let currentSnapshot: ts.IScriptSnapshot | undefined; let languageService: ts.LanguageService | undefined; let languageServiceHost: ts.LanguageServiceHost | undefined; -const analyzeCache = new WeakMap>(); - export function getReactiveReferences( ts: typeof import('typescript'), language: Language, @@ -63,640 +43,23 @@ export function getReactiveReferences( proxied.initialize(language); languageService = proxied.proxy; } - return getReactiveReferencesWorker(ts, language, languageService, sourceScript, position, leadingOffset); -} -function getReactiveReferencesWorker( - ts: typeof import('typescript'), - language: Language, - languageService: ts.LanguageService, - sourceScript: SourceScript, - position: number, - leadingOffset: number, -) { + const sourceFile = languageService.getProgram()!.getSourceFile(sourceScript.id)!; const serviceScript = sourceScript.generated?.languagePlugin.typescript?.getServiceScript( sourceScript.generated.root, ); const map = serviceScript ? language.maps.get(serviceScript.code, sourceScript) : undefined; const toSourceRange = map - ? (start: number, end: number) => { - for (const [mappedStart, mappedEnd] of map.toSourceRange(start - leadingOffset, end - leadingOffset, false)) { - return { start: mappedStart, end: mappedEnd }; - } - } - : (start: number, end: number) => ({ start, end }); - - const toSourceNode = (node: ts.Node, endNode = node) => { - const sourceRange = toSourceRange(node.getStart(sourceFile), endNode.end); - if (sourceRange) { - return { ...sourceRange, ast: node }; - } - }; - - const sourceFile = languageService.getProgram()!.getSourceFile(sourceScript.id)!; - - if (!analyzeCache.has(sourceFile)) { - analyzeCache.set(sourceFile, analyze(ts, sourceFile, toSourceRange, toSourceNode)); - } - - const { - signals, - allValuePropertyAccess, - allPropertyAccess, - allFunctionCalls, - } = analyzeCache.get(sourceFile)!; - - const info = findSignalByBindingRange(position) ?? findSignalByCallbackRange(position); - if (!info) { - return; - } - - const dependents = info.binding ? findDependents(info.binding.ast, info.binding.accessTypes) : []; - const dependencies = findDependencies(info); - - if ((!info.isDependent && !dependents.length) || (!info.isDependency && !dependencies.length)) { - return; - } - - const dependencyRanges: { start: number; end: number }[] = []; - const dependentRanges: { start: number; end: number }[] = []; - - for (const dependency of dependencies) { - let { ast } = dependency; - if (ts.isBlock(ast) && ast.statements.length) { - const sourceRange = toSourceNode( - ast.statements[0]!, - ast.statements[ast.statements.length - 1], - ); - if (sourceRange) { - dependencyRanges.push({ start: sourceRange.start, end: sourceRange.end }); + ? (pos: number, end: number) => { + for (const [mappedStart, mappedEnd] of map.toSourceRange(pos - leadingOffset, end - leadingOffset, false)) { + return { pos: mappedStart, end: mappedEnd }; } } - else { - dependencyRanges.push({ start: dependency.start, end: dependency.end }); - } - } - for (const { callback } of dependents) { - if (!callback) { - continue; - } - if (ts.isBlock(callback.ast) && callback.ast.statements.length) { - const { statements } = callback.ast; - const sourceRange = toSourceNode( - statements[0]!, - statements[statements.length - 1], - ); - if (sourceRange) { - dependentRanges.push({ start: sourceRange.start, end: sourceRange.end }); - } - } - else { - dependentRanges.push({ start: callback.start, end: callback.end }); - } - } - - return { dependencyRanges, dependentRanges }; - - function findDependencies(signal: ReactiveNode, visited = new Set()) { - if (visited.has(signal)) { - return []; - } - visited.add(signal); - - const nodes: TSNode[] = []; - let hasDependency = signal.isDependency; + : (pos: number, end: number) => ({ pos, end }); - if (signal.accessor) { - const { requiredAccess } = signal.accessor; - visit(signal.accessor, requiredAccess); - signal.accessor.ast.forEachChild(child => { - const childRange = toSourceNode(child); - if (childRange) { - visit( - childRange, - requiredAccess, - ); - } - }); - } - - if (!hasDependency) { - return []; - } - - return nodes; - - function visit(node: TSNode, requiredAccess: boolean, parentIsPropertyAccess = false) { - if (!requiredAccess) { - if (!parentIsPropertyAccess && ts.isIdentifier(node.ast)) { - const definition = languageService.getDefinitionAtPosition(sourceFile.fileName, node.start); - for (const info of definition ?? []) { - if (info.fileName !== sourceFile.fileName) { - continue; - } - const signal = findSignalByBindingRange(info.textSpan.start); - if (!signal) { - continue; - } - if (signal.binding) { - nodes.push(signal.binding); - hasDependency ||= signal.isDependency; - } - if (signal.callback) { - nodes.push(signal.callback); - } - const deps = findDependencies(signal, visited); - nodes.push(...deps); - hasDependency ||= deps.length > 0; - } - } - } - else if ( - ts.isPropertyAccessExpression(node.ast) || ts.isElementAccessExpression(node.ast) - || ts.isCallExpression(node.ast) - ) { - const definition = languageService.getDefinitionAtPosition( - sourceFile.fileName, - node.start, - ); - for (const info of definition ?? []) { - if (info.fileName !== sourceFile.fileName) { - continue; - } - const signal = findSignalByBindingRange(info.textSpan.start); - if (!signal) { - continue; - } - const oldSize = nodes.length; - if (signal.binding) { - for (const accessType of signal.binding.accessTypes) { - if (ts.isPropertyAccessExpression(node.ast)) { - if (accessType === ReactiveAccessType.ValueProperty && node.ast.name.text === 'value') { - nodes.push(signal.binding); - hasDependency ||= signal.isDependency; - } - if (accessType === ReactiveAccessType.AnyProperty && node.ast.name.text !== '') { - nodes.push(signal.binding); - hasDependency ||= signal.isDependency; - } - } - else if (ts.isElementAccessExpression(node.ast)) { - if (accessType === ReactiveAccessType.AnyProperty) { - nodes.push(signal.binding); - hasDependency ||= signal.isDependency; - } - } - else if (ts.isCallExpression(node.ast)) { - if (accessType === ReactiveAccessType.Call) { - nodes.push(signal.binding); - hasDependency ||= signal.isDependency; - } - } - } - } - const signalDetected = nodes.length > oldSize; - if (signalDetected) { - if (signal.callback) { - nodes.push(signal.callback); - } - const deps = findDependencies(signal, visited); - nodes.push(...deps); - hasDependency ||= deps.length > 0; - } - } - } - node.ast.forEachChild(child => { - const childRange = toSourceNode(child); - if (childRange) { - visit( - childRange, - requiredAccess, - ts.isPropertyAccessExpression(node.ast) || ts.isElementAccessExpression(node.ast), - ); - } - }); - } - } - - function findDependents(node: ts.Node, trackKinds: ReactiveAccessType[], visited = new Set()) { - return collectBindingRanges(ts, node, sourceFile) - .map(range => { - const sourceRange = toSourceRange(range.start, range.end); - if (sourceRange) { - return findDependentsWorker(sourceRange.start, trackKinds, visited); - } - return []; - }) - .flat(); - } - - function findDependentsWorker(pos: number, accessTypes: ReactiveAccessType[], visited = new Set()) { - if (visited.has(pos)) { - return []; - } - visited.add(pos); - - const references = languageService.findReferences(sourceFile.fileName, pos); - if (!references) { - return []; - } - const result: typeof signals = []; - for (const reference of references) { - for (const reference2 of reference.references) { - if (reference2.fileName !== sourceFile.fileName) { - continue; - } - const effect = findSignalByAccessorRange(reference2.textSpan.start); - if (effect?.accessor) { - let match = false; - if (effect.accessor.requiredAccess) { - for (const accessType of accessTypes) { - if (accessType === ReactiveAccessType.AnyProperty) { - match ||= allPropertyAccess.has(reference2.textSpan.start + reference2.textSpan.length); - } - else if (accessType === ReactiveAccessType.ValueProperty) { - match ||= allValuePropertyAccess.has(reference2.textSpan.start + reference2.textSpan.length); - } - else { - match ||= allFunctionCalls.has(reference2.textSpan.start + reference2.textSpan.length); - } - } - } - if (match) { - let hasDependent = effect.isDependent; - if (effect.binding) { - const dependents = findDependents(effect.binding.ast, effect.binding.accessTypes, visited); - result.push(...dependents); - hasDependent ||= dependents.length > 0; - } - if (hasDependent) { - result.push(effect); - } - } - } - } - } - return result; - } - - function findSignalByBindingRange(position: number): ReactiveNode | undefined { - return signals.find(ref => - ref.binding && ref.binding.start <= position - && ref.binding.end >= position - ); - } - - function findSignalByCallbackRange(position: number): ReactiveNode | undefined { - return signals.filter(ref => - ref.callback && ref.callback.start <= position - && ref.callback.end >= position - ).sort((a, b) => (a.callback!.end - a.callback!.start) - (b.callback!.end - b.callback!.start))[0]; - } - - function findSignalByAccessorRange(position: number): ReactiveNode | undefined { - return signals.filter(ref => - ref.accessor && ref.accessor.start <= position - && ref.accessor.end >= position - ).sort((a, b) => (a.accessor!.end - a.accessor!.start) - (b.accessor!.end - b.accessor!.start))[0]; - } -} - -function analyze( - ts: typeof import('typescript'), - sourceFile: ts.SourceFile, - toSourceRange: (start: number, end: number) => { start: number; end: number } | undefined, - toSourceNode: (node: ts.Node) => { ast: ts.Node; start: number; end: number } | undefined, -) { - const signals: ReactiveNode[] = []; - const allValuePropertyAccess = new Set(); - const allPropertyAccess = new Set(); - const allFunctionCalls = new Set(); - - sourceFile.forEachChild(function visit(node) { - if (ts.isVariableDeclaration(node)) { - if (node.initializer && ts.isCallExpression(node.initializer)) { - const call = node.initializer; - if (ts.isIdentifier(call.expression)) { - const callName = call.expression.escapedText as string; - if ( - callName === 'ref' || callName === 'shallowRef' || callName === 'toRef' || callName === 'useTemplateRef' - || callName === 'defineModel' - ) { - const nameRange = toSourceNode(node.name); - if (nameRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding: { - ...nameRange, - accessTypes: [ReactiveAccessType.ValueProperty], - }, - }); - } - } - else if ( - callName === 'reactive' || callName === 'shallowReactive' || callName === 'defineProps' - || callName === 'withDefaults' - ) { - const nameRange = toSourceNode(node.name); - if (nameRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding: { - ...nameRange, - accessTypes: [ReactiveAccessType.AnyProperty], - }, - }); - } - } - // TODO: toRefs - } - } - } - else if (ts.isFunctionDeclaration(node)) { - if (node.name && node.body) { - const nameRange = toSourceNode(node.name); - const bodyRange = toSourceNode(node.body); - if (nameRange && bodyRange) { - signals.push({ - isDependency: false, - isDependent: false, - binding: { - ...nameRange, - accessTypes: [ReactiveAccessType.Call], - }, - accessor: { - ...bodyRange, - requiredAccess: true, - }, - callback: bodyRange, - }); - } - } - } - else if (ts.isVariableStatement(node)) { - for (const declaration of node.declarationList.declarations) { - const name = declaration.name; - const callback = declaration.initializer; - if ( - callback && ts.isIdentifier(name) && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) - ) { - const nameRange = toSourceNode(name); - const callbackRange = toSourceNode(callback); - if (nameRange && callbackRange) { - signals.push({ - isDependency: false, - isDependent: false, - binding: { - ...nameRange, - accessTypes: [ReactiveAccessType.Call], - }, - accessor: { - ...callbackRange, - requiredAccess: true, - }, - callback: callbackRange, - }); - } - } - } - } - else if (ts.isParameter(node)) { - if (node.type && ts.isTypeReferenceNode(node.type)) { - const typeName = node.type.typeName.getText(sourceFile); - if (typeName.endsWith('Ref')) { - const nameRange = toSourceNode(node.name); - if (nameRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding: { - ...nameRange, - accessTypes: [ReactiveAccessType.ValueProperty], - }, - }); - } - } - } - } - else if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) { - const call = node; - const callName = node.expression.escapedText as string; - if ((callName === 'effect' || callName === 'watchEffect') && call.arguments.length) { - const callback = call.arguments[0]!; - if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) { - const bodyRange = toSourceNode(callback.body); - if (bodyRange) { - signals.push({ - isDependency: false, - isDependent: true, - accessor: { - ...bodyRange, - requiredAccess: true, - }, - callback: bodyRange, - }); - } - } - } - if (callName === 'watch' && call.arguments.length >= 2) { - const depsCallback = call.arguments[0]!; - const effectCallback = call.arguments[1]!; - if (ts.isArrowFunction(effectCallback) || ts.isFunctionExpression(effectCallback)) { - if (ts.isArrowFunction(depsCallback) || ts.isFunctionExpression(depsCallback)) { - const depsBodyRange = toSourceNode(depsCallback.body); - const effectBodyRange = toSourceNode(effectCallback.body); - if (depsBodyRange && effectBodyRange) { - signals.push({ - isDependency: false, - isDependent: true, - accessor: { - ...depsBodyRange, - requiredAccess: true, - }, - callback: effectBodyRange, - }); - } - } - else { - const depsRange = toSourceNode(depsCallback); - const effectBodyRange = toSourceNode(effectCallback.body); - if (depsRange && effectBodyRange) { - signals.push({ - isDependency: false, - isDependent: true, - accessor: { - ...depsRange, - requiredAccess: false, - }, - callback: effectBodyRange, - }); - } - } - } - } - else if (hyphenateAttr(callName).startsWith('use-')) { - let binding: ReactiveNode['binding']; - if (ts.isVariableDeclaration(call.parent)) { - const nameRange = toSourceNode(call.parent.name); - if (nameRange) { - binding = { - ...nameRange, - accessTypes: [ReactiveAccessType.AnyProperty, ReactiveAccessType.Call], - }; - } - } - const callRange = toSourceNode(call); - if (callRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding, - accessor: { - ...callRange, - requiredAccess: false, - }, - }); - } - } - else if ((callName === 'computed' || hyphenateAttr(callName).endsWith('-computed')) && call.arguments.length) { - const arg = call.arguments[0]!; - if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) { - let binding: ReactiveNode['binding']; - if (ts.isVariableDeclaration(call.parent)) { - const nameRange = toSourceNode(call.parent.name); - if (nameRange) { - binding = { - ...nameRange, - accessTypes: [ReactiveAccessType.ValueProperty], - }; - } - } - const bodyRange = toSourceNode(arg.body); - if (bodyRange) { - signals.push({ - isDependency: true, - isDependent: true, - binding, - accessor: { - ...bodyRange, - requiredAccess: true, - }, - callback: bodyRange, - }); - } - } - else if (ts.isIdentifier(arg)) { - let binding: ReactiveNode['binding']; - if (ts.isVariableDeclaration(call.parent)) { - const nameRange = toSourceNode(call.parent.name); - if (nameRange) { - binding = { - ...nameRange, - accessTypes: [ReactiveAccessType.ValueProperty], - }; - } - } - const argRange = toSourceNode(arg); - if (argRange) { - signals.push({ - isDependency: true, - isDependent: false, - binding, - accessor: { - ...argRange, - requiredAccess: false, - }, - }); - } - } - else if (ts.isObjectLiteralExpression(arg)) { - for (const prop of arg.properties) { - if (prop.name?.getText(sourceFile) === 'get') { - let binding: ReactiveNode['binding']; - if (ts.isVariableDeclaration(call.parent)) { - const nameRange = toSourceNode(call.parent.name); - if (nameRange) { - binding = { - ...nameRange, - accessTypes: [ReactiveAccessType.ValueProperty], - }; - } - } - if (ts.isPropertyAssignment(prop)) { - const callback = prop.initializer; - if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) { - const bodyRange = toSourceNode(callback.body); - if (bodyRange) { - signals.push({ - isDependency: true, - isDependent: true, - binding, - accessor: { - ...bodyRange, - requiredAccess: true, - }, - callback: bodyRange, - }); - } - } - } - else if (ts.isMethodDeclaration(prop) && prop.body) { - const bodyRange = toSourceNode(prop.body); - if (bodyRange) { - signals.push({ - isDependency: true, - isDependent: true, - binding, - accessor: { - ...bodyRange, - requiredAccess: true, - }, - callback: bodyRange, - }); - } - } - } - } - } - } - } - node.forEachChild(visit); + return analyzer.analyze(sourceFile, position, { + typescript: ts, + languageService, + toSourceRange, }); - - sourceFile.forEachChild(function visit(node) { - if (ts.isPropertyAccessExpression(node)) { - const sourceRange = toSourceRange(node.expression.end, node.expression.end); - if (sourceRange) { - if (node.name.text === 'value') { - allValuePropertyAccess.add(sourceRange.end); - allPropertyAccess.add(sourceRange.end); - } - else if (node.name.text !== '') { - allPropertyAccess.add(sourceRange.end); - } - } - } - else if (ts.isElementAccessExpression(node)) { - const sourceRange = toSourceRange(node.expression.end, node.expression.end); - if (sourceRange) { - allPropertyAccess.add(sourceRange.end); - } - } - else if (ts.isCallExpression(node)) { - const sourceRange = toSourceRange(node.expression.end, node.expression.end); - if (sourceRange) { - allFunctionCalls.add(sourceRange.end); - } - } - node.forEachChild(visit); - }); - - return { - signals, - allValuePropertyAccess, - allPropertyAccess, - allFunctionCalls, - }; } diff --git a/packages/typescript-plugin/lib/requests/index.ts b/packages/typescript-plugin/lib/requests/index.ts index fc0ac8e9d6..97ed8c2b2d 100644 --- a/packages/typescript-plugin/lib/requests/index.ts +++ b/packages/typescript-plugin/lib/requests/index.ts @@ -15,12 +15,10 @@ export interface Requests { isRefAtPosition( fileName: string, position: number, - ): Response< - ReturnType - >; - getComponentDirectives(fileName: string): Response< - ReturnType - >; + ): Response>; + getComponentDirectives( + fileName: string, + ): Response>; getComponentEvents( fileName: string, tag: string, @@ -39,14 +37,23 @@ export interface Requests { fileName: string, tag: string, ): Response>; - getElementNames(fileName: string): Response>; + getElementNames( + fileName: string, + ): Response>; getReactiveReferences( fileName: string, position: number, ): Response>; - getDocumentHighlights(fileName: string, position: number): Response; - getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan): Response< - ts.Classifications - >; - getQuickInfoAtPosition(fileName: string, position: ts.LineAndCharacter): Response; + getDocumentHighlights( + fileName: string, + position: number, + ): Response; + getEncodedSemanticClassifications( + fileName: string, + span: ts.TextSpan, + ): Response; + getQuickInfoAtPosition( + fileName: string, + position: ts.LineAndCharacter, + ): Response; } diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index 4d736938ac..c2d1d4958f 100644 --- a/packages/typescript-plugin/package.json +++ b/packages/typescript-plugin/package.json @@ -16,6 +16,7 @@ "@volar/typescript": "2.4.23", "@vue/language-core": "3.0.7", "@vue/shared": "^3.5.0", + "laplacenoma": "^0.0.3", "path-browserify": "^1.0.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1cf891204..c66afbfe19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -289,6 +289,9 @@ importers: '@vue/shared': specifier: ^3.5.0 version: 3.5.13 + laplacenoma: + specifier: ^0.0.3 + version: 0.0.3 path-browserify: specifier: ^1.0.1 version: 1.0.1 @@ -2503,6 +2506,9 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + laplacenoma@0.0.3: + resolution: {integrity: sha512-fsQMsMozEzGg/DhQG73nf8Nzx0XvU8RJFRViYuocM6pQSa33lucnHvJzsb4udQUzH5p5zdX63t9qRahKAhWtiQ==} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -6238,6 +6244,8 @@ snapshots: kind-of@6.0.3: {} + laplacenoma@0.0.3: {} + leven@3.1.0: {} levn@0.4.1: