647 lines
28 KiB
TypeScript
647 lines
28 KiB
TypeScript
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<string, string> = {
|
|
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<number, Node>()
|
|
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<string, LocalSymbol>()
|
|
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,
|
|
}
|
|
}
|