✨ feat(vscode): 代码阅读的 FastAPI 接口多一个筛选属性「请求方法」(GET / POST …);库规则自己报的筛选属性由规则起名(LibraryRule.attributeNames)
This commit is contained in:
1 parent
ec34ace7cf
commit
b4e4879570
9 files changed
+107
-11
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import type { BlockView } from './blockView'
|
||||
import type { CodeBlock, LanguageAdapter, Translator } from './contract'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { blockAttributes, fileAttributes } from './attributes'
|
||||
import { blockAttributes, fileAttributes, mergeAttributeNames } from './attributes'
|
||||
import { toBlockViews } from './blockView'
|
||||
import { createPythonAdapter } from './languages/python'
|
||||
import { createTypeScriptAdapter } from './languages/typescript'
|
||||
@@ -139,6 +139,17 @@ describe('文件里出现过的属性', () => {
|
||||
expect(found[0].name).toBe('kind')
|
||||
})
|
||||
|
||||
it('几份叫法合成一份:同一个属性、值排在前面的(翻译插件的)先用,没给的再取后面的(库规则的)', () => {
|
||||
expect(mergeAttributeNames([
|
||||
{ attributes: { access: '访问级别' }, values: { access: { public: '公共' } } },
|
||||
undefined,
|
||||
{ attributes: { access: '可见性', httpMethod: '请求方法' }, values: { access: { public: '开放', private: '内部' } } },
|
||||
])).toEqual({
|
||||
attributes: { access: '访问级别', httpMethod: '请求方法' },
|
||||
values: { access: { public: '公共', private: '内部' } },
|
||||
})
|
||||
})
|
||||
|
||||
it('中文直译给通用属性和值起名,和属性表里的叫法一样', async () => {
|
||||
const list = await views(ts, 'typescript', TS_SOURCE, typescriptZhTranslator)
|
||||
const found = fileAttributes(list, typescriptZhTranslator.attributeNames)
|
||||
|
||||
@@ -80,3 +80,16 @@ export function fileAttributes(blocks: readonly Attributed[], names: AttributeNa
|
||||
values: [...values].map(([value, count]) => ({ id: value, name: valueName(id, value), count })),
|
||||
}))
|
||||
}
|
||||
|
||||
/** 几份属性和值的叫法合成一份:同一个属性、同一个值谁排在前面、给了名字就用谁的(翻译插件的排在库规则的前面)。 */
|
||||
export function mergeAttributeNames(list: readonly (AttributeNames | undefined)[]): AttributeNames {
|
||||
const merged: AttributeNames = { attributes: {}, values: {} }
|
||||
for (const names of [...list].reverse()) {
|
||||
if (!names)
|
||||
continue
|
||||
Object.assign(merged.attributes, names.attributes)
|
||||
for (const [id, values] of Object.entries(names.values))
|
||||
merged.values[id] = { ...merged.values[id], ...values }
|
||||
}
|
||||
return merged
|
||||
}
|
||||
@@ -466,7 +466,10 @@ export interface RuleCard {
|
||||
rows: PropertyRow[]
|
||||
/** 块自己的名字(类名)在源码里的范围:跳到的定义就是它时闪整块。 */
|
||||
defines?: SourceRange
|
||||
/** 规则自己的筛选属性,叠在宿主从原块推出的属性上(同 BlockShape.attributes);kind 总是上面的 kind,这里填了不算。 */
|
||||
/**
|
||||
* 规则自己的筛选属性,叠在宿主从原块推出的属性上(同 BlockShape.attributes);kind 总是上面的 kind,这里填了不算。
|
||||
* 属性和值的名字由规则给(LibraryRule.attributeNames)。
|
||||
*/
|
||||
attributes?: BlockAttributes
|
||||
/**
|
||||
* 并进这一块的子块(原块 children 里的对象本身):不再单独成块,内容由 rows 讲。
|
||||
@@ -580,6 +583,12 @@ export interface LibraryRule {
|
||||
* 查到几处就跳、挑,和语言扩展查到的一样。没给就不能在段上标引用。
|
||||
*/
|
||||
resolve?: (key: string, workspace: RuleWorkspace) => Promise<RuleLocation[]>
|
||||
/**
|
||||
* 规则自己报的筛选属性(RuleCard.attributes,例如 FastAPI 接口的 httpMethod)和值叫什么,同 Translator.attributeNames:
|
||||
* 用词归规则,按翻译插件的 displayLanguage 挑(没有翻译插件时是 undefined),没有这种语言的词表就用规则默认的那份。
|
||||
* 筛选栏的名字先取翻译插件给的,没给的再取规则给的;都没给的显示 id。
|
||||
*/
|
||||
attributeNames?: (displayLanguage: string | undefined) => AttributeNames
|
||||
}
|
||||
|
||||
export interface CodeReaderApi {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { createPythonAdapter } from '../languages/python'
|
||||
import { createTypeScriptAdapter } from '../languages/typescript'
|
||||
import { CodeReaderRegistry } from '../registry'
|
||||
import { fastapiRule } from '../rules/fastapi'
|
||||
import { sqlalchemyRule } from '../rules/sqlalchemy'
|
||||
import { runtime, WASM_DIR } from '../testSupport'
|
||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||
@@ -82,6 +83,41 @@ describe('预览的 document 消息', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('库规则自己的筛选属性:名字取规则给的(FastAPI 接口的「请求方法」),值照写;规则起名时抛错记下来、照写 id', async () => {
|
||||
const routes = [
|
||||
'from fastapi import APIRouter',
|
||||
'',
|
||||
'router = APIRouter(prefix="/users")',
|
||||
'',
|
||||
'@router.get("/{user_id}")',
|
||||
'def get_user(user_id: int):',
|
||||
' pass',
|
||||
'',
|
||||
'@router.put("/{user_id}")',
|
||||
'def update_user(user_id: int):',
|
||||
' pass',
|
||||
'',
|
||||
'@router.get("")',
|
||||
'def list_users():',
|
||||
' pass',
|
||||
'',
|
||||
].join('\n')
|
||||
const { message } = await documentMessage(doc('python', routes), registryWithPython(fastapiRule), runtime, ignore)
|
||||
expect(message.attributes.find(attribute => attribute.id === 'httpMethod')).toEqual({
|
||||
id: 'httpMethod',
|
||||
name: '请求方法',
|
||||
values: [{ id: 'GET', name: 'GET', count: 2 }, { id: 'PUT', name: 'PUT', count: 1 }],
|
||||
})
|
||||
|
||||
const reports: string[] = []
|
||||
const broken: LibraryRule = { ...fastapiRule, attributeNames: () => {
|
||||
throw new Error('词表坏了')
|
||||
} }
|
||||
const again = await documentMessage(doc('python', routes), registryWithPython(broken), runtime, text => reports.push(text))
|
||||
expect(again.message.attributes.find(attribute => attribute.id === 'httpMethod')?.name).toBe('httpMethod')
|
||||
expect(reports).toEqual(['库规则 fastapi 给筛选属性起名时出错,筛选栏照写 id:词表坏了'])
|
||||
})
|
||||
|
||||
it('库规则抛错:不算解析失败,交给 report 记下,这块照常显示', async () => {
|
||||
const broken: LibraryRule = {
|
||||
id: 'broken',
|
||||
|
||||
@@ -2,8 +2,9 @@ import type { DisplayName, RuleContext } from '../contract'
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { DocumentMessage } from './messages'
|
||||
import { fileAttributes } from '../attributes'
|
||||
import { fileAttributes, mergeAttributeNames } from '../attributes'
|
||||
import { readBlockViews } from '../blockView'
|
||||
import { ruleAttributeNames } from '../libraryRules'
|
||||
|
||||
/** 生成预览数据要读的那点文档信息(TextDocument 的子集)。 */
|
||||
export interface DocumentSnapshot {
|
||||
@@ -20,7 +21,7 @@ export interface DocumentRead {
|
||||
|
||||
/**
|
||||
* 当前文档 → 发给预览的 document 消息;语言不支持或解析失败时带 error,blocks 为空。库规则出错不算失败,交给 report 记下;
|
||||
* 规则查名字的定义时问 lookup(RuleContext.lookup)。
|
||||
* 规则查名字的定义时问 lookup(RuleContext.lookup)。筛选属性和值的名字先取翻译插件的,没给的再取库规则的(LibraryRule.attributeNames)。
|
||||
*/
|
||||
export async function documentMessage(
|
||||
document: DocumentSnapshot,
|
||||
@@ -46,8 +47,10 @@ export async function documentMessage(
|
||||
if (!adapter)
|
||||
return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [] }
|
||||
try {
|
||||
const { blocks, displayNames } = await readBlockViews(runtime, adapter, translator, languageId, source, { rules: registry.rulesFor(languageId), report, lookup })
|
||||
return { message: { ...message, blocks, attributes: fileAttributes(blocks, translator?.attributeNames) }, displayNames }
|
||||
const rules = registry.rulesFor(languageId)
|
||||
const { blocks, displayNames } = await readBlockViews(runtime, adapter, translator, languageId, source, { rules, report, lookup })
|
||||
const names = mergeAttributeNames([translator?.attributeNames, ...ruleAttributeNames(rules, translator?.displayLanguage, report)])
|
||||
return { message: { ...message, blocks, attributes: fileAttributes(blocks, names) }, displayNames }
|
||||
}
|
||||
catch (err) {
|
||||
return { message: { ...message, error: err instanceof Error ? err.message : String(err) }, displayNames: [] }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CodeBlock, ImportFacts, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleBlock, RuleContext, RuleLocation, RuleReference, RuleWorkspace, Segment, SourceSpan } from './contract'
|
||||
import type { AttributeNames, CodeBlock, ImportFacts, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleBlock, RuleContext, RuleLocation, RuleReference, RuleWorkspace, Segment, SourceSpan } from './contract'
|
||||
|
||||
/** 宿主这边怎么用库规则:什么时候启用、问哪个规则、认领了之后编辑哪一段、留下哪些子块。规则本身只经 contract 拿信息。 */
|
||||
|
||||
@@ -84,6 +84,19 @@ export function claimBlock(block: CodeBlock, rules: readonly LibraryRule[], cont
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 各规则给自己的筛选属性起的名字(LibraryRule.attributeNames),按注册顺序;规则抛错时交给 report(记日志),当它没给。 */
|
||||
export function ruleAttributeNames(rules: readonly LibraryRule[], displayLanguage: string | undefined, report: (message: string) => void): AttributeNames[] {
|
||||
return rules.flatMap((rule) => {
|
||||
try {
|
||||
return rule.attributeNames ? [rule.attributeNames(displayLanguage)] : []
|
||||
}
|
||||
catch (err) {
|
||||
report(`库规则 ${rule.id} 给筛选属性起名时出错,筛选栏照写 id:${err instanceof Error ? err.message : String(err)}`)
|
||||
return []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 源码里一个节点占的那段。 */
|
||||
function nodeSpan(node: Node): SourceSpan {
|
||||
return { startIndex: node.startIndex, endIndex: node.endIndex, startRow: node.startPosition.row, endRow: node.endPosition.row }
|
||||
|
||||
@@ -83,7 +83,8 @@ describe('fastapi 库规则:接口卡片', () => {
|
||||
' 422 | 参数校验失败 | FastAPI 自动返回',
|
||||
])
|
||||
expect(properties(card).map(row => row.columns)).toEqual([undefined, undefined, undefined, undefined, undefined, ['项', '值'], ['状态码', '内容', '说明']])
|
||||
expect(card.attributes).toMatchObject({ kind: 'fastapi.route', access: 'public', async: 'true' })
|
||||
expect(card.attributes).toMatchObject({ kind: 'fastapi.route', access: 'public', async: 'true', httpMethod: 'PUT' })
|
||||
expect(fastapiRule.attributeNames?.('zh')).toEqual({ attributes: { httpMethod: '请求方法' }, values: {} })
|
||||
expect(ROUTES.slice(card.defines?.startIndex, card.defines?.endIndex)).toBe('update_user')
|
||||
// 函数体照常是子块,卡片的编辑范围是整个函数(连装饰器)
|
||||
expect(card.children.length).toBeGreaterThan(0)
|
||||
|
||||
@@ -18,6 +18,9 @@ const ICON: KindIconName = 'globe'
|
||||
/** 认路由对象、参数标记、HTTPException 时看从这几个库导入的名字(HTTPException、status 也可以从 starlette 来)。 */
|
||||
const NAME_LIBRARIES = ['fastapi', 'starlette']
|
||||
|
||||
/** 接口的筛选属性:HTTP 方法,值是大写的 GET、POST……(名字由 attributeNames 给,值照写)。 */
|
||||
const METHOD_ATTRIBUTE = 'httpMethod'
|
||||
|
||||
/** 路由对象上挂接口的方法:`router.get`、`app.post` …… */
|
||||
const METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'])
|
||||
/** HTTP 方法的颜色:Swagger UI 的配色,看接口文档的人最熟;白底上照原色看不清,亮色主题取同色相深一些的。 */
|
||||
@@ -77,6 +80,8 @@ interface Words {
|
||||
messageSeparator: string
|
||||
/** 依赖注进哪个参数:「(param)」 */
|
||||
injectedInto: (param: string) => string
|
||||
/** 筛选属性 httpMethod 叫什么(值 GET、POST……照写) */
|
||||
methodAttribute: string
|
||||
}
|
||||
|
||||
const ZH: Words = {
|
||||
@@ -93,6 +98,7 @@ const ZH: Words = {
|
||||
listSeparator: '、',
|
||||
messageSeparator: ';',
|
||||
injectedInto: param => `(${param})`,
|
||||
methodAttribute: '请求方法',
|
||||
}
|
||||
|
||||
/** 显示语言 → 用词;翻译插件的显示语言这里没有时用中文。 */
|
||||
@@ -620,6 +626,7 @@ function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard {
|
||||
badge: { label: words.kind, color: COLOR, icon: ICON },
|
||||
rows,
|
||||
...(facts.name && { defines: range(facts.name) }),
|
||||
attributes: { [METHOD_ATTRIBUTE]: route.method },
|
||||
absorbs: [],
|
||||
}
|
||||
}
|
||||
@@ -641,4 +648,5 @@ export const fastapiRule: LibraryRule = {
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
attributeNames: displayLanguage => ({ attributes: { [METHOD_ATTRIBUTE]: (WORDS[displayLanguage ?? ''] ?? ZH).methodAttribute }, values: {} }),
|
||||
}
|
||||
Reference in new issue
Block a user