⚡ perf(vscode): 语言插件按作用域规则认出名字的绑定,找别的文件起的中文名时同一个绑定只问一处、本文件定义的不问
This commit is contained in:
1 parent
12ad047b4d
commit
0070933599
14 files changed
+1159
-36
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
import type { Tree } from '@vscode/tree-sitter-wasm'
|
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 { TreeSitterRuntime } from './runtime'
|
||||||
import type { ImportedName } from './symbolLinks'
|
import type { ImportedName } from './symbolLinks'
|
||||||
import { blockAttributes } from './attributes'
|
import { blockAttributes } from './attributes'
|
||||||
@@ -507,7 +507,8 @@ export function toBlockViews(
|
|||||||
/**
|
/**
|
||||||
* 一份源码的视图数据,和它里面起了中文名的定义(预览找用到它们的各处要用,见 displayNames.ts)、
|
* 一份源码的视图数据,和它里面起了中文名的定义(预览找用到它们的各处要用,见 displayNames.ts)、
|
||||||
* 语言插件自己认得出的名字(LanguageAdapter.symbols,语言扩展查不到定义、各处出现时用)、
|
* 语言插件自己认得出的名字(LanguageAdapter.symbols,语言扩展查不到定义、各处出现时用)、
|
||||||
* 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义,见 symbolLinks.ts)、各块自己的名字和种类(调用的定义就在这份源码里时,分新建还是调用用它,见 NamedKind)。
|
* 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义,见 symbolLinks.ts)、各块自己的名字和种类(调用的定义就在这份源码里时,分新建还是调用用它,见 NamedKind)、
|
||||||
|
* 各个名字的绑定(找中文名时同一个绑定问一处,见 LanguageAdapter.bindings)。
|
||||||
*/
|
*/
|
||||||
export interface DocumentViews {
|
export interface DocumentViews {
|
||||||
blocks: BlockView[]
|
blocks: BlockView[]
|
||||||
@@ -515,6 +516,7 @@ export interface DocumentViews {
|
|||||||
symbols: LocalSymbol[]
|
symbols: LocalSymbol[]
|
||||||
imports: ImportedName[]
|
imports: ImportedName[]
|
||||||
kinds: NamedKind[]
|
kinds: NamedKind[]
|
||||||
|
bindings: NameBinding[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -587,7 +589,8 @@ export async function readBlockViews(
|
|||||||
...adapter.symbols?.(tree.rootNode, source, languageId) ?? [],
|
...adapter.symbols?.(tree.rootNode, source, languageId) ?? [],
|
||||||
...[...parts.values()].flatMap(part => part.document.language.symbols?.(part.root, source, part.code.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 {
|
finally {
|
||||||
for (const parsed of trees)
|
for (const parsed of trees)
|
||||||
|
|||||||
@@ -458,6 +458,16 @@ export interface LocalSymbol {
|
|||||||
occurrences: SourceRange[]
|
occurrences: SourceRange[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一份源码里一个名字的一个绑定(参数、变量、函数、类、导入的名字……),照这门语言的作用域规则认:各处出现(名字的范围,含定义处)
|
||||||
|
* 指的是同一个定义,内层同名的参数、推导式变量这类遮住外层的是另一个绑定。local 是这份源码自己定义的(参数、赋值、def、class……),
|
||||||
|
* 定义一定在这份源码里;导入的、哪儿都没定义的(内置的)不是。
|
||||||
|
*/
|
||||||
|
export interface NameBinding {
|
||||||
|
occurrences: SourceRange[]
|
||||||
|
local: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface DeclaratorFacts {
|
export interface DeclaratorFacts {
|
||||||
/** 变量名;解构声明是整个模式。 */
|
/** 变量名;解构声明是整个模式。 */
|
||||||
name: Node
|
name: Node
|
||||||
@@ -760,6 +770,11 @@ export interface LanguageAdapter {
|
|||||||
command?: (node: Node) => CommandFacts | undefined
|
command?: (node: Node) => CommandFacts | undefined
|
||||||
/** 一份源码里自己认得出的名字(见 LocalSymbol);VS Code 有这种语言的定义、高亮提供者时用不上,不填。 */
|
/** 一份源码里自己认得出的名字(见 LocalSymbol);VS Code 有这种语言的定义、高亮提供者时用不上,不填。 */
|
||||||
symbols?: (root: Node, source: string, languageId: string) => LocalSymbol[]
|
symbols?: (root: Node, source: string, languageId: string) => LocalSymbol[]
|
||||||
|
/**
|
||||||
|
* 一份源码里各个名字的绑定(见 NameBinding):宿主问语言扩展名字的定义时,同一个绑定只问一处,这份源码自己定义的不问。
|
||||||
|
* 只报认得准的:拿不准的(模式匹配捕获的、跳过不看的声明里的、同一个名字既导入又赋值的……)不报,宿主照旧逐处问。不填就逐处问。
|
||||||
|
*/
|
||||||
|
bindings?: (root: Node, source: string, languageId: string) => NameBinding[]
|
||||||
/** 这份源码里嵌着的别的语言的代码(见 EmbeddedCode),块里的(TextBlock.embedded)、属性表里的(HealthFacts.command)都算;没有的语言不填。 */
|
/** 这份源码里嵌着的别的语言的代码(见 EmbeddedCode),块里的(TextBlock.embedded)、属性表里的(HealthFacts.command)都算;没有的语言不填。 */
|
||||||
embedded?: (root: Node, source: string, languageId: string) => EmbeddedCode[]
|
embedded?: (root: Node, source: string, languageId: string) => EmbeddedCode[]
|
||||||
/** 这门语言的代码被别的语言嵌着、当一串步骤执行时怎么分块(见 StepBlocks);不填就按 toBlocks 分、不算一步失败就停。 */
|
/** 这门语言的代码被别的语言嵌着、当一串步骤执行时怎么分块(见 StepBlocks);不填就按 toBlocks 分、不算一步失败就停。 */
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { NamedKind } from '../blockView'
|
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 { CodeReaderRegistry } from '../registry'
|
||||||
import type { TreeSitterRuntime } from '../runtime'
|
import type { TreeSitterRuntime } from '../runtime'
|
||||||
import type { ImportedName } from '../symbolLinks'
|
import type { ImportedName } from '../symbolLinks'
|
||||||
@@ -23,7 +23,8 @@ export interface DocumentSnapshot {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 发给预览的文档,和这份源码里起了中文名的定义(扩展端接着找用到它们的各处)、语言插件自己认得出的名字(LocalSymbol)、
|
* 发给预览的文档,和这份源码里起了中文名的定义(扩展端接着找用到它们的各处)、语言插件自己认得出的名字(LocalSymbol)、
|
||||||
* 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义)、各块自己的名字和种类(调用的定义就在这份文档里时认种类用,见 NamedKind)。
|
* 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义)、各块自己的名字和种类(调用的定义就在这份文档里时认种类用,见 NamedKind)、
|
||||||
|
* 各个名字的绑定(找中文名时同一个绑定问一处,见 NameBinding)。
|
||||||
*/
|
*/
|
||||||
export interface DocumentRead {
|
export interface DocumentRead {
|
||||||
message: DocumentMessage
|
message: DocumentMessage
|
||||||
@@ -31,6 +32,7 @@ export interface DocumentRead {
|
|||||||
symbols: LocalSymbol[]
|
symbols: LocalSymbol[]
|
||||||
imports: ImportedName[]
|
imports: ImportedName[]
|
||||||
kinds: NamedKind[]
|
kinds: NamedKind[]
|
||||||
|
bindings: NameBinding[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,10 +64,10 @@ export async function documentMessage(
|
|||||||
}
|
}
|
||||||
const adapter = registry.languageFor(languageId)
|
const adapter = registry.languageFor(languageId)
|
||||||
if (!adapter)
|
if (!adapter)
|
||||||
return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [], symbols: [], imports: [], kinds: [] }
|
return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [], symbols: [], imports: [], kinds: [], bindings: [] }
|
||||||
try {
|
try {
|
||||||
const rules = registry.rulesFor(languageId)
|
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 }),
|
...(document.fileName && { fileName: document.fileName }),
|
||||||
rules: { rules, report, lookup },
|
rules: { rules, report, lookup },
|
||||||
calleeKind,
|
calleeKind,
|
||||||
@@ -80,9 +82,9 @@ export async function documentMessage(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const names = mergeAttributeNames([translator?.attributeNames, ...ruleAttributeNames(rules, translator?.displayLanguage, report)])
|
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) {
|
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: [],
|
symbols: [],
|
||||||
imports: [],
|
imports: [],
|
||||||
kinds: [],
|
kinds: [],
|
||||||
|
bindings: [],
|
||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
/** 文档符号由各个测试给 */
|
/** 文档符号由各个测试给 */
|
||||||
@@ -542,6 +543,8 @@ describe('注释里不带路径的引用按它写在哪块的注释里找', () =
|
|||||||
displayNames: [],
|
displayNames: [],
|
||||||
symbols: [],
|
symbols: [],
|
||||||
imports: [],
|
imports: [],
|
||||||
|
kinds: [],
|
||||||
|
bindings: [],
|
||||||
}) as never)
|
}) as never)
|
||||||
vi.mocked(documentSymbolNodes).mockResolvedValue([
|
vi.mocked(documentSymbolNodes).mockResolvedValue([
|
||||||
symbol('AgentService', 100, 900, [symbol('get_agent_admitted', 200, 300), symbol('get_default_agent', 400, 500)]),
|
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 source = this.document.getText()
|
||||||
const session = this.lookups.session()
|
const session = this.lookups.session()
|
||||||
const callees = this.callees.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}` })
|
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
|
||||||
}, session.lookup, callees.calleeKind)
|
}, session.lookup, callees.calleeKind)
|
||||||
if (request !== this.latest)
|
if (request !== this.latest)
|
||||||
@@ -434,7 +434,7 @@ class Preview {
|
|||||||
void this.diff.update(this.sent)
|
void this.diff.update(this.sent)
|
||||||
// 文档在解析期间又改了的,等下一次发文档再查
|
// 文档在解析期间又改了的,等下一次发文档再查
|
||||||
if (version === this.document.version) {
|
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.lookups.resolve(version, session.requests(), this.lookupSources(version))
|
||||||
this.callees.resolve(version, callees.requests(), offset => this.queued(version, offset, () => this.calleeKind(offset), undefined))
|
this.callees.resolve(version, callees.requests(), offset => this.queued(version, offset, () => this.calleeKind(offset), undefined))
|
||||||
this.callTrees.documentSent()
|
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 { 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 type { CommentRules, ExceptionRules } from './shared'
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
|
import { pythonBindings } from './pythonBindings'
|
||||||
import { pythonNameReader } from './pythonNames'
|
import { pythonNameReader } from './pythonNames'
|
||||||
import { attachComments, children, compoundShape, insideOf, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren, withExceptions } from './shared'
|
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,
|
iteration: pythonIteration,
|
||||||
call: pythonCall,
|
call: pythonCall,
|
||||||
collection: pythonCollection,
|
collection: pythonCollection,
|
||||||
|
bindings: root => pythonBindings(root),
|
||||||
displayNames: pythonNameReader,
|
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 type { CommentRules, ExceptionRules } from './shared'
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
import { attachComments, children, compoundShape, extendHeader, headerSpan, insideOf, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren, withExceptions } from './shared'
|
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'
|
import { typescriptNameReader } from './typescriptNames'
|
||||||
|
|
||||||
const GRAMMARS: Record<string, string> = {
|
const GRAMMARS: Record<string, string> = {
|
||||||
@@ -662,6 +663,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
|
|||||||
freshDoc,
|
freshDoc,
|
||||||
call: typescriptCall,
|
call: typescriptCall,
|
||||||
collection: typescriptCollection,
|
collection: typescriptCollection,
|
||||||
|
bindings: root => typescriptBindings(root),
|
||||||
displayNames: typescriptNameReader,
|
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()
|
||||||
|
}
|
||||||
@@ -118,12 +118,12 @@ describe('中文名落到哪些名字上', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('本文件定义的:定义处问各处出现,落在画出来的名字上的都换,定义处本身标 definition;长度对不上、没画出来的不换', async () => {
|
it('本文件定义的:定义处问各处出现,落在画出来的名字上的都换,定义处本身标 definition;长度对不上、没画出来的不换', async () => {
|
||||||
const { blocks, displayNames } = await read(ROUTES)
|
const { blocks, displayNames, bindings } = await read(ROUTES)
|
||||||
const definition = at(ROUTES, 'current_user')
|
const definition = at(ROUTES, 'current_user')
|
||||||
const use = at(ROUTES, 'current_user.role')
|
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 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 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)
|
expect(settled).toBe(true)
|
||||||
const expected = [
|
const expected = [
|
||||||
{ offset: definition, length: 12, name: '当前用户', definition: true },
|
{ offset: definition, length: 12, name: '当前用户', definition: true },
|
||||||
@@ -133,7 +133,7 @@ describe('中文名落到哪些名字上', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('别的文件定义的:原名在索引里(别的文件)的名字才问定义,定义处正好是索引里那个定义才换', async () => {
|
it('别的文件定义的:原名在索引里(别的文件)的名字才问定义,定义处正好是索引里那个定义才换', async () => {
|
||||||
const { blocks } = await read(ROUTES)
|
const { blocks, bindings } = await read(ROUTES)
|
||||||
const admin = at(ROUTES, 'ADMIN')
|
const admin = at(ROUTES, 'ADMIN')
|
||||||
const role = at(ROUTES, 'role')
|
const role = at(ROUTES, 'role')
|
||||||
const asked: string[] = []
|
const asked: string[] = []
|
||||||
@@ -152,14 +152,14 @@ describe('中文名落到哪些名字上', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const reports: ShownName[][] = []
|
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(settled).toBe(true)
|
||||||
expect(reports.at(-1)).toEqual([{ offset: admin, length: 5, name: '管理员' }])
|
expect(reports.at(-1)).toEqual([{ offset: admin, length: 5, name: '管理员' }])
|
||||||
expect(asked.filter(line => line.startsWith('definition')).sort()).toEqual([`definition ${admin}`, `definition ${role}`].sort())
|
expect(asked.filter(line => line.startsWith('definition')).sort()).toEqual([`definition ${admin}`, `definition ${role}`].sort())
|
||||||
})
|
})
|
||||||
|
|
||||||
it('别的文件定义的:页面看得见的先问,问完先交一次,再问其余的', async () => {
|
it('别的文件定义的:页面看得见的先问,问完先交一次,再问其余的', async () => {
|
||||||
const { blocks } = await read(ROUTES)
|
const { blocks, bindings } = await read(ROUTES)
|
||||||
const admin = at(ROUTES, 'ADMIN')
|
const admin = at(ROUTES, 'ADMIN')
|
||||||
const role = at(ROUTES, 'role')
|
const role = at(ROUTES, 'role')
|
||||||
const models = 'file:///w/models.py'
|
const models = 'file:///w/models.py'
|
||||||
@@ -177,18 +177,50 @@ describe('中文名落到哪些名字上', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const reports: [number[], boolean][] = []
|
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(asked.filter(line => line.startsWith('definition'))).toEqual([`definition ${admin}`, `definition ${role}`])
|
||||||
expect(reports).toEqual([[[], false], [[admin], false], [[admin, role], true]])
|
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 () => {
|
it('语言扩展还没起来:定义处自己都查不到出现,或要问的定义一个都没答,算没答完(过一会儿再查)', async () => {
|
||||||
const { blocks, displayNames } = await read(ROUTES)
|
const { blocks, displayNames, bindings } = await read(ROUTES)
|
||||||
expect(await resolveNames(displayNames, blocks, fakeSources(ROUTES), () => {})).toBe(false)
|
expect(await resolveNames(displayNames, blocks, bindings, fakeSources(ROUTES), () => {})).toBe(false)
|
||||||
const admin = at(ROUTES, 'ADMIN')
|
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: '管理员' }] }
|
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, bindings, 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, definitions: { [admin]: [{ uri: 'file:///w/other.py', line: 0, character: 0 }] } }), () => {})).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { BlockView } from './blockView'
|
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'
|
import type { IndexedEntry, WorkspaceIndexOf } from './workspaceIndex'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,12 +84,13 @@ export interface NameSources {
|
|||||||
* 找这份文档里显示成中文名的各处,分两步交出(先交本文件的,不等工作区索引建好;最后一次 final):
|
* 找这份文档里显示成中文名的各处,分两步交出(先交本文件的,不等工作区索引建好;最后一次 final):
|
||||||
* 1. 本文件定义的:每个定义问一次它的各处出现,落在视图数据里带下标的名字上的显示成中文名,定义处本身标 definition;
|
* 1. 本文件定义的:每个定义问一次它的各处出现,落在视图数据里带下标的名字上的显示成中文名,定义处本身标 definition;
|
||||||
* 2. 别的文件定义的:原名和工作区索引里某个中文名一样的名字才问定义提供者,定义处正是索引里那个定义才算;
|
* 2. 别的文件定义的:原名和工作区索引里某个中文名一样的名字才问定义提供者,定义处正是索引里那个定义才算;
|
||||||
* 页面看得见的先问,问完先交一次,再问其余的。
|
* 同一个绑定(见 NameBinding)的各处只问一处、答案共用,这份文档自己定义的绑定不问;页面看得见的先问,问完先交一次,再问其余的。
|
||||||
* 返回语言扩展答没答(settled):定义处自己都查不到出现,或要问的定义一个都没答,是语言扩展还没起来,过一会儿再查。
|
* 返回语言扩展答没答(settled):定义处自己都查不到出现,或要问的定义一个都没答,是语言扩展还没起来,过一会儿再查。
|
||||||
*/
|
*/
|
||||||
export async function resolveNames(
|
export async function resolveNames(
|
||||||
names: readonly DisplayName[],
|
names: readonly DisplayName[],
|
||||||
blocks: readonly BlockView[],
|
blocks: readonly BlockView[],
|
||||||
|
bindings: readonly NameBinding[],
|
||||||
sources: NameSources,
|
sources: NameSources,
|
||||||
report: (shown: ShownName[], final: boolean) => void,
|
report: (shown: ShownName[], final: boolean) => void,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@@ -112,34 +113,54 @@ export async function resolveNames(
|
|||||||
if (!shown.has(offset))
|
if (!shown.has(offset))
|
||||||
byName.set(original, [...(byName.get(original) ?? []), 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]) => {
|
await Promise.all([...byName].map(async ([original, offsets]) => {
|
||||||
const elsewhere = (await sources.indexed(original)).filter(entry => entry.location.uri !== sources.uri)
|
const elsewhere = (await sources.indexed(original)).filter(entry => entry.location.uri !== sources.uri)
|
||||||
for (const offset of elsewhere.length > 0 ? offsets : [])
|
for (const offset of elsewhere.length > 0 ? offsets : []) {
|
||||||
asks.push({ offset, original, elsewhere })
|
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
|
let answered = 0
|
||||||
const ask = (list: typeof asks): Promise<unknown> => Promise.all(list.map(async ({ offset, original, elsewhere }) => {
|
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(offset)
|
const found = await sources.definitions(at)
|
||||||
if (found.length > 0)
|
if (found.length > 0)
|
||||||
answered++
|
answered++
|
||||||
const entry = elsewhere.find(({ location }) => found.some(at => at.uri === location.uri && at.line === location.start.line && at.character === location.start.character))
|
const entry = elsewhere.find(({ location }) => found.some(start => start.uri === location.uri && start.line === location.start.line && start.character === location.start.character))
|
||||||
if (entry)
|
if (!entry)
|
||||||
|
return
|
||||||
|
for (const offset of offsets)
|
||||||
shown.set(offset, { offset, length: original.length, name: entry.text })
|
shown.set(offset, { offset, length: original.length, name: entry.text })
|
||||||
}))
|
}))
|
||||||
const near: typeof asks = []
|
const near: Parameters<typeof ask>[0] = []
|
||||||
const far: typeof asks = []
|
const far: Parameters<typeof ask>[0] = []
|
||||||
for (const item of asks)
|
for (const item of asks.values()) {
|
||||||
(sources.visible(item.offset) ? near : far).push(item)
|
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) {
|
if (near.length > 0 && far.length > 0) {
|
||||||
await ask(near)
|
await ask(near)
|
||||||
report([...shown.values()], false)
|
report([...shown.values()], false)
|
||||||
await ask(far)
|
await ask(far)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await ask(asks)
|
await ask([...near, ...far])
|
||||||
}
|
}
|
||||||
if (asks.length > 0 && answered === 0)
|
if (asks.size > 0 && answered === 0)
|
||||||
settled = false
|
settled = false
|
||||||
report([...shown.values()], true)
|
report([...shown.values()], true)
|
||||||
return settled
|
return settled
|
||||||
|
|||||||
Reference in new issue
Block a user