✨ feat(vscode): shell 被当成一串步骤执行时(Dockerfile 的 RUN)只用 && 连着的一串拆成一条一步;tree-sitter-bash 把 a && b > f 读成整串带重定向,重定向改算在最后那条命令上
This commit is contained in:
1 parent
3353cf870c
commit
149cb51cc0
2 files changed
+90
-2
No files matched your search
@@ -138,6 +138,33 @@ describe('shell 属性表信息(facts)', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('shell 被别的语言嵌着当步骤执行(Dockerfile 的 RUN)', () => {
|
||||||
|
const steps = async (source: string): Promise<{ blocks: Outline[], chained: boolean, headers: string[] } | undefined> => {
|
||||||
|
const parsed = await runtime.parse(adapter, 'shellscript', source)
|
||||||
|
try {
|
||||||
|
const found = adapter.steps?.(parsed.rootNode, source, 'shellscript')
|
||||||
|
return found && { blocks: outline(found.blocks), chained: found.chained, headers: found.blocks.map(block => source.slice(block.header.startIndex, block.header.endIndex)) }
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
parsed.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('只用 && 连着的一串拆成一条一步(一步失败就停);整串带着的重定向只算在最后一条上,它的头部延到重定向结尾', async () => {
|
||||||
|
expect(await steps('apt-get update && echo hi > /f && rm -rf /x')).toEqual({
|
||||||
|
blocks: [{ at: 1, kind: 'command' }, { at: 1, kind: 'command' }, { at: 1, kind: 'command' }],
|
||||||
|
chained: true,
|
||||||
|
headers: ['apt-get update', 'echo hi > /f', 'rm -rf /x'],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('条件串、有 || 的串、几句分开写的照 toBlocks 一句一步,不算一步失败就停', async () => {
|
||||||
|
expect(await steps('[ -f x ] && rm x')).toMatchObject({ blocks: [{ at: 1, kind: 'if' }], chained: false })
|
||||||
|
expect(await steps('a && b || c')).toMatchObject({ blocks: [{ at: 1, kind: 'command' }], chained: false })
|
||||||
|
expect(await steps('a\nb')).toMatchObject({ blocks: [{ at: 1, kind: 'command' }, { at: 2, kind: 'command' }], chained: false })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('shell 命令、名字', () => {
|
describe('shell 命令、名字', () => {
|
||||||
const commands = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'shellscript', source, node => adapter.command?.(node))
|
const commands = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'shellscript', source, node => adapter.command?.(node))
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { BlockShape, CodeBlock, CommandFacts, CommandWord, DeclaratorFacts, FunctionFacts, InterpreterFacts, LanguageAdapter, LocalSymbol, Node, OptionsFacts, PositionalParameter, Redirect, SourceRange, SwitchFacts, TextKind, TokenRole } from '../contract'
|
import type { BlockShape, CodeBlock, CommandFacts, CommandWord, DeclaratorFacts, FunctionFacts, InterpreterFacts, LanguageAdapter, LocalSymbol, Node, OptionsFacts, PositionalParameter, Redirect, SourceRange, StepBlocks, SwitchFacts, TextKind, TokenRole } from '../contract'
|
||||||
import type { CommentRules, ListBlocks } from './shared'
|
import type { CommentRules, ListBlocks } from './shared'
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
import { bashNameReader } from './bashNames'
|
import { bashNameReader } from './bashNames'
|
||||||
@@ -461,7 +461,27 @@ function bashCommand(node: Node): CommandFacts | undefined {
|
|||||||
if (!command || !name)
|
if (!command || !name)
|
||||||
return undefined
|
return undefined
|
||||||
const args = command.childrenForFieldName('argument').filter(arg => arg !== null)
|
const args = command.childrenForFieldName('argument').filter(arg => arg !== null)
|
||||||
return { name: commandWord(name), arguments: args.map(commandWord), redirects: redirectsOf(node) }
|
const inherited = node.type === 'command' ? redirectedChain(node) : undefined
|
||||||
|
return { name: commandWord(name), arguments: args.map(commandWord), redirects: [...redirectsOf(node), ...(inherited ? redirectsOf(inherited) : [])] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一串 `a && b` 里的最后一句。 */
|
||||||
|
function lastOperand(list: Node): Node | undefined {
|
||||||
|
return named(list).filter(child => child.type !== 'comment').at(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tree-sitter-bash 把 `a && b > f` 读成整串带着重定向(redirected_statement 的 body 是 `a && b`),其实重定向只属于最后那条命令 b:
|
||||||
|
* command 是这样一串的最后一条时给出那个 redirected_statement,它的重定向算在 command 上。
|
||||||
|
*/
|
||||||
|
function redirectedChain(command: Node): Node | undefined {
|
||||||
|
let child = command
|
||||||
|
let parent = command.parent
|
||||||
|
while (parent?.type === 'list' && lastOperand(parent)?.id === child.id) {
|
||||||
|
child = parent
|
||||||
|
parent = parent.parent
|
||||||
|
}
|
||||||
|
return child.id !== command.id && parent?.type === 'redirected_statement' && parent.childForFieldName('body')?.id === child.id ? parent : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 名字 ────────────────────────────────────────────────────────────────────
|
// ─── 名字 ────────────────────────────────────────────────────────────────────
|
||||||
@@ -569,6 +589,46 @@ function bashTokenRole(leaf: Node): TokenRole {
|
|||||||
return DECLARATION_KEYWORDS.has(leaf.type) ? 'storage' : 'plain'
|
return DECLARATION_KEYWORDS.has(leaf.type) ? 'storage' : 'plain'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 一串 `a && b` 本身(整串带着重定向的,见 redirectedChain,是它 body 里的那串)。 */
|
||||||
|
function chainList(node: Node): Node | undefined {
|
||||||
|
const body = node.type === 'redirected_statement' ? node.childForFieldName('body') : node
|
||||||
|
return body?.type === 'list' ? body : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一串里的各条命令,照源码顺序;整串带着重定向的往里拆(重定向算在最后一条上)。 */
|
||||||
|
function chainOperands(node: Node): Node[] {
|
||||||
|
const list = chainList(node)
|
||||||
|
return list ? named(list).filter(child => child.type !== 'comment').flatMap(chainOperands) : [node]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `a && b && c`:只用 && 连着的一串(都不是条件)。 */
|
||||||
|
function andChain(node: Node): boolean {
|
||||||
|
const operators = (list: Node): string[] => children(list).flatMap((child) => {
|
||||||
|
const inner = chainList(child)
|
||||||
|
return inner ? operators(inner) : child.isNamed ? [] : [child.type]
|
||||||
|
})
|
||||||
|
const list = chainList(node)
|
||||||
|
return !!list && operators(list).every(operator => operator === '&&') && !chainOperands(node).some(isTest)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一步的块:整串带着的重定向算在最后一条上,它的头部延到重定向结尾。 */
|
||||||
|
function stepBlocks(operand: Node): CodeBlock[] {
|
||||||
|
const redirected = operand.type === 'command' ? redirectedChain(operand) : undefined
|
||||||
|
return statementBlocks(operand, true).map(block => redirected ? { ...block, header: { ...block.header, endIndex: redirected.endIndex, endRow: redirected.endPosition.row } } : block)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 被别的语言当一串步骤执行的 shell(Dockerfile 的 RUN):只有一句、是只用 && 连着的一串命令时拆开,一条命令一步(一步失败就停);
|
||||||
|
* 别的照 toBlocks 一句一步。
|
||||||
|
*/
|
||||||
|
function bashSteps(root: Node): StepBlocks {
|
||||||
|
const items = named(root)
|
||||||
|
const [only, ...more] = items.filter(item => item.type !== 'comment')
|
||||||
|
if (only && more.length === 0 && andChain(only))
|
||||||
|
return { blocks: chainOperands(only).flatMap(stepBlocks), chained: true }
|
||||||
|
return { blocks: statements(items, -1, true), chained: false }
|
||||||
|
}
|
||||||
|
|
||||||
/** bash / sh / zsh 脚本(VS Code 的 languageId 是 shellscript)的语言插件。@param wasmDir 语法包 .wasm 所在目录 */
|
/** bash / sh / zsh 脚本(VS Code 的 languageId 是 shellscript)的语言插件。@param wasmDir 语法包 .wasm 所在目录 */
|
||||||
export function createBashAdapter(wasmDir: string): LanguageAdapter {
|
export function createBashAdapter(wasmDir: string): LanguageAdapter {
|
||||||
return {
|
return {
|
||||||
@@ -576,6 +636,7 @@ export function createBashAdapter(wasmDir: string): LanguageAdapter {
|
|||||||
languageIds: ['shellscript'],
|
languageIds: ['shellscript'],
|
||||||
grammarPath: () => path.join(wasmDir, 'tree-sitter-bash.wasm'),
|
grammarPath: () => path.join(wasmDir, 'tree-sitter-bash.wasm'),
|
||||||
toBlocks: root => programBlocks(root),
|
toBlocks: root => programBlocks(root),
|
||||||
|
steps: root => bashSteps(root),
|
||||||
tokenRole: bashTokenRole,
|
tokenRole: bashTokenRole,
|
||||||
command: bashCommand,
|
command: bashCommand,
|
||||||
symbols: root => bashSymbols(root),
|
symbols: root => bashSymbols(root),
|
||||||
|
|||||||
Reference in new issue
Block a user