✨ feat(vscode): Rust 的 #![…] 内部属性做成「编译属性」卡片
This commit is contained in:
1 parent
2f610e9cb8
commit
04a11eb0b2
14 files changed
+386
-20
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user