From 00709335996976a339a77e846cf698a27eafc33e Mon Sep 17 00:00:00 2001 From: cruldra Date: Tue, 13 Oct 2026 14:40:07 +1100 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20perf(vscode):=20=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E6=8C=89=E4=BD=9C=E7=94=A8=E5=9F=9F=E8=A7=84?= =?UTF-8?q?=E5=88=99=E8=AE=A4=E5=87=BA=E5=90=8D=E5=AD=97=E7=9A=84=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=EF=BC=8C=E6=89=BE=E5=88=AB=E7=9A=84=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=B5=B7=E7=9A=84=E4=B8=AD=E6=96=87=E5=90=8D=E6=97=B6=E5=90=8C?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E7=BB=91=E5=AE=9A=E5=8F=AA=E9=97=AE=E4=B8=80?= =?UTF-8?q?=E5=A4=84=E3=80=81=E6=9C=AC=E6=96=87=E4=BB=B6=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E7=9A=84=E4=B8=8D=E9=97=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vscode/src/codeReader/blockView.ts | 9 +- vscode/src/codeReader/contract.ts | 15 + .../src/codeReader/editor/documentMessage.ts | 14 +- .../codeReader/editor/previewEditor.test.ts | 3 + vscode/src/codeReader/editor/previewEditor.ts | 4 +- vscode/src/codeReader/languages/bindings.ts | 93 +++++ vscode/src/codeReader/languages/python.ts | 2 + .../languages/pythonBindings.test.ts | 162 +++++++++ .../codeReader/languages/pythonBindings.ts | 327 +++++++++++++++++ vscode/src/codeReader/languages/typescript.ts | 2 + .../languages/typescriptBindings.test.ts | 122 +++++++ .../languages/typescriptBindings.ts | 339 ++++++++++++++++++ vscode/src/codeReader/nameResolution.test.ts | 52 ++- vscode/src/codeReader/nameResolution.ts | 51 ++- 14 files changed, 1159 insertions(+), 36 deletions(-) create mode 100644 vscode/src/codeReader/languages/bindings.ts create mode 100644 vscode/src/codeReader/languages/pythonBindings.test.ts create mode 100644 vscode/src/codeReader/languages/pythonBindings.ts create mode 100644 vscode/src/codeReader/languages/typescriptBindings.test.ts create mode 100644 vscode/src/codeReader/languages/typescriptBindings.ts diff --git a/vscode/src/codeReader/blockView.ts b/vscode/src/codeReader/blockView.ts index 130ee71..06da02b 100644 --- a/vscode/src/codeReader/blockView.ts +++ b/vscode/src/codeReader/blockView.ts @@ -1,5 +1,5 @@ import type { Tree } from '@vscode/tree-sitter-wasm' -import type { BlockAttributes, BlockConcept, BlockKind, CodeBlock, DisplayName, EmbeddedCode, EmbeddedReading, HttpEndpoint, Inline, KindBadge, LanguageAdapter, LibraryRule, LocalSymbol, Markdown, Node, PreviewTable, PreviewText, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, SourceDocument, SourceRange, SourceSpan, StepBlocks, TextBlock, TokenRole, Translator } from './contract' +import type { BlockAttributes, BlockConcept, BlockKind, CodeBlock, DisplayName, EmbeddedCode, EmbeddedReading, HttpEndpoint, Inline, KindBadge, LanguageAdapter, LibraryRule, LocalSymbol, Markdown, NameBinding, Node, PreviewTable, PreviewText, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, SourceDocument, SourceRange, SourceSpan, StepBlocks, TextBlock, TokenRole, Translator } from './contract' import type { TreeSitterRuntime } from './runtime' import type { ImportedName } from './symbolLinks' import { blockAttributes } from './attributes' @@ -507,7 +507,8 @@ export function toBlockViews( /** * 一份源码的视图数据,和它里面起了中文名的定义(预览找用到它们的各处要用,见 displayNames.ts)、 * 语言插件自己认得出的名字(LanguageAdapter.symbols,语言扩展查不到定义、各处出现时用)、 - * 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义,见 symbolLinks.ts)、各块自己的名字和种类(调用的定义就在这份源码里时,分新建还是调用用它,见 NamedKind)。 + * 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义,见 symbolLinks.ts)、各块自己的名字和种类(调用的定义就在这份源码里时,分新建还是调用用它,见 NamedKind)、 + * 各个名字的绑定(找中文名时同一个绑定问一处,见 LanguageAdapter.bindings)。 */ export interface DocumentViews { blocks: BlockView[] @@ -515,6 +516,7 @@ export interface DocumentViews { symbols: LocalSymbol[] imports: ImportedName[] kinds: NamedKind[] + bindings: NameBinding[] } /** @@ -587,7 +589,8 @@ export async function readBlockViews( ...adapter.symbols?.(tree.rootNode, source, languageId) ?? [], ...[...parts.values()].flatMap(part => part.document.language.symbols?.(part.root, source, part.code.languageId) ?? []), ] - return { blocks, displayNames: names, symbols, imports: importedNames(importsIn(codeBlocks)), kinds: namedKinds(codeBlocks) } + const bindings = adapter.bindings?.(tree.rootNode, source, languageId) ?? [] + return { blocks, displayNames: names, symbols, imports: importedNames(importsIn(codeBlocks)), kinds: namedKinds(codeBlocks), bindings } } finally { for (const parsed of trees) diff --git a/vscode/src/codeReader/contract.ts b/vscode/src/codeReader/contract.ts index 72711a0..2a31bc7 100644 --- a/vscode/src/codeReader/contract.ts +++ b/vscode/src/codeReader/contract.ts @@ -458,6 +458,16 @@ export interface LocalSymbol { occurrences: SourceRange[] } +/** + * 一份源码里一个名字的一个绑定(参数、变量、函数、类、导入的名字……),照这门语言的作用域规则认:各处出现(名字的范围,含定义处) + * 指的是同一个定义,内层同名的参数、推导式变量这类遮住外层的是另一个绑定。local 是这份源码自己定义的(参数、赋值、def、class……), + * 定义一定在这份源码里;导入的、哪儿都没定义的(内置的)不是。 + */ +export interface NameBinding { + occurrences: SourceRange[] + local: boolean +} + export interface DeclaratorFacts { /** 变量名;解构声明是整个模式。 */ name: Node @@ -760,6 +770,11 @@ export interface LanguageAdapter { command?: (node: Node) => CommandFacts | undefined /** 一份源码里自己认得出的名字(见 LocalSymbol);VS Code 有这种语言的定义、高亮提供者时用不上,不填。 */ symbols?: (root: Node, source: string, languageId: string) => LocalSymbol[] + /** + * 一份源码里各个名字的绑定(见 NameBinding):宿主问语言扩展名字的定义时,同一个绑定只问一处,这份源码自己定义的不问。 + * 只报认得准的:拿不准的(模式匹配捕获的、跳过不看的声明里的、同一个名字既导入又赋值的……)不报,宿主照旧逐处问。不填就逐处问。 + */ + bindings?: (root: Node, source: string, languageId: string) => NameBinding[] /** 这份源码里嵌着的别的语言的代码(见 EmbeddedCode),块里的(TextBlock.embedded)、属性表里的(HealthFacts.command)都算;没有的语言不填。 */ embedded?: (root: Node, source: string, languageId: string) => EmbeddedCode[] /** 这门语言的代码被别的语言嵌着、当一串步骤执行时怎么分块(见 StepBlocks);不填就按 toBlocks 分、不算一步失败就停。 */ diff --git a/vscode/src/codeReader/editor/documentMessage.ts b/vscode/src/codeReader/editor/documentMessage.ts index 37cf637..2f82f0d 100644 --- a/vscode/src/codeReader/editor/documentMessage.ts +++ b/vscode/src/codeReader/editor/documentMessage.ts @@ -1,5 +1,5 @@ import type { NamedKind } from '../blockView' -import type { DisplayName, LocalSymbol, RuleContext, SourceDocument } from '../contract' +import type { DisplayName, LocalSymbol, NameBinding, RuleContext, SourceDocument } from '../contract' import type { CodeReaderRegistry } from '../registry' import type { TreeSitterRuntime } from '../runtime' import type { ImportedName } from '../symbolLinks' @@ -23,7 +23,8 @@ export interface DocumentSnapshot { /** * 发给预览的文档,和这份源码里起了中文名的定义(扩展端接着找用到它们的各处)、语言插件自己认得出的名字(LocalSymbol)、 - * 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义)、各块自己的名字和种类(调用的定义就在这份文档里时认种类用,见 NamedKind)。 + * 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义)、各块自己的名字和种类(调用的定义就在这份文档里时认种类用,见 NamedKind)、 + * 各个名字的绑定(找中文名时同一个绑定问一处,见 NameBinding)。 */ export interface DocumentRead { message: DocumentMessage @@ -31,6 +32,7 @@ export interface DocumentRead { symbols: LocalSymbol[] imports: ImportedName[] kinds: NamedKind[] + bindings: NameBinding[] } /** @@ -62,10 +64,10 @@ export async function documentMessage( } const adapter = registry.languageFor(languageId) if (!adapter) - return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [], symbols: [], imports: [], kinds: [] } + return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [], symbols: [], imports: [], kinds: [], bindings: [] } try { const rules = registry.rulesFor(languageId) - const { blocks, displayNames, symbols, imports, kinds } = await readBlockViews(runtime, adapter, translator, languageId, source, { + const { blocks, displayNames, symbols, imports, kinds, bindings } = await readBlockViews(runtime, adapter, translator, languageId, source, { ...(document.fileName && { fileName: document.fileName }), rules: { rules, report, lookup }, calleeKind, @@ -80,9 +82,9 @@ export async function documentMessage( }, }) const names = mergeAttributeNames([translator?.attributeNames, ...ruleAttributeNames(rules, translator?.displayLanguage, report)]) - return { message: { ...message, blocks: markEntryBlocks(blocks, source, document.entryNames ?? []), attributes: fileAttributes(blocks, names) }, displayNames, symbols, imports, kinds } + return { message: { ...message, blocks: markEntryBlocks(blocks, source, document.entryNames ?? []), attributes: fileAttributes(blocks, names) }, displayNames, symbols, imports, kinds, bindings } } catch (err) { - return { message: { ...message, error: err instanceof Error ? err.message : String(err) }, displayNames: [], symbols: [], imports: [], kinds: [] } + return { message: { ...message, error: err instanceof Error ? err.message : String(err) }, displayNames: [], symbols: [], imports: [], kinds: [], bindings: [] } } } diff --git a/vscode/src/codeReader/editor/previewEditor.test.ts b/vscode/src/codeReader/editor/previewEditor.test.ts index 475679c..6072693 100644 --- a/vscode/src/codeReader/editor/previewEditor.test.ts +++ b/vscode/src/codeReader/editor/previewEditor.test.ts @@ -45,6 +45,7 @@ vi.mock('./documentMessage', () => ({ symbols: [], imports: [], kinds: [], + bindings: [], })), })) /** 文档符号由各个测试给 */ @@ -542,6 +543,8 @@ describe('注释里不带路径的引用按它写在哪块的注释里找', () = displayNames: [], symbols: [], imports: [], + kinds: [], + bindings: [], }) as never) vi.mocked(documentSymbolNodes).mockResolvedValue([ symbol('AgentService', 100, 900, [symbol('get_agent_admitted', 200, 300), symbol('get_default_agent', 400, 500)]), diff --git a/vscode/src/codeReader/editor/previewEditor.ts b/vscode/src/codeReader/editor/previewEditor.ts index e73f5fd..63319e9 100644 --- a/vscode/src/codeReader/editor/previewEditor.ts +++ b/vscode/src/codeReader/editor/previewEditor.ts @@ -414,7 +414,7 @@ class Preview { const source = this.document.getText() const session = this.lookups.session() const callees = this.callees.session() - const { message, displayNames, symbols, imports, kinds } = await documentMessage({ languageId, version, getText: () => source, fileName: this.document.fileName, entryNames: this.owner.entries.namesOf(this.document.uri) }, this.registry, this.runtime, (text) => { + const { message, displayNames, symbols, imports, kinds, bindings } = await documentMessage({ languageId, version, getText: () => source, fileName: this.document.fileName, entryNames: this.owner.entries.namesOf(this.document.uri) }, this.registry, this.runtime, (text) => { logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` }) }, session.lookup, callees.calleeKind) if (request !== this.latest) @@ -434,7 +434,7 @@ class Preview { void this.diff.update(this.sent) // 文档在解析期间又改了的,等下一次发文档再查 if (version === this.document.version) { - this.names.resolve(version, report => resolveNames(displayNames, message.blocks, this.nameSources(version), report)) + this.names.resolve(version, report => resolveNames(displayNames, message.blocks, bindings, this.nameSources(version), report)) this.lookups.resolve(version, session.requests(), this.lookupSources(version)) this.callees.resolve(version, callees.requests(), offset => this.queued(version, offset, () => this.calleeKind(offset), undefined)) this.callTrees.documentSent() diff --git a/vscode/src/codeReader/languages/bindings.ts b/vscode/src/codeReader/languages/bindings.ts new file mode 100644 index 0000000..7726fe1 --- /dev/null +++ b/vscode/src/codeReader/languages/bindings.ts @@ -0,0 +1,93 @@ +import type { NameBinding, Node, SourceRange } from '../contract' + +/** 语法树里的名字节点(Python 只有 identifier;TypeScript 的类名是 type_identifier,解构出来的变量是 shorthand_property_identifier_pattern)。 */ +const NAME_TYPES = new Set(['identifier', 'type_identifier', 'shorthand_property_identifier_pattern']) + +/** 名字在一层作用域里是怎么绑定的:这份源码自己定义的(参数、赋值、def……),还是导入的。 */ +export type BindKind = 'local' | 'import' + +/** 一层作用域:绑定了哪些名字(各是怎么绑定的),哪些名字拿不准(解析到这层的不报)。 */ +export class Scope { + readonly binds = new Map() + readonly unsure = new Set() + + constructor(readonly kind: string, readonly parent: Scope | undefined, readonly id: number) {} + + bind(name: string, kind: BindKind): void { + listOf(this.binds, name).push(kind) + } +} + +/** 一处出现:名字在哪层作用域里出现、范围。 */ +interface Seen { + scope: Scope + name: string + range: SourceRange +} + +/** + * 认绑定的语言插件共用的:边走语法树边记下各层作用域、各处出现,走完按语言的规则(resolve)把每处出现落到一个绑定上,同一个绑定的归到一起。 + * resolve 交回绑定的键(同一个键是同一个绑定),拿不准的交回 undefined、不报。 + */ +export class BindingCollector { + private readonly seen: Seen[] = [] + private readonly scopes: Scope[] = [] + + scope(kind: string, parent: Scope | undefined): Scope { + const scope = new Scope(kind, parent, this.scopes.length) + this.scopes.push(scope) + return scope + } + + /** 名字节点 node 出现在 scope 里(用到它,或者它的定义处)。 */ + see(scope: Scope, node: Node): void { + this.seen.push({ scope, name: node.text, range: { startIndex: node.startIndex, endIndex: node.endIndex } }) + } + + /** 拿不准 node 里的名字:scope 里这些名字都不报。 */ + unsure(scope: Scope, node: Node): void { + if (NAME_TYPES.has(node.type)) + scope.unsure.add(node.text) + for (const child of node.namedChildren) { + if (child) + this.unsure(scope, child) + } + } + + /** + * 按 resolve 归到各个绑定:绑定只有本文件定义的是 local;只有一处导入的不是 local;哪儿都没绑定的(内置的)不是 local; + * 既导入又本文件定义、导入了几次的拿不准,不报。 + */ + bindings(resolve: (scope: Scope, name: string) => string | undefined): NameBinding[] { + const kinds = new Map() + for (const scope of this.scopes) { + for (const [name, list] of scope.binds) { + const key = resolve(scope, name) + if (key !== undefined) + listOf(kinds, key).push(...list) + } + } + const occurrences = new Map() + for (const { scope, name, range } of this.seen) { + const key = resolve(scope, name) + if (key !== undefined) + listOf(occurrences, key).push(range) + } + return [...occurrences].flatMap(([key, ranges]) => { + const list = kinds.get(key) ?? [] + const imports = list.filter(kind => kind === 'import').length + if (imports > 1 || (imports > 0 && imports < list.length)) + return [] + return [{ occurrences: ranges, local: list.length > 0 && imports === 0 }] + }) + } +} + +function listOf(map: Map, key: string): T[] { + let list = map.get(key) + if (!list) { + list = [] + map.set(key, list) + } + return list +} diff --git a/vscode/src/codeReader/languages/python.ts b/vscode/src/codeReader/languages/python.ts index af37b23..3939107 100644 --- a/vscode/src/codeReader/languages/python.ts +++ b/vscode/src/codeReader/languages/python.ts @@ -1,6 +1,7 @@ import type { Access, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, ElementFacts, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract' import type { CommentRules, ExceptionRules } from './shared' import * as path from 'node:path' +import { pythonBindings } from './pythonBindings' import { pythonNameReader } from './pythonNames' import { attachComments, children, compoundShape, insideOf, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren, withExceptions } from './shared' @@ -670,6 +671,7 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter { iteration: pythonIteration, call: pythonCall, collection: pythonCollection, + bindings: root => pythonBindings(root), displayNames: pythonNameReader, } } diff --git a/vscode/src/codeReader/languages/pythonBindings.test.ts b/vscode/src/codeReader/languages/pythonBindings.test.ts new file mode 100644 index 0000000..0cf3856 --- /dev/null +++ b/vscode/src/codeReader/languages/pythonBindings.test.ts @@ -0,0 +1,162 @@ +import type { NameBinding } from '../contract' +import { describe, expect, it } from 'vitest' +import { runtime, WASM_DIR } from '../testSupport' +import { createPythonAdapter } from './python' + +const py = createPythonAdapter(WASM_DIR) + +/** 读一份源码的绑定,按名字第几次出现(0 起,按整词数)问它落在哪个绑定上。 */ +async function bindingsOf(source: string): Promise<{ of: (name: string, n: number) => NameBinding | undefined, same: (name: string, ...ns: number[]) => boolean }> { + const tree = await runtime.parse(py, 'python', source) + let list: NameBinding[] + try { + list = py.bindings!(tree.rootNode, source, 'python') + } + finally { + tree.delete() + } + const at = (name: string, n: number): number => [...source.matchAll(new RegExp(`\\b${name}\\b`, 'g'))][n].index! + const of = (name: string, n: number): NameBinding | undefined => list.find(binding => binding.occurrences.some(range => range.startIndex === at(name, n) && range.endIndex === at(name, n) + name.length)) + return { of, same: (name, ...ns) => ns.every(n => of(name, n) !== undefined && of(name, n) === of(name, ns[0])) } +} + +describe('python 的名字绑定', () => { + it('同一个函数里参数的各处是一个绑定、本文件定义;内层函数同名参数是另一个;内层没重新绑定的用的是外层的;没定义过的不是本文件定义', async () => { + const { of, same } = await bindingsOf([ + 'def outer(session, other):', + ' use(session)', + ' def inner(session):', + ' return session', + ' def closure():', + ' return other', + ' return session', + ].join('\n')) + expect(same('session', 0, 1, 4)).toBe(true) + expect(same('session', 2, 3)).toBe(true) + expect(of('session', 2)).not.toBe(of('session', 0)) + expect(of('session', 0)?.local).toBe(true) + expect(same('other', 0, 1)).toBe(true) + expect(of('use', 0)?.local).toBe(false) + }) + + it('推导式变量是推导式自己的,第一个 for 的可迭代对象在外层、后面的在推导式里', async () => { + const { of, same } = await bindingsOf([ + 'def f(items, x):', + ' a = [x for x in items if x]', + ' b = {k: v for k, v in x.items()}', + ' c = [y for xs in items for y in xs]', + ' return x', + ].join('\n')) + expect(same('x', 1, 2, 3)).toBe(true) + expect(same('x', 0, 4, 5)).toBe(true) + expect(of('x', 1)).not.toBe(of('x', 0)) + expect(same('items', 0, 1, 3)).toBe(true) + // x.items 的属性名不是名字的绑定 + expect(of('items', 2)).toBeUndefined() + expect(same('xs', 0, 1)).toBe(true) + expect(same('k', 0, 1)).toBe(true) + }) + + it('global、nonlocal 照声明找到外层那个;没声明的同名赋值是函数自己的', async () => { + const { of, same } = await bindingsOf([ + 'counter = 0', + 'def bump():', + ' global counter', + ' counter += 1', + 'def make():', + ' total = 0', + ' def add(n):', + ' nonlocal total', + ' total += n', + ' return total', + 'def shadow():', + ' counter = 5', + ' return counter', + ].join('\n')) + expect(same('counter', 0, 1, 2)).toBe(true) + expect(same('counter', 3, 4)).toBe(true) + expect(of('counter', 3)).not.toBe(of('counter', 0)) + expect(same('total', 0, 1, 2, 3)).toBe(true) + }) + + it('类主体里绑定的名字:类主体里直接用的、推导式第一个可迭代对象是类的,方法里、推导式里面的是模块的', async () => { + const { of, same } = await bindingsOf([ + 'limit = 10', + 'class Config:', + ' limit = 5', + ' doubled = limit * 2', + ' values = [limit for _ in range(limit)]', + ' def get(self):', + ' return limit', + ].join('\n')) + expect(same('limit', 1, 2, 4)).toBe(true) + expect(same('limit', 0, 3, 5)).toBe(true) + expect(of('limit', 1)).not.toBe(of('limit', 0)) + }) + + it('lambda 的参数是它自己的,默认值在外层;海象运算符在推导式里绑定的是外面那个函数的;with、except 的 as 是本文件定义的', async () => { + const { of, same } = await bindingsOf([ + 'def f(x, data, path):', + ' g = lambda x, y=x: x + y', + ' if any((hit := d) for d in data):', + ' return hit', + ' with open(path) as fh:', + ' fh.read()', + ' try:', + ' pass', + ' except ValueError as err:', + ' log(err)', + ].join('\n')) + expect(same('x', 0, 2)).toBe(true) + expect(same('x', 1, 3)).toBe(true) + expect(of('x', 1)).not.toBe(of('x', 0)) + expect(same('hit', 0, 1)).toBe(true) + expect(same('fh', 0, 1)).toBe(true) + expect(same('err', 0, 1)).toBe(true) + expect(of('err', 0)?.local).toBe(true) + expect(of('ValueError', 0)?.local).toBe(false) + }) + + it('导入的名字不是本文件定义的;import a.b 绑定 a,as 前面的原名、模块路径不报;同一个名字导入两次、又导入又赋值的拿不准不报', async () => { + const { of, same } = await bindingsOf([ + 'import os.path', + 'from app.models import User, Role as R', + 'from x import dup', + 'from y import dup', + 'from z import mixed', + 'mixed = wrap(mixed)', + 'def f():', + ' return os, User, R, dup, mixed, path', + ].join('\n')) + expect(same('os', 0, 1)).toBe(true) + expect(of('os', 0)?.local).toBe(false) + expect(same('User', 0, 1)).toBe(true) + expect(same('R', 0, 1)).toBe(true) + expect(of('Role', 0)).toBeUndefined() + expect(of('models', 0)).toBeUndefined() + expect(of('path', 0)).toBeUndefined() + expect(of('dup', 2)).toBeUndefined() + expect(of('mixed', 3)).toBeUndefined() + }) + + it('拿不准的不报:模式匹配捕获的名字、类型参数;有 from m import * 时模块级的名字', async () => { + const matched = await bindingsOf([ + 'def f(value):', + ' match value:', + ' case Point(x=px):', + ' return px', + ' return value', + 'def g[T](item: T) -> T:', + ' return item', + ].join('\n')) + expect(matched.of('px', 0)).toBeUndefined() + expect(matched.of('px', 1)).toBeUndefined() + expect(matched.same('value', 0, 1, 2)).toBe(true) + expect(matched.of('T', 1)).toBeUndefined() + expect(matched.same('item', 0, 1)).toBe(true) + const starred = await bindingsOf('from m import *\nx = 1\ndef f():\n y = 2\n return x, y, print\n') + expect(starred.of('x', 0)).toBeUndefined() + expect(starred.of('print', 0)).toBeUndefined() + expect(starred.same('y', 0, 1)).toBe(true) + }) +}) diff --git a/vscode/src/codeReader/languages/pythonBindings.ts b/vscode/src/codeReader/languages/pythonBindings.ts new file mode 100644 index 0000000..78ce3ca --- /dev/null +++ b/vscode/src/codeReader/languages/pythonBindings.ts @@ -0,0 +1,327 @@ +import type { NameBinding, Node } from '../contract' +import type { BindKind, Scope } from './bindings' +import { BindingCollector } from './bindings' +import { named } from './shared' + +/** 推导式、生成器表达式:自成一层作用域,第一个 for 的可迭代对象在外层求值。 */ +const COMPREHENSIONS = new Set(['list_comprehension', 'set_comprehension', 'dictionary_comprehension', 'generator_expression']) + +/** 赋值目标里一层层往里拆的写法。 */ +const TARGET_LISTS = new Set(['pattern_list', 'tuple_pattern', 'list_pattern', 'tuple', 'list', 'expression_list', 'parenthesized_expression', 'list_splat_pattern', 'list_splat', 'as_pattern_target']) + +/** + * Python 的作用域规则:模块、函数(def、lambda)、类、推导式各是一层。函数里赋过值、当参数的名字是函数自己的, + * 用到的名字从里往外找(往外找时跳过类主体),global / nonlocal 声明的照声明找。参数的注解和默认值、装饰器、父类在外层求值。 + * 模式匹配捕获的、类型参数(PEP 695)、`type X = …` 拿不准;模块里有 `from m import *` 时模块级的名字都拿不准。 + */ +class PythonScopes { + readonly collector = new BindingCollector() + readonly module: Scope + /** 各层用 global / nonlocal 声明的名字 */ + private readonly declared = new Map>() + private star = false + + constructor() { + this.module = this.collector.scope('module', undefined) + } + + visit(node: Node, scope: Scope): void { + switch (node.type) { + case 'identifier': + this.collector.see(scope, node) + return + case 'function_definition': + this.function(node, scope) + return + case 'lambda': { + const inner = this.collector.scope('function', scope) + this.parameters(node.childForFieldName('parameters'), scope, inner) + this.visitField(node, 'body', inner) + return + } + case 'class_definition': + this.class(node, scope) + return + case 'assignment': + case 'augmented_assignment': + case 'for_statement': + this.withTarget(node, 'left', scope) + return + case 'as_pattern': + this.withTarget(node, 'alias', scope) + return + case 'named_expression': { + const name = node.childForFieldName('name') + if (name) + this.declare(name, this.functionScope(scope), 'local', scope) + this.visitField(node, 'value', scope) + return + } + case 'delete_statement': + for (const target of named(node)) + this.target(target, scope) + return + case 'import_statement': + case 'import_from_statement': + this.import(node, scope) + return + case 'future_import_statement': + return + case 'global_statement': + case 'nonlocal_statement': + for (const name of named(node)) { + this.declarations(scope).set(name.text, node.type === 'global_statement' ? 'global' : 'nonlocal') + this.collector.see(scope, name) + } + return + case 'keyword_argument': + this.visitField(node, 'value', scope) + return + case 'attribute': + this.visitField(node, 'object', scope) + return + case 'match_statement': + this.match(node, scope) + return + case 'type_alias_statement': + this.collector.unsure(scope, node) + return + } + if (COMPREHENSIONS.has(node.type)) { + this.comprehension(node, scope) + return + } + for (const child of named(node)) + this.visit(child, scope) + } + + bindings(): NameBinding[] { + return this.collector.bindings((scope, name) => this.resolve(scope, name)) + } + + private function(node: Node, scope: Scope): void { + const name = node.childForFieldName('name') + if (name) + this.declare(name, scope, 'local', scope) + const inner = this.collector.scope('function', scope) + const types = node.childForFieldName('type_parameters') + if (types) { + this.collector.unsure(scope, types) + this.collector.unsure(inner, types) + } + this.parameters(node.childForFieldName('parameters'), scope, inner) + this.visitField(node, 'return_type', scope) + this.visitField(node, 'body', inner) + } + + private class(node: Node, scope: Scope): void { + const name = node.childForFieldName('name') + if (name) + this.declare(name, scope, 'local', scope) + const inner = this.collector.scope('class', scope) + const types = node.childForFieldName('type_parameters') + if (types) { + this.collector.unsure(scope, types) + this.collector.unsure(inner, types) + } + this.visitField(node, 'superclasses', scope) + this.visitField(node, 'body', inner) + } + + /** 参数名是函数这一层的;注解、默认值在外层求值。 */ + private parameters(list: Node | null, outer: Scope, inner: Scope): void { + for (const parameter of named(list)) { + switch (parameter.type) { + case 'identifier': + case 'list_splat_pattern': + case 'dictionary_splat_pattern': + this.parameter(parameter, inner) + break + case 'typed_parameter': + this.parameter(named(parameter)[0], inner) + this.visitField(parameter, 'type', outer) + break + case 'default_parameter': + case 'typed_default_parameter': + this.parameter(parameter.childForFieldName('name'), inner) + this.visitField(parameter, 'type', outer) + this.visitField(parameter, 'value', outer) + break + case 'keyword_separator': + case 'positional_separator': + break + default: + this.collector.unsure(inner, parameter) + } + } + } + + private parameter(node: Node | null | undefined, scope: Scope): void { + const name = node?.type === 'list_splat_pattern' || node?.type === 'dictionary_splat_pattern' ? named(node)[0] : node + if (name?.type === 'identifier') + this.declare(name, scope, 'local', scope) + else if (node) + this.collector.unsure(scope, node) + } + + /** 推导式自成一层:变量是它的,第一个 for 的可迭代对象在外层求值,后面的在里面。 */ + private comprehension(node: Node, scope: Scope): void { + const inner = this.collector.scope('comprehension', scope) + let first = true + for (const child of named(node)) { + if (child.type !== 'for_in_clause') { + this.visit(child, inner) + continue + } + for (let i = 0; i < child.childCount; i++) { + const part = child.child(i) + if (!part?.isNamed) + continue + const field = child.fieldNameForChild(i) + if (field === 'left') + this.target(part, inner) + else + this.visit(part, field === 'right' && first ? scope : inner) + } + first = false + } + } + + /** 模式匹配:被匹配的照常算;case 里的模式(捕获的名字、值)拿不准,守卫和分支照常算。 */ + private match(node: Node, scope: Scope): void { + for (const child of named(node)) { + if (child.type !== 'block') { + this.visit(child, scope) + continue + } + for (const clause of named(child)) { + for (const part of named(clause)) { + if (part.type === 'case_pattern') + this.collector.unsure(scope, part) + else + this.visit(part, scope) + } + } + } + } + + /** 导入的名字:`import a.b` 绑定 a,`as` 后面的名字、`from m import x` 的 x;模块路径、`as` 前面的原名不算。 */ + private import(node: Node, scope: Scope): void { + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i) + if (!child) + continue + if (child.type === 'wildcard_import') + this.star = true + if (node.fieldNameForChild(i) !== 'name') + continue + const name = child.type === 'aliased_import' ? child.childForFieldName('alias') : named(child)[0] + if (name?.type === 'identifier') + this.declare(name, scope, 'import', scope) + } + } + + /** field 那个子节点是赋值目标,别的照常算。 */ + private withTarget(node: Node, field: string, scope: Scope): void { + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i) + if (!child?.isNamed) + continue + if (node.fieldNameForChild(i) === field) + this.target(child, scope) + else + this.visit(child, scope) + } + } + + /** 赋值目标:名字在这一层绑定;属性、下标是用到对象;认不出的写法拿不准。 */ + private target(node: Node, scope: Scope): void { + if (node.type === 'identifier') + this.declare(node, scope, 'local', scope) + else if (TARGET_LISTS.has(node.type)) + named(node).forEach(child => this.target(child, scope)) + else if (node.type === 'attribute' || node.type === 'subscript') + this.visit(node, scope) + else + this.collector.unsure(scope, node) + } + + /** name 在 owner 这一层绑定,出现在 scope 里。 */ + private declare(name: Node, owner: Scope, kind: BindKind, scope: Scope): void { + owner.bind(name.text, kind) + this.collector.see(scope, name) + } + + private visitField(node: Node, field: string, scope: Scope): void { + for (const child of node.childrenForFieldName(field)) { + if (child?.isNamed) + this.visit(child, scope) + } + } + + private declarations(scope: Scope): Map { + let found = this.declared.get(scope) + if (!found) { + found = new Map() + this.declared.set(scope, found) + } + return found + } + + /** 海象运算符绑定的那一层:往外第一层不是推导式的。 */ + private functionScope(scope: Scope): Scope { + let found = scope + while (found.kind === 'comprehension' && found.parent) + found = found.parent + return found + } + + /** scope 里用到的 name 落在哪个绑定上;拿不准的没有。 */ + private resolve(scope: Scope, name: string): string | undefined { + for (let current: Scope | undefined = scope; current; current = current.parent) { + if (current !== scope && current.kind === 'class') + continue + if (current.unsure.has(name)) + return undefined + const declared = this.declared.get(current)?.get(name) + if (declared === 'global') + return this.global(name) + if (declared === 'nonlocal') + return this.nonlocal(current, name) + if (current.binds.has(name)) + return current === this.module ? this.global(name) : `${current.id}:${name}` + } + return this.global(name) + } + + /** scope 里 nonlocal 声明的 name:往外找最近一层绑定它的函数,找不到拿不准。 */ + private nonlocal(scope: Scope, name: string): string | undefined { + for (let current = scope.parent; current && current !== this.module; current = current.parent) { + if (current.kind === 'class') + continue + if (current.unsure.has(name)) + return undefined + const declared = this.declared.get(current)?.get(name) + if (declared === 'nonlocal') + return this.nonlocal(current, name) + if (declared === undefined && current.binds.has(name)) + return `${current.id}:${name}` + if (declared !== undefined) + return undefined + } + return undefined + } + + /** 模块级的名字(含哪儿都没绑定的内置名字);有 `from m import *` 时拿不准。 */ + private global(name: string): string | undefined { + return this.star ? undefined : `:${name}` + } +} + +/** 一份 Python 源码里各个名字的绑定(见 NameBinding)。 */ +export function pythonBindings(root: Node): NameBinding[] { + const scopes = new PythonScopes() + for (const child of named(root)) + scopes.visit(child, scopes.module) + return scopes.bindings() +} diff --git a/vscode/src/codeReader/languages/typescript.ts b/vscode/src/codeReader/languages/typescript.ts index 201b9e9..57b2b41 100644 --- a/vscode/src/codeReader/languages/typescript.ts +++ b/vscode/src/codeReader/languages/typescript.ts @@ -2,6 +2,7 @@ import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts import type { CommentRules, ExceptionRules } from './shared' import * as path from 'node:path' import { attachComments, children, compoundShape, extendHeader, headerSpan, insideOf, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren, withExceptions } from './shared' +import { typescriptBindings } from './typescriptBindings' import { typescriptNameReader } from './typescriptNames' const GRAMMARS: Record = { @@ -662,6 +663,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter { freshDoc, call: typescriptCall, collection: typescriptCollection, + bindings: root => typescriptBindings(root), displayNames: typescriptNameReader, } } diff --git a/vscode/src/codeReader/languages/typescriptBindings.test.ts b/vscode/src/codeReader/languages/typescriptBindings.test.ts new file mode 100644 index 0000000..869f5a6 --- /dev/null +++ b/vscode/src/codeReader/languages/typescriptBindings.test.ts @@ -0,0 +1,122 @@ +import type { NameBinding } from '../contract' +import { describe, expect, it } from 'vitest' +import { runtime, WASM_DIR } from '../testSupport' +import { createTypeScriptAdapter } from './typescript' + +const ts = createTypeScriptAdapter(WASM_DIR) + +/** 读一份源码的绑定,按名字第几次出现(0 起,按整词数)问它落在哪个绑定上。 */ +async function bindingsOf(source: string, languageId = 'typescript'): Promise<{ of: (name: string, n: number) => NameBinding | undefined, same: (name: string, ...ns: number[]) => boolean }> { + const tree = await runtime.parse(ts, languageId, source) + let list: NameBinding[] + try { + list = ts.bindings!(tree.rootNode, source, languageId) + } + finally { + tree.delete() + } + const at = (name: string, n: number): number => [...source.matchAll(new RegExp(`(? list.find(binding => binding.occurrences.some(range => range.startIndex === at(name, n) && range.endIndex === at(name, n) + name.length)) + return { of, same: (name, ...ns) => ns.every(n => of(name, n) !== undefined && of(name, n) === of(name, ns[0])) } +} + +describe('typescript 的名字绑定', () => { + it('同一个函数里参数的各处是一个绑定、本文件定义;内层箭头函数同名参数是另一个;内层没重新绑定的用的是外层的', async () => { + const { of, same } = await bindingsOf([ + 'function outer(session: Session, other: number) {', + ' use(session)', + ' const inner = (session: string) => session', + ' function closure() { return other }', + ' return session', + '}', + ].join('\n')) + expect(same('session', 0, 1, 4)).toBe(true) + expect(same('session', 2, 3)).toBe(true) + expect(of('session', 2)).not.toBe(of('session', 0)) + expect(of('session', 0)?.local).toBe(true) + expect(same('other', 0, 1)).toBe(true) + expect(of('use', 0)?.local).toBe(false) + expect(of('Session', 0)).toBeUndefined() + }) + + it('块里的 let 遮住外层;var 提到函数体;for…of、catch 的变量各是一层', async () => { + const { of, same } = await bindingsOf([ + 'function f(items) {', + ' let x = 1', + ' if (items) {', + ' let x = 2', + ' use(x)', + ' var y = x', + ' }', + ' for (const x of items) use(x)', + ' try { run() } catch (x) { use(x) }', + ' return [x, y]', + '}', + ].join('\n')) + expect(same('x', 0, 8)).toBe(true) + expect(same('x', 1, 2, 3)).toBe(true) + expect(same('x', 4, 5)).toBe(true) + expect(same('x', 6, 7)).toBe(true) + expect(new Set([0, 1, 4, 6].map(n => of('x', n))).size).toBe(4) + expect(same('y', 0, 1)).toBe(true) + }) + + it('导入的(连 require 进来的)不是本文件定义的,as 前面的原名不报;解构出来的、默认值里用到的照常认', async () => { + const { of, same } = await bindingsOf([ + 'import Default, { a, b as c } from \'m\'', + 'import * as ns from \'n\'', + 'const { p, q: r = a, ...rest } = ns.obj', + 'const { helper } = require(\'./h\')', + 'export function g({ p: inner }: Props = fallback) { return [p, r, rest, c, Default, inner, helper] }', + ].join('\n')) + // require 进来的按导入算:跳到定义会跳进那个模块 + expect(same('helper', 0, 1)).toBe(true) + expect(of('helper', 0)?.local).toBe(false) + expect(same('a', 0, 1)).toBe(true) + expect(of('a', 0)?.local).toBe(false) + expect(of('b', 0)).toBeUndefined() + expect(same('c', 0, 1)).toBe(true) + expect(same('Default', 0, 1)).toBe(true) + expect(same('ns', 0, 1)).toBe(true) + expect(same('p', 0, 2)).toBe(true) + expect(of('p', 0)?.local).toBe(true) + expect(of('q', 0)).toBeUndefined() + expect(same('r', 0, 1)).toBe(true) + expect(same('rest', 0, 1)).toBe(true) + expect(same('inner', 0, 1)).toBe(true) + }) + + it('类:类声明的名字和方法里 new 它是同一个;类表达式、函数表达式自己的名字只在里面', async () => { + const { of, same } = await bindingsOf([ + 'class Box { static make() { return new Box(limit) } size = limit }', + 'const limit = 3', + 'const Named = class Inner { clone() { return new Inner() } }', + 'const fn = function self() { return self }', + 'const self = 1', + ].join('\n')) + expect(same('Box', 0, 1)).toBe(true) + expect(same('limit', 0, 1, 2)).toBe(true) + expect(same('Inner', 0, 1)).toBe(true) + expect(same('self', 0, 1)).toBe(true) + expect(of('self', 2)).not.toBe(of('self', 0)) + }) + + it('拿不准的不报:对象字面量的简写、JSX 标签名、块里的函数声明、命名空间的名字、declare 的', async () => { + const { of, same } = await bindingsOf([ + 'const value = 1', + 'const obj = { value }', + 'const el = ', + 'if (obj) { function hoisted() {} hoisted() }', + 'namespace Space { export const z = 1 }', + 'declare const ambient: number', + 'use(Space, ambient)', + ].join('\n'), 'typescriptreact') + expect(of('value', 1)).toBeUndefined() + expect(same('value', 0, 2)).toBe(true) + expect(of('Value', 0)).toBeUndefined() + expect(of('hoisted', 0)).toBeUndefined() + expect(of('hoisted', 1)).toBeUndefined() + expect(of('Space', 1)).toBeUndefined() + expect(of('ambient', 1)).toBeUndefined() + }) +}) diff --git a/vscode/src/codeReader/languages/typescriptBindings.ts b/vscode/src/codeReader/languages/typescriptBindings.ts new file mode 100644 index 0000000..db2b051 --- /dev/null +++ b/vscode/src/codeReader/languages/typescriptBindings.ts @@ -0,0 +1,339 @@ +import type { NameBinding, Node } from '../contract' +import type { BindKind, Scope } from './bindings' +import { BindingCollector } from './bindings' +import { named } from './shared' + +/** 只写类型的地方:名字是类型那一套的,不看。 */ +const TYPE_NODES = new Set([ + 'type_annotation', + 'type_arguments', + 'type_parameters', + 'type_alias_declaration', + 'interface_declaration', + 'implements_clause', + 'opting_type_annotation', + 'omitting_type_annotation', + 'asserts_annotation', + 'type_predicate_annotation', + 'abstract_method_signature', + 'method_signature', + 'property_signature', + 'index_signature', +]) + +/** 函数:自己一层放参数,函数体再一层。 */ +const FUNCTIONS = new Set(['function_declaration', 'generator_function_declaration', 'function_expression', 'function', 'generator_function', 'arrow_function', 'method_definition']) + +/** 自成一层块作用域(let、const、class 在里面)。 */ +const BLOCKS = new Set(['statement_block', 'switch_body', 'for_statement', 'for_in_statement']) + +/** 声明里的名字怎么记:declare 是在 owner 这一层绑定,assign 是给已有的名字赋值(解构赋值)。 */ +type PatternMode = { declare: Scope, kind: BindKind } | 'assign' + +/** + * TypeScript / JavaScript 的作用域规则:模块、函数(参数一层、函数体一层)、块各是一层。let、const、class、函数声明在所在的块, + * var 提到所在的函数体;用到的名字从里往外找。只认值那一套名字:写在类型里的、对象字面量里的简写属性、JSX 标签名不报; + * 命名空间、declare 声明、块里的函数声明、`import a = b.c` 拿不准。 + */ +class TypeScriptScopes { + readonly collector = new BindingCollector() + readonly module: Scope + + constructor() { + this.module = this.collector.scope('function', undefined) + } + + visit(node: Node, scope: Scope): void { + if (TYPE_NODES.has(node.type)) + return + if (FUNCTIONS.has(node.type)) { + this.function(node, scope) + return + } + switch (node.type) { + case 'identifier': + this.collector.see(scope, node) + return + case 'lexical_declaration': + case 'variable_declaration': + this.declaration(node, scope, node.type === 'variable_declaration' ? this.varScope(scope) : scope) + return + case 'class_declaration': + case 'abstract_class_declaration': + case 'class': + this.class(node, scope) + return + case 'enum_declaration': + case 'function_signature': { + const name = node.childForFieldName('name') + if (name) + this.declare(name, scope, 'local', scope) + return + } + case 'import_statement': + this.import(node, scope) + return + case 'ambient_declaration': + this.collector.unsure(scope, node) + return + case 'internal_module': + case 'module': + case 'import_alias': { + // 里面不看,只是它在这一层绑定的名字拿不准 + const name = node.childForFieldName('name') ?? named(node)[0] + if (name) + this.collector.unsure(scope, name) + return + } + case 'export_clause': + case 'namespace_export': + case 'shorthand_property_identifier': + return + case 'catch_clause': + this.catch(node, scope) + return + case 'assignment_expression': + this.assignment(node, scope) + return + case 'as_expression': + case 'satisfies_expression': { + const [value] = named(node) + if (value) + this.visit(value, scope) + return + } + case 'jsx_opening_element': + case 'jsx_closing_element': + case 'jsx_self_closing_element': + for (const attribute of node.childrenForFieldName('attribute')) { + if (attribute) + this.visit(attribute, scope) + } + return + } + if (BLOCKS.has(node.type)) { + this.block(node, scope) + return + } + for (const child of named(node)) + this.visit(child, scope) + } + + bindings(): NameBinding[] { + return this.collector.bindings((scope, name) => { + for (let current: Scope | undefined = scope; current; current = current.parent) { + if (current.unsure.has(name)) + return undefined + if (current.binds.has(name)) + return `${current.id}:${name}` + } + return `:${name}` + }) + } + + /** 块:let、const、class、for 里声明的变量在这一层;`for (… of …)` 的可迭代对象也在这一层(按规范它看得见循环变量)。 */ + private block(node: Node, scope: Scope): void { + const inner = this.collector.scope('block', scope) + if (node.type === 'for_in_statement') { + const kind = node.childForFieldName('kind')?.type + const left = node.childForFieldName('left') + if (left) + this.pattern(left, kind === undefined ? 'assign' : { declare: kind === 'var' ? this.varScope(scope) : inner, kind: 'local' }, inner) + for (const field of ['right', 'body']) { + const child = node.childForFieldName(field) + if (child) + this.visit(child, inner) + } + return + } + for (const child of named(node)) + this.visit(child, inner) + } + + /** 函数:参数一层(默认值在这一层求值),函数体再一层;函数表达式自己的名字只在它自己里面。 */ + private function(node: Node, scope: Scope): void { + const name = node.childForFieldName('name') + let outer = scope + if (name?.type === 'identifier') { + if (node.type === 'function_declaration' || node.type === 'generator_function_declaration') { + // 块里的函数声明在非严格模式下会提到外面,拿不准 + if (scope.kind === 'block') + scope.unsure.add(name.text) + this.declare(name, scope, 'local', scope) + } + else { + outer = this.collector.scope('name', scope) + this.declare(name, outer, 'local', outer) + } + } + else if (name) { + this.visit(name, scope) + } + const parameters = this.collector.scope('parameters', outer) + const single = node.childForFieldName('parameter') + if (single) + this.pattern(single, { declare: parameters, kind: 'local' }, parameters) + for (const parameter of named(node.childForFieldName('parameters'))) { + const pattern = parameter.childForFieldName('pattern') + if (pattern && pattern.type !== 'this') + this.pattern(pattern, { declare: parameters, kind: 'local' }, parameters) + const value = parameter.childForFieldName('value') + if (value) + this.visit(value, parameters) + for (const decorator of named(parameter).filter(child => child.type === 'decorator')) + this.visit(decorator, scope) + } + for (const decorator of named(node).filter(child => child.type === 'decorator')) + this.visit(decorator, scope) + const body = node.childForFieldName('body') + if (body?.type === 'statement_block') { + const inner = this.collector.scope('function', parameters) + for (const child of named(body)) + this.visit(child, inner) + } + else if (body) { + this.visit(body, parameters) + } + } + + /** 类:类声明的名字在所在的块,类表达式的名字只在类里面;父类、装饰器、成员照常算。 */ + private class(node: Node, scope: Scope): void { + const name = node.childForFieldName('name') + let inner = scope + if (name && node.type === 'class') { + inner = this.collector.scope('name', scope) + this.declare(name, inner, 'local', inner) + } + else if (name) { + this.declare(name, scope, 'local', scope) + } + for (const child of named(node)) { + if (child.type !== name?.type || child.startIndex !== name.startIndex) + this.visit(child, inner) + } + } + + /** 变量声明;`const x = require('m')` 这种在 JavaScript 里按导入算(跳到定义会跳进 m)。 */ + private declaration(node: Node, scope: Scope, owner: Scope): void { + for (const declarator of named(node)) { + if (declarator.type !== 'variable_declarator') { + this.visit(declarator, scope) + continue + } + const value = declarator.childForFieldName('value') + const required = value?.type === 'call_expression' && value.childForFieldName('function')?.text === 'require' + const name = declarator.childForFieldName('name') + if (name) + this.pattern(name, { declare: owner, kind: required ? 'import' : 'local' }, scope) + if (value) + this.visit(value, scope) + } + } + + /** 导入的名字:默认导入、`* as ns`、`{ a, b as c }` 里 as 后面的(没有 as 就是它自己)。 */ + private import(node: Node, scope: Scope): void { + for (const clause of named(node)) { + if (clause.type !== 'import_clause' && clause.type !== 'import_require_clause') + continue + for (const part of named(clause)) { + if (part.type === 'identifier') { + this.declare(part, scope, 'import', scope) + } + else if (part.type === 'namespace_import') { + const [name] = named(part) + if (name) + this.declare(name, scope, 'import', scope) + } + else if (part.type === 'named_imports') { + for (const specifier of named(part)) { + const name = specifier.childForFieldName('alias') ?? specifier.childForFieldName('name') + if (name?.type === 'identifier') + this.declare(name, scope, 'import', scope) + } + } + } + } + } + + /** catch 的参数和它的块是同一层。 */ + private catch(node: Node, scope: Scope): void { + const inner = this.collector.scope('block', scope) + const parameter = node.childForFieldName('parameter') + if (parameter) + this.pattern(parameter, { declare: inner, kind: 'local' }, inner) + for (const child of named(node.childForFieldName('body'))) + this.visit(child, inner) + } + + /** 赋值:左边是名字、解构是给已有的名字赋值,是成员就是用到对象。 */ + private assignment(node: Node, scope: Scope): void { + const left = node.childForFieldName('left') + if (left) + this.pattern(left, 'assign', scope) + const right = node.childForFieldName('right') + if (right) + this.visit(right, scope) + } + + /** 声明、参数、解构里的名字:照 mode 在一层绑定或者当用到;默认值、计算出来的属性名照常算;声明里认不出的写法拿不准。 */ + private pattern(node: Node, mode: PatternMode, scope: Scope): void { + switch (node.type) { + case 'identifier': + case 'shorthand_property_identifier_pattern': + if (mode === 'assign') + this.collector.see(scope, node) + else + this.declare(node, mode.declare, mode.kind, scope) + return + case 'object_pattern': + case 'array_pattern': + case 'rest_pattern': + named(node).forEach(child => this.pattern(child, mode, scope)) + return + case 'pair_pattern': { + const key = node.childForFieldName('key') + if (key?.type === 'computed_property_name') + this.visit(key, scope) + const value = node.childForFieldName('value') + if (value) + this.pattern(value, mode, scope) + return + } + case 'assignment_pattern': + case 'object_assignment_pattern': { + const left = node.childForFieldName('left') + if (left) + this.pattern(left, mode, scope) + const right = node.childForFieldName('right') + if (right) + this.visit(right, scope) + return + } + } + if (mode === 'assign') + this.visit(node, scope) + else + this.collector.unsure(mode.declare, node) + } + + private declare(name: Node, owner: Scope, kind: BindKind, scope: Scope): void { + owner.bind(name.text, kind) + this.collector.see(scope, name) + } + + /** var 提到的那一层:往外第一层函数体(或模块)。 */ + private varScope(scope: Scope): Scope { + let found = scope + while (found.kind !== 'function' && found.parent) + found = found.parent + return found + } +} + +/** 一份 TypeScript / JavaScript 源码里各个名字的绑定(见 NameBinding)。 */ +export function typescriptBindings(root: Node): NameBinding[] { + const scopes = new TypeScriptScopes() + for (const child of named(root)) + scopes.visit(child, scopes.module) + return scopes.bindings() +} diff --git a/vscode/src/codeReader/nameResolution.test.ts b/vscode/src/codeReader/nameResolution.test.ts index 27b685e..71b40ff 100644 --- a/vscode/src/codeReader/nameResolution.test.ts +++ b/vscode/src/codeReader/nameResolution.test.ts @@ -118,12 +118,12 @@ describe('中文名落到哪些名字上', () => { }) it('本文件定义的:定义处问各处出现,落在画出来的名字上的都换,定义处本身标 definition;长度对不上、没画出来的不换', async () => { - const { blocks, displayNames } = await read(ROUTES) + const { blocks, displayNames, bindings } = await read(ROUTES) const definition = at(ROUTES, 'current_user') const use = at(ROUTES, 'current_user.role') const sources = fakeSources(ROUTES, { highlights: { [definition]: [{ start: definition, end: definition + 12 }, { start: use, end: use + 12 }, { start: 3, end: 5 }, { start: at(ROUTES, 'ADMIN'), end: at(ROUTES, 'ADMIN') + 2 }] } }) const reports: [ShownName[], boolean][] = [] - const settled = await resolveNames(displayNames, blocks, sources, (names, final) => reports.push([names, final])) + const settled = await resolveNames(displayNames, blocks, bindings, sources, (names, final) => reports.push([names, final])) expect(settled).toBe(true) const expected = [ { offset: definition, length: 12, name: '当前用户', definition: true }, @@ -133,7 +133,7 @@ describe('中文名落到哪些名字上', () => { }) it('别的文件定义的:原名在索引里(别的文件)的名字才问定义,定义处正好是索引里那个定义才换', async () => { - const { blocks } = await read(ROUTES) + const { blocks, bindings } = await read(ROUTES) const admin = at(ROUTES, 'ADMIN') const role = at(ROUTES, 'role') const asked: string[] = [] @@ -152,14 +152,14 @@ describe('中文名落到哪些名字上', () => { }, }) const reports: ShownName[][] = [] - const settled = await resolveNames([], blocks, sources, names => reports.push(names)) + const settled = await resolveNames([], blocks, bindings, sources, names => reports.push(names)) expect(settled).toBe(true) expect(reports.at(-1)).toEqual([{ offset: admin, length: 5, name: '管理员' }]) expect(asked.filter(line => line.startsWith('definition')).sort()).toEqual([`definition ${admin}`, `definition ${role}`].sort()) }) it('别的文件定义的:页面看得见的先问,问完先交一次,再问其余的', async () => { - const { blocks } = await read(ROUTES) + const { blocks, bindings } = await read(ROUTES) const admin = at(ROUTES, 'ADMIN') const role = at(ROUTES, 'role') const models = 'file:///w/models.py' @@ -177,18 +177,50 @@ describe('中文名落到哪些名字上', () => { }, }) const reports: [number[], boolean][] = [] - await resolveNames([], blocks, sources, (names, final) => reports.push([names.map(({ offset }) => offset), final])) + await resolveNames([], blocks, bindings, sources, (names, final) => reports.push([names.map(({ offset }) => offset), final])) expect(asked.filter(line => line.startsWith('definition'))).toEqual([`definition ${admin}`, `definition ${role}`]) expect(reports).toEqual([[[], false], [[admin], false], [[admin, role], true]]) }) + it('别的文件定义的:同一个绑定只问一处(看得见的那处),答案各处共用;本文件自己定义的绑定(参数、赋值)不问', async () => { + const source = [ + 'from app.models import Owner', + '', + 'def f(role, owner):', + ' owner = Owner(role=role)', + ' check(Owner, role)', + ' return owner', + '', + ].join('\n') + const { blocks, bindings } = await read(source) + const models = 'file:///w/models.py' + const owners = [at(source, 'Owner('), at(source, 'Owner,')] + const asked: string[] = [] + const sources = fakeSources(source, { + asked, + visible: offset => offset === owners[1], + indexed: { + Owner: [{ location: { uri: models, start: { line: 3, character: 6 }, end: { line: 3, character: 11 } }, text: '所有者' }], + role: [{ location: { uri: models, start: { line: 9, character: 4 }, end: { line: 9, character: 8 } }, text: '角色' }], + owner: [{ location: { uri: models, start: { line: 12, character: 4 }, end: { line: 12, character: 9 } }, text: '所有人' }], + }, + definitions: Object.fromEntries([...source.matchAll(/\bOwner\b/g)].map(match => [match.index!, [{ uri: models, line: 3, character: 6 }]])), + }) + const reports: ShownName[][] = [] + expect(await resolveNames([], blocks, bindings, sources, names => reports.push(names))).toBe(true) + // 参数 role、局部变量 owner 不问;实参名 role= 是被调用的函数的参数,照旧问;Owner 三处问看得见的那一处 + expect(asked.filter(line => line.startsWith('definition'))).toEqual([`definition ${owners[1]}`, `definition ${at(source, 'role=')}`]) + const shown = reports.at(-1)!.filter(name => name.name === '所有者').map(({ offset }) => offset) + expect(shown).toEqual(expect.arrayContaining(owners)) + }) + it('语言扩展还没起来:定义处自己都查不到出现,或要问的定义一个都没答,算没答完(过一会儿再查)', async () => { - const { blocks, displayNames } = await read(ROUTES) - expect(await resolveNames(displayNames, blocks, fakeSources(ROUTES), () => {})).toBe(false) + const { blocks, displayNames, bindings } = await read(ROUTES) + expect(await resolveNames(displayNames, blocks, bindings, fakeSources(ROUTES), () => {})).toBe(false) const admin = at(ROUTES, 'ADMIN') const indexed = { ADMIN: [{ location: { uri: 'file:///w/models.py', start: { line: 6, character: 4 }, end: { line: 6, character: 9 } }, text: '管理员' }] } - expect(await resolveNames([], blocks, fakeSources(ROUTES, { indexed }), () => {})).toBe(false) - expect(await resolveNames([], blocks, fakeSources(ROUTES, { indexed, definitions: { [admin]: [{ uri: 'file:///w/other.py', line: 0, character: 0 }] } }), () => {})).toBe(true) + expect(await resolveNames([], blocks, bindings, fakeSources(ROUTES, { indexed }), () => {})).toBe(false) + expect(await resolveNames([], blocks, bindings, fakeSources(ROUTES, { indexed, definitions: { [admin]: [{ uri: 'file:///w/other.py', line: 0, character: 0 }] } }), () => {})).toBe(true) }) }) diff --git a/vscode/src/codeReader/nameResolution.ts b/vscode/src/codeReader/nameResolution.ts index 4e86f9c..e832566 100644 --- a/vscode/src/codeReader/nameResolution.ts +++ b/vscode/src/codeReader/nameResolution.ts @@ -1,5 +1,5 @@ import type { BlockView } from './blockView' -import type { DisplayName, IndexEntry, Inline, LanguageAdapter, PreviewText, PropertyItem, RuleLocation } from './contract' +import type { DisplayName, IndexEntry, Inline, LanguageAdapter, NameBinding, PreviewText, PropertyItem, RuleLocation } from './contract' import type { IndexedEntry, WorkspaceIndexOf } from './workspaceIndex' /** @@ -84,12 +84,13 @@ export interface NameSources { * 找这份文档里显示成中文名的各处,分两步交出(先交本文件的,不等工作区索引建好;最后一次 final): * 1. 本文件定义的:每个定义问一次它的各处出现,落在视图数据里带下标的名字上的显示成中文名,定义处本身标 definition; * 2. 别的文件定义的:原名和工作区索引里某个中文名一样的名字才问定义提供者,定义处正是索引里那个定义才算; - * 页面看得见的先问,问完先交一次,再问其余的。 + * 同一个绑定(见 NameBinding)的各处只问一处、答案共用,这份文档自己定义的绑定不问;页面看得见的先问,问完先交一次,再问其余的。 * 返回语言扩展答没答(settled):定义处自己都查不到出现,或要问的定义一个都没答,是语言扩展还没起来,过一会儿再查。 */ export async function resolveNames( names: readonly DisplayName[], blocks: readonly BlockView[], + bindings: readonly NameBinding[], sources: NameSources, report: (shown: ShownName[], final: boolean) => void, ): Promise { @@ -112,34 +113,54 @@ export async function resolveNames( if (!shown.has(offset)) byName.set(original, [...(byName.get(original) ?? []), offset]) } - const asks: { offset: number, original: string, elsewhere: IndexedName[] }[] = [] + const bindingAt = new Map() + for (const binding of bindings) { + for (const { startIndex, endIndex } of binding.occurrences) + bindingAt.set(startIndex, { binding, end: endIndex }) + } + // 同一个绑定的各处问一处、答案共用;这份文档自己定义的(参数、赋值)定义就在这份文档里,对不上别的文件的中文名,不问 + const asks = new Map() await Promise.all([...byName].map(async ([original, offsets]) => { const elsewhere = (await sources.indexed(original)).filter(entry => entry.location.uri !== sources.uri) - for (const offset of elsewhere.length > 0 ? offsets : []) - asks.push({ offset, original, elsewhere }) + for (const offset of elsewhere.length > 0 ? offsets : []) { + const at = bindingAt.get(offset) + const binding = at?.end === offset + original.length ? at.binding : undefined + if (binding?.local) + continue + const ask = asks.get(binding ?? offset) ?? { offsets: [], original, elsewhere } + ask.offsets.push(offset) + asks.set(binding ?? offset, ask) + } })) let answered = 0 - const ask = (list: typeof asks): Promise => Promise.all(list.map(async ({ offset, original, elsewhere }) => { - const found = await sources.definitions(offset) + const ask = (list: { at: number, offsets: number[], original: string, elsewhere: IndexedName[] }[]): Promise => Promise.all(list.map(async ({ at, offsets, original, elsewhere }) => { + const found = await sources.definitions(at) if (found.length > 0) answered++ - const entry = elsewhere.find(({ location }) => found.some(at => at.uri === location.uri && at.line === location.start.line && at.character === location.start.character)) - if (entry) + const entry = elsewhere.find(({ location }) => found.some(start => start.uri === location.uri && start.line === location.start.line && start.character === location.start.character)) + if (!entry) + return + for (const offset of offsets) shown.set(offset, { offset, length: original.length, name: entry.text }) })) - const near: typeof asks = [] - const far: typeof asks = [] - for (const item of asks) - (sources.visible(item.offset) ? near : far).push(item) + const near: Parameters[0] = [] + const far: Parameters[0] = [] + for (const item of asks.values()) { + const visible = item.offsets.find(offset => sources.visible(offset)) + if (visible === undefined) + far.push({ ...item, at: item.offsets[0] }) + else + near.push({ ...item, at: visible }) + } if (near.length > 0 && far.length > 0) { await ask(near) report([...shown.values()], false) await ask(far) } else { - await ask(asks) + await ask([...near, ...far]) } - if (asks.length > 0 && answered === 0) + if (asks.size > 0 && answered === 0) settled = false report([...shown.values()], true) return settled