⚡ perf(vscode): 语言插件按作用域规则认出名字的绑定,找别的文件起的中文名时同一个绑定只问一处、本文件定义的不问

This commit is contained in:
cruldra committed 2026-10-13 14:40:07 +11:00
1 parent 12ad047b4d
commit 0070933599
14 files changed
+1159 -36

No files matched your search

+6 -3
View File
@@ -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)
+15
View File
@@ -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 分、不算一步失败就停。 */
@@ -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: [] }
}
}
@@ -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)]),
@@ -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()
@@ -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<string, BindKind[]>()
readonly unsure = new Set<string>()
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<string, BindKind[]>()
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<string, SourceRange[]>()
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<T>(map: Map<string, T[]>, key: string): T[] {
let list = map.get(key)
if (!list) {
list = []
map.set(key, list)
}
return list
}
@@ -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,
}
}
@@ -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)
})
})
@@ -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<Scope, Map<string, 'global' | 'nonlocal'>>()
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<string, 'global' | 'nonlocal'> {
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()
}
@@ -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<string, string> = {
@@ -662,6 +663,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
freshDoc,
call: typescriptCall,
collection: typescriptCollection,
bindings: root => typescriptBindings(root),
displayNames: typescriptNameReader,
}
}
@@ -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(`(?<![\\w$])${name}(?![\\w$])`, '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('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 = <Value prop={value} />',
'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()
})
})
@@ -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()
}
+42 -10
View File
@@ -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)
})
})
+36 -15
View File
@@ -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<boolean> {
@@ -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<number, { binding: NameBinding, end: number }>()
for (const binding of bindings) {
for (const { startIndex, endIndex } of binding.occurrences)
bindingAt.set(startIndex, { binding, end: endIndex })
}
// 同一个绑定的各处问一处、答案共用;这份文档自己定义的(参数、赋值)定义就在这份文档里,对不上别的文件的中文名,不问
const asks = new Map<NameBinding | number, { offsets: number[], original: string, elsewhere: IndexedName[] }>()
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<unknown> => 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<unknown> => 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<typeof ask>[0] = []
const far: Parameters<typeof ask>[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