import type { DisplayNameFacts, DisplayNameReader, Node } from '../contract' import type { NameCandidate } from './displayNames' import { COMMENT_MARK, displayNameFacts, firstClause } from './displayNames' import { children, named } from './shared' /** 名字的最后一节:`Comment`、`typing.Annotated` 的 `Annotated`。 */ function lastName(node: Node | null | undefined): string | undefined { if (node?.type === 'identifier') return node.text return node?.type === 'attribute' ? node.childForFieldName('attribute')?.text : undefined } /** 普通字符串字面量的内容(不带引号、前缀);f-string 这类带插值的没有。 */ function plainString(node: Node | undefined): string | undefined { if (node?.type !== 'string' || named(node).some(part => part.type === 'interpolation')) return undefined const start = named(node).find(part => part.type === 'string_start') const end = named(node).find(part => part.type === 'string_end') const text = start && end ? node.text.slice(start.endIndex - node.startIndex, end.startIndex - node.startIndex).trim() : '' return text || undefined } /** `Comment("当前用户")`(调用的名字最后一节是 Comment,不管从哪导入、是不是本地定义的)的第一个参数。 */ function commentCall(node: Node | undefined): string | undefined { if (node?.type !== 'call' || lastName(node.childForFieldName('function')) !== COMMENT_MARK) return undefined return plainString(named(node.childForFieldName('arguments'))[0]) } /** 注解 `Annotated[T, …, Comment("…")]`(`typing.Annotated` 也算)里的中文名。 */ function annotatedName(annotation: Node | null): string | undefined { const inner = named(annotation)[0] let head: Node | undefined let items: Node[] = [] if (inner?.type === 'generic_type') { head = named(inner)[0] items = named(named(inner).find(part => part.type === 'type_parameter')).map(item => item.type === 'type' ? named(item)[0] : item) } else if (inner?.type === 'subscript') { head = inner.childForFieldName('value') ?? undefined items = inner.childrenForFieldName('subscript').filter((item): item is Node => item !== null) } if (lastName(head) !== 'Annotated') return undefined return items.slice(1).map(commentCall).find(text => text !== undefined) } /** 装饰器 `@Comment("…")` 给的中文名。 */ function decoratorName(definition: Node): string | undefined { const outer = definition.parent?.type === 'decorated_definition' ? definition.parent : undefined return named(outer).filter(child => child.type === 'decorator').map(decorator => commentCall(named(decorator)[0])).find(text => text !== undefined) } /** 参数节点里定义的那个名字:`x`、`x: int`、`x=1`、`*args`、`**kw`;`*`、`/` 分隔符没有。 */ function parameterName(param: Node): Node | undefined { switch (param.type) { case 'identifier': return param case 'typed_parameter': return parameterName(named(param)[0]) case 'default_parameter': case 'typed_default_parameter': return param.childForFieldName('name') ?? undefined case 'list_splat_pattern': case 'dictionary_splat_pattern': return named(param).find(child => child.type === 'identifier') default: return undefined } } /** * docstring 里各参数的说明取第一小句:Google 写法 `Args:` 一节里的 `name (type): 说明`,Sphinx 写法 `:param name: 说明` * (`:param type name:` 也算)。名字去掉 `*` / `**`。 */ export function docstringParameters(doc: string): Map { const found = new Map() const put = (name: string, text: string): void => { const clause = firstClause(text) if (clause && !found.has(name)) found.set(name, clause) } const lines = doc.split('\n') const indentOf = (line: string): number => line.length - line.trimStart().length for (let i = 0; i < lines.length; i++) { const sphinx = /^\s*:param\s+(?:[^\s:]+\s+)?\*{0,2}(\w+)\s*:(.*)$/.exec(lines[i]) if (sphinx) { put(sphinx[1], sphinx[2]) continue } if (!/^\s*(?:Args|Arguments):\s*$/.test(lines[i])) continue const sectionIndent = indentOf(lines[i]) let entryIndent: number | undefined for (i++; i < lines.length; i++) { const line = lines[i] if (line.trim() === '') continue const indent = indentOf(line) if (indent <= sectionIndent) { i-- break } entryIndent ??= indent const entry = indent === entryIndent ? /^\s*\*{0,2}(\w+)(?:\s*\([^)]*\))?\s*:(.*)$/.exec(line) : null if (entry) put(entry[1], entry[2]) } } return found } /** 函数主体开头的 docstring 的原文。 */ function docstringOf(fn: Node): string | undefined { const first = named(fn.childForFieldName('body')).find(child => child.type !== 'comment') const [expression, ...rest] = first?.type === 'expression_statement' ? named(first) : [] return rest.length === 0 ? plainString(expression) : undefined } function functionCandidates(fn: Node): NameCandidate[] { const name = fn.childForFieldName('name') const doc = docstringOf(fn) const described = doc === undefined ? new Map() : docstringParameters(doc) const params = named(fn.childForFieldName('parameters')).flatMap((param): NameCandidate[] => { const paramName = parameterName(param) if (!paramName) return [] return [{ name: paramName, marked: annotatedName(param.childForFieldName('type')), described: described.get(paramName.text), trailing: true }] }) return [...(name ? [{ name, marked: decoratorName(fn) }] : []), ...params] } /** 赋值里定义的名字:`x = …`、`x: T = …`、只写注解的 `x: T`、`self.x = …` 的 x、解构的各个名字。 */ function targetNames(target: Node | null): Node[] { if (!target) return [] if (target.type === 'identifier') return [target] if (target.type === 'attribute') { const attribute = target.childForFieldName('attribute') return attribute ? [attribute] : [] } if (['pattern_list', 'tuple_pattern', 'list_pattern', 'list_splat_pattern'].includes(target.type)) return named(target).flatMap(targetNames) return [] } function assignmentCandidates(assignment: Node): NameCandidate[] { const marked = annotatedName(assignment.childForFieldName('type')) return targetNames(assignment.childForFieldName('left')).map(name => ({ name, marked, trailing: true })) } /** * Python 的中文名:`Annotated[T, Comment("…")]`(参数、变量、类字段)、`@Comment("…")`(函数、类)、 * 定义那一行行尾的 `# @Comment 名字`(多行参数表里的参数、变量、字段、枚举成员),都没有时参数取函数 docstring 里它的说明。 */ export function pythonDisplayNames(root: Node, source: string): DisplayNameFacts { const candidates: NameCandidate[] = [] const comments: Node[] = [] const visit = (node: Node): void => { switch (node.type) { case 'comment': comments.push(node) return case 'function_definition': candidates.push(...functionCandidates(node)) break case 'class_definition': { const name = node.childForFieldName('name') if (name) candidates.push({ name, marked: decoratorName(node) }) break } case 'assignment': candidates.push(...assignmentCandidates(node)) break } for (const child of children(node)) visit(child) } visit(root) return displayNameFacts(candidates, comments, source) } export const pythonNameReader: DisplayNameReader = { files: { python: '**/*.py' }, mentions: source => source.includes(COMMENT_MARK) || /\b(?:Args|Arguments):|:param\s/.test(source), find: (root, source) => pythonDisplayNames(root, source), }