✨ feat(vscode): Rust 的 #![…] 内部属性做成「编译属性」卡片
This commit is contained in:
1 parent
2f610e9cb8
commit
04a11eb0b2
14 files changed
+386
-20
No files matched your search
@@ -428,6 +428,7 @@ export interface DocumentViews {
|
||||
* 命令名的命令规则(SourceDocument.commandRule)。
|
||||
*/
|
||||
export interface DocumentExtras {
|
||||
fileName?: SourceDocument['fileName']
|
||||
rules?: RuleRun
|
||||
calleeKind?: SourceDocument['calleeKind']
|
||||
commandRule?: SourceDocument['commandRule']
|
||||
@@ -467,7 +468,7 @@ export async function readBlockViews(
|
||||
parts.set(code.node.startIndex, { code, root, document, translator: language.translator, steps })
|
||||
}
|
||||
const { names, markers } = adapter.displayNames?.find(tree.rootNode, source, languageId) ?? { names: [], markers: [] }
|
||||
const { calleeKind, commandRule } = extras
|
||||
const { fileName, calleeKind, commandRule } = extras
|
||||
const readings = new Map<number, EmbeddedReading>()
|
||||
const embedded = (node: Node): EmbeddedReading | undefined => {
|
||||
const part = parts.get(node.startIndex)
|
||||
@@ -481,6 +482,7 @@ export async function readBlockViews(
|
||||
source,
|
||||
language: adapter,
|
||||
markers,
|
||||
...(fileName && { fileName }),
|
||||
...(calleeKind && { calleeKind }),
|
||||
...(commandRule && { commandRule }),
|
||||
...(parts.size > 0 && { embedded }),
|
||||
|
||||
@@ -12,7 +12,7 @@ export type { Node }
|
||||
*/
|
||||
export type BlockKind
|
||||
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
|
||||
| 'trait' | 'impl' | 'module'
|
||||
| 'trait' | 'impl' | 'module' | 'attribute'
|
||||
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
|
||||
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline'
|
||||
| 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start'
|
||||
@@ -214,6 +214,35 @@ export interface ClassFacts {
|
||||
implements: Node[]
|
||||
}
|
||||
|
||||
/** Rust 的内部属性 `#![…]`:`cfg_attr(条件, a, b)` 拆成生效条件和 a、b 两个属性,别的属性就是它自己一个。 */
|
||||
export interface AttributeFacts {
|
||||
/** 生效的各个属性,照写的顺序。 */
|
||||
attributes: AttributeItem[]
|
||||
/** `cfg_attr` 的生效条件;别的属性没有。 */
|
||||
condition?: CfgPredicate
|
||||
/** 写在哪一层:文件顶层、`mod` 身体、`impl` 身体;别处(`extern` 块里)没有。作用范围按它定,文件顶层还要看是不是 crate 根文件。 */
|
||||
placement?: 'file' | 'module' | 'impl'
|
||||
}
|
||||
|
||||
/** 一个属性:`名字`、`名字 = 值` 或 `名字(各项)`。 */
|
||||
export interface AttributeItem {
|
||||
/** 整个属性(名字连参数)在源码里的范围,预览照原文着色。 */
|
||||
span: SourceSpan
|
||||
/** 名字,路径照写(`rustfmt::skip`)。 */
|
||||
name: string
|
||||
/** `名字 = 值` 的值:字符串去掉引号,别的照原文。 */
|
||||
value?: string
|
||||
/** `名字(…)` 括号里逗号隔开的各项,照原文(`clippy::too_many_arguments` 是一项);没写括号是空的。 */
|
||||
arguments: string[]
|
||||
}
|
||||
|
||||
/** `cfg` 条件:`not(…)`、`all(…)`、`any(…)`,一个配置项(`test`、`target_os = "windows"`),认不出的写法照原文。 */
|
||||
export type CfgPredicate
|
||||
= | { kind: 'not', operand: CfgPredicate }
|
||||
| { kind: 'all' | 'any', operands: CfgPredicate[] }
|
||||
| { kind: 'option', name: string, value?: string }
|
||||
| { kind: 'other', text: string }
|
||||
|
||||
/** 枚举的一个成员;节点都指向源码。 */
|
||||
export interface EnumMemberFacts {
|
||||
name: Node
|
||||
@@ -480,6 +509,8 @@ export interface TraitBlock extends BlockShape { kind: 'trait', facts: ClassFact
|
||||
/** Rust 的 impl:标题是「某类型 的方法」,方法是子块,放进方法表的某一行。 */
|
||||
export interface ImplBlock extends BlockShape { kind: 'impl', facts: ClassFacts }
|
||||
export interface ModuleBlock extends BlockShape { kind: 'module', facts: ClassFacts }
|
||||
/** Rust 的内部属性 `#![…]`:写给所在 crate 或模块的编译属性。 */
|
||||
export interface AttributeBlock extends BlockShape { kind: 'attribute', facts: AttributeFacts }
|
||||
/** 枚举块的头部是整个枚举(成员都在属性表里讲),没有子块;库规则认出来的枚举(Python)是整个类,留下的方法等是子块。 */
|
||||
export interface EnumBlock extends BlockShape { kind: 'enum', facts: EnumFacts }
|
||||
/** 异常块的头部是整个类(类属性都在属性表里讲),留下的方法(含构造函数)等是子块。 */
|
||||
@@ -497,7 +528,7 @@ export interface StageBlock extends BlockShape { kind: 'stage', facts: StageFact
|
||||
export interface HealthBlock extends BlockShape { kind: 'health', facts: HealthFacts }
|
||||
export interface StartBlock extends BlockShape { kind: 'start', facts: StartFacts }
|
||||
export type FactBlock
|
||||
= | FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
|
||||
= | FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | AttributeBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
|
||||
| DirectiveBlock | StageBlock | HealthBlock | StartBlock
|
||||
|
||||
/** 带说明(facts.doc)的块。 */
|
||||
@@ -695,6 +726,8 @@ export interface DisplayNameReader {
|
||||
export interface SourceDocument {
|
||||
source: string
|
||||
language: LanguageAdapter
|
||||
/** 文件路径(VS Code 的 TextDocument.fileName);宿主没给(嵌着的代码、只读一段源码)时没有,按文件名定的说法写成判断不了的那种。 */
|
||||
fileName?: string
|
||||
/** 写在注释里给代码阅读看的记号(中文名记号,见 DisplayNameFacts.markers):不是说明文字,翻注释、说明时去掉 */
|
||||
markers: SourceRange[]
|
||||
/**
|
||||
@@ -743,7 +776,7 @@ export interface ThemeColor { dark: string, light: string }
|
||||
|
||||
/**
|
||||
* 种类图标:插件自己定义的一组 lucide 图标(https://lucide.dev/icons 上的 kebab-case 名字)。前 26 个给内置种类(几种共用一个:
|
||||
* 命令、管道、执行共用 square-terminal,解释器、解析指令共用 terminal,选项、设置共用 sliders-horizontal,字段、标签共用 tag),
|
||||
* 命令、管道、执行共用 square-terminal,解释器、解析指令共用 terminal,选项、设置、编译属性共用 sliders-horizontal,字段、标签共用 tag),
|
||||
* 宿主标的入口块也用这组里的 play(entries/entryMarks.ts 的 ENTRY_CONCEPT),
|
||||
* 后面的给库规则的种类(React 的组件、状态、引用、数据源、派生值、副作用、事件处理函数、渲染输出也在里面);库规则的种类(KindBadge.icon)只能从这组里挑,要新图标就加进这组(webview 的 kindIcons.tsx 同时给它配上图标),
|
||||
* 没有按任意名字找图标的路。
|
||||
@@ -804,6 +837,7 @@ export const KIND_ICONS: Record<BlockKind, KindIconName> = {
|
||||
trait: 'network',
|
||||
impl: 'square-function',
|
||||
module: 'layers',
|
||||
attribute: 'sliders-horizontal',
|
||||
type: 'braces',
|
||||
enum: 'list-ordered',
|
||||
exception: 'triangle-alert',
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface DocumentSnapshot {
|
||||
languageId: string
|
||||
version: number
|
||||
getText: () => string
|
||||
/** 文件路径,交给翻译插件(SourceDocument.fileName) */
|
||||
fileName?: string
|
||||
/** 这份文件是项目写明的入口时,预览里名字对上的块标成入口(见 entries/entryPoints.ts) */
|
||||
entryNames?: readonly string[]
|
||||
}
|
||||
@@ -57,6 +59,7 @@ export async function documentMessage(
|
||||
try {
|
||||
const rules = registry.rulesFor(languageId)
|
||||
const { blocks, displayNames, symbols } = await readBlockViews(runtime, adapter, translator, languageId, source, {
|
||||
...(document.fileName && { fileName: document.fileName }),
|
||||
rules: { rules, report, lookup },
|
||||
calleeKind,
|
||||
commandRule: commandRuleLookup(registry.commandRulesFor(languageId), report),
|
||||
|
||||
@@ -328,7 +328,7 @@ class Preview {
|
||||
const source = this.document.getText()
|
||||
const session = this.lookups.session()
|
||||
const callees = this.callees.session()
|
||||
const { message, displayNames, symbols } = await documentMessage({ languageId, version, getText: () => source, entryNames: this.owner.entries.namesOf(this.document.uri) }, this.registry, this.runtime, (text) => {
|
||||
const { message, displayNames, symbols } = await documentMessage({ languageId, version, getText: () => source, fileName: this.document.fileName, entryNames: this.owner.entries.namesOf(this.document.uri) }, this.registry, this.runtime, (text) => {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
|
||||
}, session.lookup, callees.calleeKind)
|
||||
if (request !== this.latest || !this.ready)
|
||||
@@ -356,7 +356,7 @@ class Preview {
|
||||
*/
|
||||
private async readOld(source: string): Promise<OldDocument | undefined> {
|
||||
const { message } = await documentMessage(
|
||||
{ languageId: this.document.languageId, version: this.document.version, getText: () => source, entryNames: this.owner.entries.namesOf(this.document.uri) },
|
||||
{ languageId: this.document.languageId, version: this.document.version, getText: () => source, fileName: this.document.fileName, entryNames: this.owner.entries.namesOf(this.document.uri) },
|
||||
this.registry,
|
||||
this.runtime,
|
||||
() => {},
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { toBlockViews } from '../blockView'
|
||||
import { blockMarks } from '../diff/viewDiff'
|
||||
import { rustImplRule } from '../rules/rustImpl'
|
||||
import { isCrateRoot } from '../rules/rustTree'
|
||||
import { readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { rustZhTranslator } from '../translators/rustZh'
|
||||
import { createRustAdapter } from './rust'
|
||||
@@ -10,9 +11,9 @@ import { createRustAdapter } from './rust'
|
||||
const adapter = createRustAdapter(WASM_DIR)
|
||||
const sample = readFixture('rustLang.rs')
|
||||
|
||||
function views(source: string): Promise<BlockView[]> {
|
||||
function views(source: string, fileName?: string): Promise<BlockView[]> {
|
||||
return withBlocks(adapter, 'rust', source, blocks =>
|
||||
toBlockViews(blocks, { source, language: adapter, markers: [] }, rustZhTranslator, {
|
||||
toBlockViews(blocks, { source, language: adapter, markers: [], ...(fileName && { fileName }) }, rustZhTranslator, {
|
||||
rules: [rustImplRule],
|
||||
report: () => {},
|
||||
}))
|
||||
@@ -270,3 +271,68 @@ describe('rust 注释', () => {
|
||||
expect(markdown(impl?.children.find(block => block.kind === 'comment'))).toBe('三\n\n四')
|
||||
})
|
||||
})
|
||||
|
||||
describe('rust 编译属性', () => {
|
||||
it('tauri 的 main.rs 开头的 cfg_attr:属性、生效条件、作用范围、说明', async () => {
|
||||
const source = [
|
||||
'// Prevents additional console window on Windows in release, DO NOT REMOVE!!',
|
||||
'#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]',
|
||||
'',
|
||||
'fn main() {}',
|
||||
'',
|
||||
].join('\n')
|
||||
const list = await views(source, '/repo/apps/desktop/src-tauri/src/main.rs')
|
||||
expect(list.map(block => block.kind)).toEqual(['comment', 'attribute', 'function'])
|
||||
expect(viewLines([card(list, 'attribute')])).toEqual([
|
||||
'attribute 2',
|
||||
' 属性: windows_subsystem = "windows"',
|
||||
' 生效条件: 非 调试构建',
|
||||
' 作用范围: 整个 crate',
|
||||
' 说明: 在 Windows 上编译为图形界面程序(windows 子系统),启动时不弹出控制台窗口',
|
||||
])
|
||||
const attribute = card(list, 'attribute')
|
||||
const value = 'properties' in attribute ? attribute.properties[0]?.value : undefined
|
||||
expect(Array.isArray(value) && value.map(piece => 'role' in piece && piece.role)).toEqual(['variable', 'plain', 'plain', 'plain', 'string', 'string', 'string'])
|
||||
})
|
||||
|
||||
it('allow 列出各个 lint,别的文件顶层是本模块', async () => {
|
||||
const list = await views('#![allow(dead_code, unused_imports)]\n', '/repo/src/credentials.rs')
|
||||
expect(viewLines(list)).toEqual([
|
||||
'attribute 1',
|
||||
' 属性: allow(dead_code, unused_imports)',
|
||||
' 作用范围: 本模块',
|
||||
' 说明: 编译检查(lint)级别设为放行:dead_code、unused_imports',
|
||||
])
|
||||
})
|
||||
|
||||
it('认不得的属性没有说明,不知道文件名时作用范围是所在模块', async () => {
|
||||
expect(viewLines(await views('#![rustfmt::skip]\n'))).toEqual([
|
||||
'attribute 1',
|
||||
' 属性: rustfmt::skip',
|
||||
' 作用范围: 所在模块',
|
||||
])
|
||||
})
|
||||
|
||||
it('cfg_attr 写了几个属性是一张表;all、any 套着的条件加括号;mod 身体里是本模块', async () => {
|
||||
const source = [
|
||||
'mod platform {',
|
||||
' #![cfg_attr(all(unix, not(any(test, feature = "mock"))), allow(clippy::foo), deny(warnings))]',
|
||||
'}',
|
||||
'',
|
||||
].join('\n')
|
||||
const mod = card(await views(source, '/repo/src/main.rs'), 'module')
|
||||
expect(viewLines(mod.children)).toEqual([
|
||||
'attribute 2',
|
||||
' 属性: 2 个',
|
||||
' allow(clippy::foo) | 编译检查(lint)级别设为放行:clippy::foo',
|
||||
' deny(warnings) | 编译检查(lint)级别设为报错:warnings',
|
||||
' 生效条件: unix 并且 非 (测试构建 或者 启用特性 mock)',
|
||||
' 作用范围: 本模块',
|
||||
])
|
||||
})
|
||||
|
||||
it('crate 根文件:main.rs、lib.rs、src/bin 下的、包根 tests / examples / benches 里直接放的', () => {
|
||||
expect(['/p/src/main.rs', '/p/src/lib.rs', '/p/src/bin/tool.rs', '/p/tests/api.rs', 'C:\\p\\examples\\demo.rs'].map(isCrateRoot)).toEqual([true, true, true, true, true])
|
||||
expect(['/p/src/credentials.rs', '/p/src/tests/helpers.rs', '/p/src/bin/tool/args.rs'].map(isCrateRoot)).toEqual([false, false, false])
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Access, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, TokenRole, TraitBlock } from '../contract'
|
||||
import type { Access, AttributeFacts, AttributeItem, CfgPredicate, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TokenRole, TraitBlock } from '../contract'
|
||||
import type { CommentRules } from './shared'
|
||||
import * as path from 'node:path'
|
||||
import { leadingAttributes, visibilityFacts } from '../rules/rustTree'
|
||||
import { compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named } from './shared'
|
||||
import { children, compoundShape, extendHeader, headerSpan, isCommentNode, leafBlock, leafShape, listBlocks, named } from './shared'
|
||||
|
||||
const STORAGE = new Set(['pub', 'async', 'const', 'static', 'mut', 'let'])
|
||||
|
||||
@@ -69,6 +69,8 @@ function itemBlock(item: Node): CodeBlock {
|
||||
case 'function_item':
|
||||
case 'function_signature_item':
|
||||
return functionBlock(item)
|
||||
case 'inner_attribute_item':
|
||||
return innerAttributeBlock(item)
|
||||
default:
|
||||
return leafBlock('statement', item)
|
||||
}
|
||||
@@ -104,6 +106,109 @@ function valueBlock(node: Node, kind: 'const' | 'variable'): CodeBlock {
|
||||
}
|
||||
}
|
||||
|
||||
const PLACEMENTS: Record<string, AttributeFacts['placement']> = { source_file: 'file', mod_item: 'module', impl_item: 'impl' }
|
||||
|
||||
/** `#![…]`:写给所在 crate 或模块的编译属性。`cfg_attr(条件, a, b)` 拆成生效条件和 a、b。 */
|
||||
function innerAttributeBlock(node: Node): CodeBlock {
|
||||
const attribute = named(node).find(child => child.type === 'attribute')
|
||||
if (!attribute)
|
||||
return leafBlock('statement', node)
|
||||
const meta = metaItem(tokens(attribute))
|
||||
const [condition, ...applied] = meta.name === 'cfg_attr' ? meta.list ?? [] : []
|
||||
const conditional = condition !== undefined && applied.length > 0
|
||||
const container = node.parent?.type === 'source_file' ? node.parent : node.parent?.parent
|
||||
const placement = container ? PLACEMENTS[container.type] : undefined
|
||||
return {
|
||||
kind: 'attribute',
|
||||
...leafShape(node),
|
||||
facts: {
|
||||
attributes: conditional ? applied.map(item => attributeItem(metaItem(item))) : [attributeItem(meta)],
|
||||
...(conditional && { condition: cfgPredicate(condition) }),
|
||||
...(placement && { placement }),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** 属性的写法:名字(路径),后面 `= 值` 或括号里逗号隔开的各段。tokens 是这一段的各个词。 */
|
||||
interface MetaItem {
|
||||
tokens: Node[]
|
||||
name: string
|
||||
value?: Node
|
||||
list?: Node[][]
|
||||
}
|
||||
|
||||
function metaItem(words: Node[]): MetaItem {
|
||||
const at = words.findIndex(word => word.type === '=' || word.type === 'token_tree')
|
||||
const path = at < 0 ? words : words.slice(0, at)
|
||||
const mark = words[at]
|
||||
return {
|
||||
tokens: words,
|
||||
name: textOf(path),
|
||||
...(mark?.type === '=' && words[at + 1] && { value: words[at + 1] }),
|
||||
...(mark?.type === 'token_tree' && { list: listItems(mark) }),
|
||||
}
|
||||
}
|
||||
|
||||
/** 括号里逗号隔开的各段,每段是它的各个词。 */
|
||||
function listItems(tree: Node): Node[][] {
|
||||
const items: Node[][] = [[]]
|
||||
for (const word of tokens(tree).slice(1, -1)) {
|
||||
if (word.type === ',')
|
||||
items.push([])
|
||||
else
|
||||
items[items.length - 1].push(word)
|
||||
}
|
||||
return items.filter(item => item.length > 0)
|
||||
}
|
||||
|
||||
function attributeItem(meta: MetaItem): AttributeItem {
|
||||
return {
|
||||
span: spanOf(meta.tokens),
|
||||
name: meta.name,
|
||||
...(meta.value && { value: literalText(meta.value) }),
|
||||
arguments: (meta.list ?? []).map(textOf),
|
||||
}
|
||||
}
|
||||
|
||||
function cfgPredicate(words: Node[]): CfgPredicate {
|
||||
const meta = metaItem(words)
|
||||
if (meta.list && (meta.name === 'all' || meta.name === 'any'))
|
||||
return { kind: meta.name, operands: meta.list.map(cfgPredicate) }
|
||||
if (meta.list?.length === 1 && meta.name === 'not')
|
||||
return { kind: 'not', operand: cfgPredicate(meta.list[0]) }
|
||||
if (!meta.list && words[0]?.type === 'identifier' && (words.length === 1 || meta.value))
|
||||
return { kind: 'option', name: meta.name, ...(meta.value && { value: literalText(meta.value) }) }
|
||||
return { kind: 'other', text: textOf(words) }
|
||||
}
|
||||
|
||||
/** 节点的各个词(含标点),注释不算。 */
|
||||
function tokens(node: Node): Node[] {
|
||||
return children(node).filter(child => !isCommentNode(child))
|
||||
}
|
||||
|
||||
/** 连着的几个词在源码里的原文(它们是同一个节点的子节点)。 */
|
||||
function textOf(words: Node[]): string {
|
||||
const [first] = words
|
||||
const last = words[words.length - 1]
|
||||
const parent = first?.parent
|
||||
if (!first || !last || !parent)
|
||||
return ''
|
||||
return parent.text.slice(first.startIndex - parent.startIndex, last.endIndex - parent.startIndex)
|
||||
}
|
||||
|
||||
function spanOf(words: Node[]): SourceSpan {
|
||||
const first = words[0]
|
||||
const last = words[words.length - 1]
|
||||
return { startIndex: first.startIndex, endIndex: last.endIndex, startRow: first.startPosition.row, endRow: last.endPosition.row }
|
||||
}
|
||||
|
||||
/** 字面量的值:字符串去掉引号,别的照原文。 */
|
||||
function literalText(node: Node): string {
|
||||
if (node.type !== 'string_literal')
|
||||
return node.text
|
||||
return named(node).find(child => child.type === 'string_content')?.text ?? ''
|
||||
}
|
||||
|
||||
/** 写在前面的 `#[…]` 算进头部。 */
|
||||
function withAttributes(block: CodeBlock): CodeBlock {
|
||||
const [attribute] = leadingAttributes(block.node)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClassBlock, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, PropertyItem, PropertyRow, TokenRole, TraitBlock } from '../contract'
|
||||
import type { AttributeBlock, AttributeFacts, AttributeItem, CfgPredicate, ClassBlock, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, PropertyItem, PropertyRow, SourceSpan, TokenRole, TraitBlock } from '../contract'
|
||||
import type { RustVisibility } from './rustTree'
|
||||
import { bodyFunctions, docSentence, isMethod, matchArms, oneLine, returnText, signatureParams, structFields, variantPayload } from './rustTree'
|
||||
|
||||
@@ -72,6 +72,97 @@ export function moduleRows(block: ModuleBlock, say: RustSay): PropertyRow[] {
|
||||
]
|
||||
}
|
||||
|
||||
const LINT_LEVELS: Record<string, string> = { allow: '放行', warn: '警告', deny: '报错', forbid: '强制报错(内层不能再放宽)' }
|
||||
|
||||
/**
|
||||
* 编译属性 `#![…]`:属性(照原文着色)/ 生效条件(只有 cfg_attr)/ 作用范围 / 说明(认得的属性才有)。
|
||||
* cfg_attr 里写了几个属性时「属性」是一张「属性 | 说明」表,不再另起「说明」一行。
|
||||
* original 把源码的一段照原文着色;crateRoot 是这份文件是不是 crate 根文件,不知道文件名时没有。
|
||||
*/
|
||||
export function attributeRows(block: AttributeBlock, original: (span: SourceSpan) => Inline[], crateRoot: boolean | undefined): PropertyRow[] {
|
||||
const { attributes, condition, placement } = block.facts
|
||||
const single = attributes.length === 1 ? attributes[0] : undefined
|
||||
const description = single && attributeNote(single)
|
||||
return [
|
||||
single
|
||||
? { label: '属性', value: original(single.span) }
|
||||
: {
|
||||
label: '属性',
|
||||
value: [hint(`${attributes.length} 个`)],
|
||||
columns: ['属性', '说明'],
|
||||
items: attributes.map(item => ({ cells: [original(item.span), attributeNote(item) ?? [hint('—')]] })),
|
||||
},
|
||||
...(condition ? [{ label: '生效条件', value: cfgText(condition, false) }] : []),
|
||||
{ label: '作用范围', value: [keyword(attributeScope(placement, crateRoot))] },
|
||||
...(description ? [{ label: '说明', value: description }] : []),
|
||||
]
|
||||
}
|
||||
|
||||
function attributeScope(placement: AttributeFacts['placement'], crateRoot: boolean | undefined): string {
|
||||
switch (placement) {
|
||||
case 'file':
|
||||
return crateRoot === undefined ? '所在模块' : crateRoot ? '整个 crate' : '本模块'
|
||||
case 'module':
|
||||
return '本模块'
|
||||
case 'impl':
|
||||
return '本 impl 块'
|
||||
default:
|
||||
return '所在模块'
|
||||
}
|
||||
}
|
||||
|
||||
/** 认得的属性的书面说明;认不得的没有。 */
|
||||
function attributeNote(item: AttributeItem): Inline[] | undefined {
|
||||
const names = item.arguments.flatMap((name, i) => [...(i > 0 ? [note('、')] : []), { text: name, role: 'variable' as const }])
|
||||
const level = LINT_LEVELS[item.name]
|
||||
if (level)
|
||||
return names.length > 0 ? [note(`编译检查(lint)级别设为${level}:`), ...names] : undefined
|
||||
switch (item.name) {
|
||||
case 'windows_subsystem':
|
||||
if (item.value === 'windows')
|
||||
return [note('在 Windows 上编译为图形界面程序(windows 子系统),启动时不弹出控制台窗口')]
|
||||
return item.value === 'console' ? [note('在 Windows 上编译为控制台程序(console 子系统),启动时打开控制台窗口')] : undefined
|
||||
case 'feature':
|
||||
return names.length > 0 ? [note('启用实验特性(需 nightly 编译器):'), ...names] : undefined
|
||||
case 'no_std':
|
||||
return [note('不链接标准库 std,只使用核心库 core')]
|
||||
case 'no_main':
|
||||
return [note('不使用标准的 main 入口函数,程序入口另行提供')]
|
||||
case 'recursion_limit':
|
||||
return item.value ? [note(`宏展开等编译期递归的深度上限设为 ${item.value}(默认 128)`)] : undefined
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const CFG_OPTIONS: Record<string, string> = { debug_assertions: '调试构建', test: '测试构建' }
|
||||
const CFG_VALUES: Record<string, string> = { target_os: '目标系统', feature: '启用特性' }
|
||||
|
||||
/** cfg 条件读成中文:not 是「非」,all 用「并且」、any 用「或者」连起来,套在别的条件里的加括号;认不得的照原文。 */
|
||||
function cfgText(predicate: CfgPredicate, nested: boolean): Inline[] {
|
||||
switch (predicate.kind) {
|
||||
case 'not':
|
||||
return [operator('非'), plain(' '), ...cfgText(predicate.operand, true)]
|
||||
case 'all':
|
||||
case 'any': {
|
||||
if (predicate.operands.length === 0)
|
||||
return [hint(predicate.kind === 'all' ? '总是成立' : '从不成立')]
|
||||
const joiner = operator(predicate.kind === 'all' ? '并且' : '或者')
|
||||
const parts = predicate.operands.flatMap((operand, i) => [...(i > 0 ? [plain(' '), joiner, plain(' ')] : []), ...cfgText(operand, true)])
|
||||
return nested && predicate.operands.length > 1 ? [plain('('), ...parts, plain(')')] : parts
|
||||
}
|
||||
case 'option': {
|
||||
const { name, value } = predicate
|
||||
if (value === undefined)
|
||||
return [plain(CFG_OPTIONS[name] ?? name)]
|
||||
const label = CFG_VALUES[name]
|
||||
return label ? [plain(`${label} `), { text: value, role: 'string' }] : [plain(`${name} = `), { text: `"${value}"`, role: 'string' }]
|
||||
}
|
||||
case 'other':
|
||||
return [plain(predicate.text)]
|
||||
}
|
||||
}
|
||||
|
||||
export function enumRows(block: EnumBlock, say: RustSay): PropertyRow[] {
|
||||
return [
|
||||
nameRow(block.facts.name, say, 'type'),
|
||||
@@ -178,6 +269,14 @@ function keyword(text: string): Inline {
|
||||
return { text, role: 'keyword' }
|
||||
}
|
||||
|
||||
function operator(text: string): Inline {
|
||||
return { text, role: 'operator' }
|
||||
}
|
||||
|
||||
function note(text: string): Inline {
|
||||
return { text, role: 'comment' }
|
||||
}
|
||||
|
||||
function range(node: import('../contract').Node): { startIndex: number, endIndex: number } {
|
||||
return { startIndex: node.startIndex, endIndex: node.endIndex }
|
||||
}
|
||||
|
||||
@@ -32,6 +32,21 @@ export function oneLine(text: string): string {
|
||||
return text.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
const TARGET_DIRS = new Set(['examples', 'tests', 'benches'])
|
||||
|
||||
/**
|
||||
* Cargo 默认认作 crate 根的文件:`main.rs`、`lib.rs`,`src/bin/` 下的 `.rs`,
|
||||
* 包根下 `examples/`、`tests/`、`benches/` 里直接放的 `.rs`(`src/` 下同名目录里的是模块)。
|
||||
*/
|
||||
export function isCrateRoot(fileName: string): boolean {
|
||||
const [base, parent, grandparent] = fileName.split(/[\\/]/).reverse()
|
||||
if (base === 'main.rs' || base === 'lib.rs')
|
||||
return true
|
||||
if (!base?.endsWith('.rs') || parent === undefined)
|
||||
return false
|
||||
return parent === 'bin' ? grandparent === 'src' : TARGET_DIRS.has(parent) && grandparent !== 'src'
|
||||
}
|
||||
|
||||
/** 节点上的可见性。pub(in …) 不在这四档里,按本 crate 算(只本包能用)。 */
|
||||
export function rustVisibility(node: Node): RustVisibility {
|
||||
const vis = named(node).find(child => child.type === 'visibility_modifier')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { FactBlock, Node, PropertyRow, SourceDocument, Translator } from '../contract'
|
||||
import { enumRows, functionRows, implRows, importRows, moduleRows, plainSay, structRows, traitRows } from '../rules/rustRows'
|
||||
import type { FactBlock, Inline, Node, PropertyRow, SourceDocument, SourceSpan, Translator } from '../contract'
|
||||
import { attributeRows, enumRows, functionRows, implRows, importRows, moduleRows, plainSay, structRows, traitRows } from '../rules/rustRows'
|
||||
import { isCrateRoot } from '../rules/rustTree'
|
||||
import { createZhTemplateTranslator, KIND_ZH, ZH_ATTRIBUTE_NAMES } from './zhTemplate'
|
||||
|
||||
const say = plainSay()
|
||||
@@ -96,6 +97,8 @@ function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
|
||||
return implRows(block, say)
|
||||
case 'module':
|
||||
return moduleRows(block, say)
|
||||
case 'attribute':
|
||||
return attributeRows(block, span => original(block.node, span, document), document.fileName === undefined ? undefined : isCrateRoot(document.fileName))
|
||||
case 'const':
|
||||
case 'variable':
|
||||
return template.describe(block, document)
|
||||
@@ -104,6 +107,29 @@ function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
|
||||
}
|
||||
}
|
||||
|
||||
/** node 里 span 那一段照原文:叶子按源码着色,叶子之间的空白照抄。 */
|
||||
function original(node: Node, span: SourceSpan, { source, language }: SourceDocument): Inline[] {
|
||||
const out: Inline[] = []
|
||||
let cursor = span.startIndex
|
||||
const visit = (current: Node): void => {
|
||||
if (current.endIndex <= span.startIndex || current.startIndex >= span.endIndex || current.startIndex === current.endIndex)
|
||||
return
|
||||
if (current.childCount === 0) {
|
||||
if (current.startIndex > cursor)
|
||||
out.push({ text: source.slice(cursor, current.startIndex), role: 'plain' })
|
||||
out.push({ text: current.text, role: language.tokenRole(current) })
|
||||
cursor = current.endIndex
|
||||
return
|
||||
}
|
||||
for (const child of current.children) {
|
||||
if (child)
|
||||
visit(child)
|
||||
}
|
||||
}
|
||||
visit(node)
|
||||
return out
|
||||
}
|
||||
|
||||
/** 行注释去掉 `//`(文档注释连 `/`、`!`)和后面一个空格,缩进留给 Markdown;块注释去掉开头结尾的注释符号。 */
|
||||
function commentText(node: Node, text: string): string {
|
||||
if (node.type === 'line_comment')
|
||||
|
||||
@@ -207,6 +207,7 @@ export const KIND_ZH: Record<BlockKind, string> = {
|
||||
trait: '特质',
|
||||
impl: '实现',
|
||||
module: '模块',
|
||||
attribute: '编译属性',
|
||||
enum: '枚举',
|
||||
exception: '异常',
|
||||
type: '类型',
|
||||
@@ -722,6 +723,9 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return healthRows(block.facts, phrases)
|
||||
case 'start':
|
||||
return startRows(block.facts, phrases)
|
||||
// Rust 的编译属性由 Rust 的直译排(translators/rustZh.ts)
|
||||
case 'attribute':
|
||||
return []
|
||||
default:
|
||||
return block.facts.scope ? scopedVariableRows(block.facts.declarators, block.facts.scope, phrases) : variableRows(block, phrases)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user