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 * as path from 'node:path' import { bashNameReader } from './bashNames' import { children, compoundShape, leafBlock, leafShape, listBlocks, named } from './shared' /** 按习惯当常量用的名字:全大写(可带数字、下划线)。 */ const CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/ const POSITIONAL = /^[1-9]$/ /** 流程关键字(按关键字着色)和声明关键字(按声明着色)。 */ const FLOW_KEYWORDS = new Set(['if', 'then', 'else', 'elif', 'fi', 'for', 'while', 'until', 'do', 'done', 'case', 'esac', 'in', 'select']) const DECLARATION_KEYWORDS = new Set(['local', 'export', 'readonly', 'declare', 'typeset', 'function', 'unset']) /** 写在 `set` 里的单字母开关 → bash 的长名字(`set -o` 用的那个)。 */ const SHORT_SWITCHES: Record = { a: 'allexport', b: 'notify', e: 'errexit', f: 'noglob', h: 'hashall', k: 'keyword', m: 'monitor', n: 'noexec', t: 'onecmd', u: 'nounset', v: 'verbose', x: 'xtrace', B: 'braceexpand', C: 'noclobber', E: 'errtrace', H: 'histexpand', P: 'physical', T: 'functrace', } /** 打印到输出的内置命令:函数里没重定向的这几句是函数的输出。 */ const PRINTERS = new Set(['echo', 'printf']) const LOOPS = new Set(['for_statement', 'c_style_for_statement', 'while_statement']) /** 分情况里一个情况最多几句、每句一行写完,才整个排成一张表(不拆成子块)。 */ const COMPACT_CASE_STATEMENTS = 3 const SIMPLE_STATEMENTS = new Set(['command', 'redirected_statement', 'variable_assignment', 'declaration_command', 'negated_command']) /** 不出值的展开:命令替换、变量展开、算术展开、进程替换;值里有它们就不是写死的。 */ const EXPANSIONS = new Set(['simple_expansion', 'expansion', 'command_substitution', 'arithmetic_expansion', 'process_substitution']) const COMMENTS: CommentRules = { lineStyle: () => '#', // 紧贴在函数上面一行的注释是函数的说明 isDoc: (comment) => { const next = comment.nextSibling return next?.type === 'function_definition' && next.startPosition.row === comment.endPosition.row + 1 }, documents: new Set(['function']), } function statements(items: Node[], afterRow: number, top: boolean): CodeBlock[] { return listBlocks(items, afterRow, item => statementBlocks(item, top), COMMENTS) } const nested: ListBlocks = (items, afterRow) => statements(items, afterRow, false) /** 节点里(不进嵌套的函数定义)满足 test 的节点,先序。 */ function descendants(node: Node, test: (node: Node) => boolean, out: Node[] = []): Node[] { for (const child of children(node)) { if (test(child)) out.push(child) if (child.type !== 'function_definition') descendants(child, test, out) } return out } function commandName(command: Node | null | undefined): string | undefined { return command?.type === 'command' ? command.childForFieldName('name')?.text : undefined } /** 语句本身执行的那条命令:`cmd`,或带重定向的 `cmd >&2` 里的 cmd。 */ function commandOf(statement: Node): Node | undefined { if (statement.type === 'command') return statement const body = statement.type === 'redirected_statement' ? statement.childForFieldName('body') : null return body?.type === 'command' ? body : undefined } // ─── 块 ────────────────────────────────────────────────────────────────────── function statementBlocks(node: Node, top: boolean): CodeBlock[] { switch (node.type) { case 'variable_assignment': return [variableBlock(node, [node], top, false)] case 'variable_assignments': return [variableBlock(node, named(node).filter(child => child.type === 'variable_assignment'), top, false)] case 'declaration_command': return [declarationBlock(node, top)] case 'function_definition': return [functionBlock(node)] case 'if_statement': return ifBlocks(node) case 'for_statement': case 'c_style_for_statement': case 'while_statement': return [loopBlock('loop', node, node)] case 'case_statement': return [caseBlock(node)] case 'pipeline': return [pipelineBlock(node)] case 'redirected_statement': { const body = node.childForFieldName('body') return body && body.type !== 'command' ? statementBlocks(body, top) : [commandBlock(node)] } case 'command': return [commandBlock(node)] case 'negated_command': case 'test_command': return [leafBlock(isTest(node) ? 'if' : 'command', node)] case 'list': return [leafBlock(conditionList(node) ? 'if' : 'command', node)] default: return [leafBlock('statement', node)] } } /** 值里没有展开(变量、命令替换……),是写死的。 */ function isLiteral(value: Node): boolean { return !EXPANSIONS.has(value.type) && named(value).every(isLiteral) } /** 常量:readonly 的,或脚本顶层、名字全大写、值写死的。 */ function variableBlock(node: Node, assignments: Node[], top: boolean, readonly: boolean): CodeBlock { const declarators = assignments.flatMap((assignment): DeclaratorFacts[] => { if (assignment.type !== 'variable_assignment') return [{ name: assignment }] const name = assignment.childForFieldName('name') const value = assignment.childForFieldName('value') return name ? [{ name, ...(value && { value }) }] : [] }) const constant = readonly || (top && declarators.length > 0 && declarators.every(({ name, value }) => CONSTANT_NAME.test(name.text) && value && isLiteral(value))) return { kind: constant ? 'const' : 'variable', ...leafShape(node), facts: { declarators } } } /** local / export / readonly / declare:声明的变量是一块;什么都没声明(`export -f fn`)的当命令。 */ function declarationBlock(node: Node, top: boolean): CodeBlock { const keyword = node.child(0)?.type const declared = named(node).filter(child => child.type === 'variable_assignment' || child.type === 'variable_name') if (declared.length === 0) return leafBlock('command', node) const readonly = keyword === 'readonly' || named(node).some(child => child.type === 'word' && /^-\w*r/.test(child.text)) return variableBlock(node, declared, top, readonly) } function commandBlock(node: Node): CodeBlock { const command = commandOf(node) const name = commandName(command) if (name === 'set' && command && node.type === 'command') { const switches = optionSwitches(named(command).filter(child => child.type !== 'command_name')) if (switches) return { kind: 'options', ...leafShape(node), facts: { switches } } } return leafBlock(name === 'return' ? 'return' : 'command', node) } /** `set -euo pipefail`、`set +x`、`set -o nounset` 的各个开关;有不是开关的词(`set -- a b`)时不是。 */ function optionSwitches(words: Node[]): OptionsFacts['switches'] | undefined { const switches: SwitchFacts[] = [] for (let i = 0; i < words.length; i++) { const text = words[i].text const match = /^([-+])([a-z]+)$/i.exec(text) if (!match) return undefined const on = match[1] === '-' for (const letter of match[2]) { if (letter === 'o') { const name = words[++i]?.text if (!name || !/^[a-z]+$/.test(name)) return undefined switches.push({ id: name, on, flag: `${match[1]}o ${name}` }) continue } switches.push({ id: SHORT_SWITCHES[letter] ?? letter, on, flag: `${match[1]}${letter}` }) } } return switches.length > 0 ? switches : undefined } /** 条件:`[ … ]`、`[[ … ]]`,前面可以带 `!`。 */ function isTest(node: Node): boolean { if (node.type === 'negated_command') return named(node).some(isTest) return node.type === 'test_command' } /** `a && b || c` 里用 && / || 连起来的各句,照源码顺序。 */ function listOperands(node: Node): Node[] { return node.type === 'list' ? named(node).filter(child => child.type !== 'comment').flatMap(listOperands) : [node] } /** * 当条件读的一串:全是条件(`[ -f x ] && [ … ]`,读成「成立:…」),或前面全是条件、最后跟一句(`[ … ] || continue`、 * `[ -f x ] && rm x`,读成「如果(不是)〔…〕,…」)。 */ function conditionList(node: Node): boolean { const operands = listOperands(node) return operands.slice(0, -1).every(isTest) && (isTest(operands[operands.length - 1]) || operands.length > 1) } /** 语句块(`{ … }`、do … done、then 后面)里的各句连同注释:去掉关键字、分号。 */ function bodyItems(nodes: Node[]): Node[] { return nodes.filter(child => child.isNamed) } /** 复合语句块:头部到第一句之前(没有句子时到 fromEnd 为止),子块是各句。 */ function compound(kind: TextKind, outer: Node, items: Node[], fromEnd: number): CodeBlock { const shape: BlockShape = compoundShape(outer, items[0]?.startIndex ?? fromEnd, items, nested) return { kind, ...shape } } /** 循环:头部到 do 为止,子块是 do … done 里的各句。管道后面跟的 while 也是这样拆,只是块本身是整个管道。 */ function loopBlock(kind: TextKind, outer: Node, loop: Node): CodeBlock { const body = loop.childForFieldName('body') const items = body ? bodyItems(children(body)) : [] const open = body?.child(0) return compound(kind, outer, items, open?.endIndex ?? loop.endIndex) } /** if 一块,elif / else 展平成紧跟它的兄弟块(同 Python)。 */ function ifBlocks(node: Node): CodeBlock[] { const clauses = named(node).filter(child => child.type === 'elif_clause' || child.type === 'else_clause') return [clauseBlock('if', node), ...clauses.map(clause => clauseBlock('else', clause))] } /** if、elif 的主体是 then 后面、下一个 elif / else 之前的各句;else 的主体是 else 后面的各句。 */ function clauseBlock(kind: TextKind, node: Node): CodeBlock { const all = children(node) const opener = all.findIndex(child => child.type === (node.type === 'else_clause' ? 'else' : 'then')) const rest = all.slice(opener + 1) const end = rest.findIndex(child => child.type === 'elif_clause' || child.type === 'else_clause' || child.type === 'fi') const items = bodyItems(end < 0 ? rest : rest.slice(0, end)) return compound(kind, node, items, all[opener]?.endIndex ?? node.endIndex) } function caseItems(node: Node): Node[] { return named(node).filter(child => child.type === 'case_item') } /** 一个情况里要做的各句(连同注释):`)` 后面的、`;;` 之前的。 */ function caseBody(item: Node): Node[] { const all = children(item) const close = all.findIndex(child => child.type === ')') return bodyItems(all.slice(close + 1)) } /** 每个情况都只有几句、每句一行写完、没有注释和嵌套的语句块。 */ function compactCase(node: Node): boolean { return caseItems(node).every((item) => { const body = caseBody(item) return body.length <= COMPACT_CASE_STATEMENTS && body.every(statement => SIMPLE_STATEMENTS.has(statement.type) && statement.startPosition.row === statement.endPosition.row && (statement.type !== 'redirected_statement' || commandOf(statement) !== undefined)) }) } /** 分情况:几个情况都短时整条语句一块(情况排成表),否则头部到 in 为止、每个情况一块子块。 */ function caseBlock(node: Node): CodeBlock { if (compactCase(node)) return leafBlock('case', node) const items = caseItems(node) const inKeyword = children(node).find(child => child.type === 'in') const shape = compoundShape(node, items[0]?.startIndex ?? inKeyword?.endIndex ?? node.endIndex, [], nested) return { kind: 'case', ...shape, children: [...shape.children, ...items.map(branchBlock)] } } /** 一个情况:头部是它的模式(到 `)` 为止),子块是要做的各句。 */ function branchBlock(item: Node): CodeBlock { const body = caseBody(item) const close = children(item).find(child => child.type === ')') return compound('case', item, body, close?.endIndex ?? item.endIndex) } /** 管道:最后一段是循环(`find … | while read -r x; do … done`)时循环的主体是子块。 */ function pipelineBlock(node: Node): CodeBlock { const last = named(node).at(-1) if (last && LOOPS.has(last.type)) return loopBlock('pipeline', node, last) return leafBlock('pipeline', node) } // ─── 函数 ──────────────────────────────────────────────────────────────────── /** `$1`、`"$1"`、`${1}` 这种只是取第几个参数的值:取的是第几个。 */ function positionalValue(value: Node | null): number | undefined { if (!value) return undefined const inner = value.type === 'string' && named(value).length === 1 ? named(value)[0] : value if (inner.type !== 'simple_expansion' && inner.type !== 'expansion') return undefined const expanded = named(inner) const name = expanded.length === 1 ? expanded[0] : undefined return name?.type === 'variable_name' && POSITIONAL.test(name.text) ? Number(name.text) : undefined } /** 函数里用到的第几个参数,和它们在函数里的名字(`local url="$1"`、`url=$1`)。 */ function positionalParameters(body: Node): PositionalParameter[] { const used = new Set(descendants(body, node => node.type === 'variable_name' && POSITIONAL.test(node.text) && (node.parent?.type === 'simple_expansion' || node.parent?.type === 'expansion')).map(node => Number(node.text))) const names = new Map() for (const assignment of descendants(body, node => node.type === 'variable_assignment')) { const position = positionalValue(assignment.childForFieldName('value')) const name = assignment.childForFieldName('name') if (position !== undefined && name && !names.has(position)) names.set(position, name) } return [...used].sort((a, b) => a - b).map(position => ({ position, ...(names.has(position) && { name: names.get(position) }) })) } /** 重定向了输出(`>&2`、`> file`)的语句不算打印到输出。 */ function printsToOutput(statement: Node): boolean { const name = commandName(commandOf(statement)) if (!name || !PRINTERS.has(name)) return false return redirectsOf(statement).every(redirect => redirect.kind === 'read' || redirect.stream === 'error') } /** 函数自己的各句(进 if、循环、分情况,不进命令替换、管道、嵌套的函数)里打印到输出的那几句。 */ function printedStatements(body: Node): Node[] { const out: Node[] = [] const visit = (node: Node): void => { for (const child of named(node)) { if (child.type === 'command' || child.type === 'redirected_statement') { if (printsToOutput(child)) out.push(child) } else if (child.type !== 'function_definition' && child.type !== 'pipeline' && child.type !== 'command_substitution') { visit(child) } } } visit(body) return out } /** 同一文件里有没有 `$(name …)`:函数的输出被拿去用了。 */ function captured(root: Node, name: string): boolean { return descendants(root, node => node.type === 'command_substitution' && named(node).some(inner => commandName(inner) === name || commandName(listOperands(inner)[0]) === name)).length > 0 } function functionBlock(node: Node): CodeBlock { const name = node.childForFieldName('name') const body = node.childForFieldName('body') const items = body?.type === 'compound_statement' ? bodyItems(children(body)) : [] const open = body?.child(0) const shape = compoundShape(node, items[0]?.startIndex ?? open?.endIndex ?? node.endIndex, items, nested) const facts: FunctionFacts = { ...(name && { name }), decorators: [], parameters: [], positional: body ? positionalParameters(body) : [], prints: body ? printedStatements(body) : [], captured: !!name && captured(node.tree.rootNode, name.text), returns: body ? descendants(body, child => commandName(child) === 'return') : [], } return { kind: 'function', ...shape, facts } } // ─── 解释器 ────────────────────────────────────────────────────────────────── /** `#!/usr/bin/env bash`、`#!/bin/sh -e`。 */ function interpreterFacts(shebang: Node): InterpreterFacts { const command = shebang.text.slice(2).trim() const [program = '', ...rest] = command.split(/\s+/) if (path.posix.basename(program) === 'env') { const named = rest.find(word => !word.startsWith('-')) return { program: named ? path.posix.basename(named) : 'env', lookup: 'env', command } } return { program: path.posix.basename(program), lookup: 'path', command } } function programBlocks(root: Node): CodeBlock[] { const items = named(root) const [first] = items if (first?.type === 'comment' && first.startPosition.row === 0 && first.text.startsWith('#!')) { const interpreter: CodeBlock = { kind: 'interpreter', ...leafShape(first), facts: interpreterFacts(first) } return [interpreter, ...statements(items.slice(1), 0, true)] } return statements(items, -1, true) } // ─── 命令 ──────────────────────────────────────────────────────────────────── /** 一个 `>`、`>>`、`>&2`、`2>&1`、`&>`、`<` 重定向;heredoc 这类不算。 */ function redirectOf(node: Node): Redirect | undefined { if (node.type !== 'file_redirect') return undefined const descriptor = named(node).find(child => child.type === 'file_descriptor')?.text const operator = children(node).find(child => !child.isNamed)?.type const target = node.childForFieldName('destination') if (!operator || !target) return undefined const stream = descriptor === '2' ? 'error' : 'output' switch (operator) { case '>&': if (target.text === '1' || target.text === '2') return { kind: 'merge', stream, into: target.text === '2' ? 'error' : 'output' } return { kind: 'write', stream, target } case '>': case '>|': return { kind: 'write', stream, target } case '>>': return { kind: 'append', stream, target } case '&>': return { kind: 'write', stream: 'both', target } case '&>>': return { kind: 'append', stream: 'both', target } case '<': return { kind: 'read', source: target } default: return undefined } } function redirectsOf(statement: Node): Redirect[] { if (statement.type !== 'redirected_statement') return [] return statement.childrenForFieldName('redirect').flatMap((redirect) => { const found = redirect ? redirectOf(redirect) : undefined return found ? [found] : [] }) } /** 一个词写死的文字:去掉引号、反斜杠转义;里面有展开的没有。 */ function literalOf(word: Node): string | undefined { switch (word.type) { case 'word': case 'number': case 'extglob_pattern': return word.text.replace(/\\(.)/g, '$1') case 'raw_string': return word.text.slice(1, -1) case 'command_name': { const [inner] = named(word) return inner ? literalOf(inner) : undefined } case 'string': { const parts = named(word) return parts.every(part => part.type === 'string_content') ? parts.map(part => part.text.replace(/\\([$`"\\])/g, '$1')).join('') : undefined } case 'concatenation': { const parts = named(word).map(literalOf) return parts.every(part => part !== undefined) ? parts.join('') : undefined } default: return undefined } } function commandWord(node: Node): CommandWord { const literal = literalOf(node) return literal === undefined ? { node } : { node, literal } } /** 一条命令(带不带重定向):命令名、后面的各个词(连写死的文字)、重定向。 */ function bashCommand(node: Node): CommandFacts | undefined { const command = commandOf(node) const name = command?.childForFieldName('name') if (!command || !name) return undefined const args = command.childrenForFieldName('argument').filter(arg => arg !== null) 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 } // ─── 名字 ──────────────────────────────────────────────────────────────────── function rangeOf(node: Node): SourceRange { return { startIndex: node.startIndex, endIndex: node.endIndex } } /** * 同一文件里认得出的名字:函数(定义处是函数名,用到的是同名的命令),变量(定义处是赋值、local / export 声明、for 的变量、 * read 读进的名字,用到的是 `$x`、`${x…}` 和这些定义处)。第几个参数、`$?` 这类不算。 */ function bashSymbols(root: Node): LocalSymbol[] { const symbols = new Map() const symbol = (key: string): LocalSymbol => { let found = symbols.get(key) if (!found) { found = { definitions: [], occurrences: [] } symbols.set(key, found) } return found } const define = (key: string, node: Node): void => { const entry = symbol(key) entry.definitions.push(rangeOf(node)) entry.occurrences.push(rangeOf(node)) } const functions = new Set(descendants(root, node => node.type === 'function_definition').map(fn => fn.childForFieldName('name')?.text)) const visit = (node: Node): void => { const parent = node.parent if (node.type === 'function_definition') { const name = node.childForFieldName('name') if (name) define(`function:${name.text}`, name) } else if (node.type === 'word' && parent?.type === 'command_name' && functions.has(node.text)) { symbol(`function:${node.text}`).occurrences.push(rangeOf(node)) } else if (node.type === 'variable_name' && !POSITIONAL.test(node.text)) { const defining = parent?.type === 'variable_assignment' || parent?.type === 'declaration_command' || parent?.type === 'for_statement' if (defining) define(`variable:${node.text}`, node) else if (parent?.type === 'simple_expansion' || parent?.type === 'expansion') symbol(`variable:${node.text}`).occurrences.push(rangeOf(node)) } else if (node.type === 'command' && commandName(node) === 'read') { for (const arg of node.childrenForFieldName('argument')) { if (arg?.type === 'word' && /^[a-z_]\w*$/i.test(arg.text)) define(`variable:${arg.text}`, arg) } } for (const child of children(node)) visit(child) } visit(root) return [...symbols.values()].filter(entry => entry.definitions.length > 0) } // ─── 词性 ──────────────────────────────────────────────────────────────────── function insideString(leaf: Node): boolean { for (let node = leaf.parent; node; node = node.parent) { if (EXPANSIONS.has(node.type)) return false if (node.type === 'string' || node.type === 'heredoc_body') return true } return false } function bashTokenRole(leaf: Node): TokenRole { const parent = leaf.parent switch (leaf.type) { case 'comment': return 'comment' case 'string_content': case 'raw_string': case 'ansi_c_string': case 'heredoc_content': case 'heredoc_body': case 'extglob_pattern': case 'regex': return 'string' case 'number': return 'number' case 'variable_name': return CONSTANT_NAME.test(leaf.text) ? 'constant' : 'variable' case 'special_variable_name': return 'variable' case 'test_operator': return 'operator' case 'file_descriptor': return 'number' case 'word': if (parent?.type === 'command_name' || parent?.type === 'function_definition') return 'function' if (insideString(leaf) || parent?.type === 'case_item') return 'string' return /^\d+(?:\.\d+)*$/.test(leaf.text) ? 'number' : 'plain' case '"': return 'string' } if (FLOW_KEYWORDS.has(leaf.type)) return 'keyword' 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 所在目录 */ export function createBashAdapter(wasmDir: string): LanguageAdapter { return { id: 'bash', languageIds: ['shellscript'], grammarPath: () => path.join(wasmDir, 'tree-sitter-bash.wasm'), toBlocks: root => programBlocks(root), steps: root => bashSteps(root), tokenRole: bashTokenRole, command: bashCommand, symbols: root => bashSymbols(root), displayNames: bashNameReader, } }