190 lines
7.7 KiB
TypeScript
190 lines
7.7 KiB
TypeScript
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),
|
||
}
|