Files
superwork/vscode/src/codeReader/languages/pythonNames.ts
T

190 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, string> {
const found = new Map<string, string>()
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<string, string>() : 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),
}