⚡ 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

@@ -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()
}