From 0eb381a6facd5eb7af12ec89917a406b81916453 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 20 Nov 2025 12:41:54 +0000 Subject: [PATCH] feat: add bash tree-sitter support - Add bash query patterns for parsing bash scripts - Add bash language support to languageParser.ts for .sh and .bash extensions - Create comprehensive test suite for bash parsing - Add sample bash fixture with various language constructs Fixes #9430 --- .../__tests__/fixtures/sample-bash.ts | 143 ++++++++++++++++++ .../tree-sitter/__tests__/inspectBash.spec.ts | 51 +++++++ .../parseSourceCodeDefinitions.bash.spec.ts | 102 +++++++++++++ src/services/tree-sitter/languageParser.ts | 6 + src/services/tree-sitter/queries/bash.ts | 84 ++++++++++ src/services/tree-sitter/queries/index.ts | 1 + 6 files changed, 387 insertions(+) create mode 100644 src/services/tree-sitter/__tests__/fixtures/sample-bash.ts create mode 100644 src/services/tree-sitter/__tests__/inspectBash.spec.ts create mode 100644 src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.bash.spec.ts create mode 100644 src/services/tree-sitter/queries/bash.ts diff --git a/src/services/tree-sitter/__tests__/fixtures/sample-bash.ts b/src/services/tree-sitter/__tests__/fixtures/sample-bash.ts new file mode 100644 index 0000000000..bb1b678fd0 --- /dev/null +++ b/src/services/tree-sitter/__tests__/fixtures/sample-bash.ts @@ -0,0 +1,143 @@ +export const sampleBashContent = ` +#!/bin/bash + +# Function definition - demonstrates basic function structure +function multi_line_function() { + local param1=\$1 + local param2=\$2 + echo "Processing \$param1 and \$param2" + return 0 +} + +# Alternative function syntax +another_function() { + local result="" + for i in {1..5}; do + result+="\$i " + done + echo "\$result" +} + +# Variable assignments and exports +GLOBAL_VAR="global_value" +export PATH_VAR="/usr/local/bin:\$PATH" +readonly CONSTANT_VAR="immutable" +declare -a array_var=( + "element1" + "element2" + "element3" +) + +# Alias definitions +alias ll='ls -la' +alias grep='grep --color=auto' +alias ..='cd ..' + +# Complex variable assignment with command substitution +CURRENT_DIR=\$( + pwd | + sed 's/\\/home\\///' | + tr '/' '-' +) + +# Here document example +cat < output.txt + This is a multi-line + here document that spans + several lines for testing +EOF + +# Case statement with multiple patterns +case "\$1" in + start|START) + echo "Starting service..." + start_service + ;; + stop|STOP) + echo "Stopping service..." + stop_service + ;; + restart|RESTART) + echo "Restarting service..." + restart_service + ;; + *) + echo "Unknown command" + exit 1 + ;; +esac + +# If statement with multiple conditions +if [[ -f "\$CONFIG_FILE" && -r "\$CONFIG_FILE" ]]; then + source "\$CONFIG_FILE" + echo "Configuration loaded" +elif [[ -f "\$DEFAULT_CONFIG" ]]; then + source "\$DEFAULT_CONFIG" + echo "Default configuration loaded" +else + echo "No configuration found" + exit 1 +fi + +# While loop with read +while IFS= read -r line; do + process_line "\$line" + counter=\$((counter + 1)) +done < input.txt + +# For loop with array iteration +for element in "\${array_var[@]}"; do + echo "Processing: \$element" + transform_element "\$element" +done + +# Pipeline example +cat data.txt | + grep "pattern" | + sort -u | + head -20 > results.txt + +# Function with arithmetic operations +calculate_sum() { + local sum=0 + for num in "\$@"; do + sum=\$((sum + num)) + done + echo \$sum +} + +# Source another script +source ./config.sh +. ./utils.sh + +# Array manipulation +declare -A associative_array +associative_array["key1"]="value1" +associative_array["key2"]="value2" + +# Test command examples +if [ -z "\$VAR" ]; then + echo "Variable is empty" +fi + +if [[ "\$VAR" =~ ^[0-9]+\$ ]]; then + echo "Variable is numeric" +fi + +# Command substitution in arithmetic context +result=\$((10 + \$(get_value))) + +# Redirection examples +exec 3< input.txt +exec 4> output.txt +exec 5>&1 + +# Trap signal handling +trap cleanup EXIT +trap 'echo "Interrupted"' INT TERM +` + +export default { + path: "test.sh", + content: sampleBashContent, +} diff --git a/src/services/tree-sitter/__tests__/inspectBash.spec.ts b/src/services/tree-sitter/__tests__/inspectBash.spec.ts new file mode 100644 index 0000000000..1dd6c2cfc0 --- /dev/null +++ b/src/services/tree-sitter/__tests__/inspectBash.spec.ts @@ -0,0 +1,51 @@ +import { inspectTreeStructure, debugLog } from "./helpers" +import { sampleBashContent } from "./fixtures/sample-bash" + +describe("Inspect Bash", () => { + it("should capture Bash-specific constructs", async () => { + const result = await inspectTreeStructure(sampleBashContent, "bash") + debugLog("Bash Inspect Result:", result) + + // Check for function definitions + expect(result).toContain("function_definition") + + // Check for variable assignments + expect(result).toContain("variable_assignment") + + // Check for command structures + expect(result).toContain("command") + + // Check for control flow structures + expect(result).toContain("case_statement") + expect(result).toContain("if_statement") + expect(result).toContain("while_statement") + expect(result).toContain("for_statement") + + // Check for test commands + expect(result).toContain("test_command") + + // Check for arithmetic operations + expect(result).toContain("arithmetic_expansion") + + // Check for here documents + expect(result).toContain("heredoc_redirect") + + // Check for pipelines + expect(result).toContain("pipeline") + + // Check for redirections + expect(result).toContain("redirect") + + // Check for arrays + expect(result).toContain("array") + + // Check for command substitution + expect(result).toContain("command_substitution") + + // Check for program structure + expect(result).toContain("program") + + // The shebang appears as a comment in the tree + expect(result).toContain("comment") + }) +}) diff --git a/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.bash.spec.ts b/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.bash.spec.ts new file mode 100644 index 0000000000..478901c65f --- /dev/null +++ b/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.bash.spec.ts @@ -0,0 +1,102 @@ +/* +TODO: The following structures can be parsed by tree-sitter but lack query support: + +1. Process Substitution: + (process_substitution) + Example: diff <(command1) <(command2) + +2. Coprocess: + (coproc_statement) + Example: coproc NAME { command; } + +3. Compound Commands: + (compound_statement) + Example: { command1; command2; } + +4. Select Loops: + (select_statement) + Example: select item in list; do ...; done + +5. Extended Glob Patterns: + (extglob_pattern) + Example: !(pattern), ?(pattern), *(pattern), +(pattern), @(pattern) +*/ + +import { testParseSourceCodeDefinitions, debugLog } from "./helpers" +import { sampleBashContent } from "./fixtures/sample-bash" +import { bashQuery } from "../queries" + +// Bash test options +const bashOptions = { + language: "bash", + wasmFile: "tree-sitter-bash.wasm", + queryString: bashQuery, + extKey: "sh", +} + +describe("parseSourceCodeDefinitionsForFile with Bash", () => { + let parseResult: string | undefined + + beforeAll(async () => { + // Cache parse result for all tests + parseResult = await testParseSourceCodeDefinitions("test.sh", sampleBashContent, bashOptions) + debugLog("Bash Parse Result:", parseResult) + }) + + it("should parse function definitions", () => { + expect(parseResult).toMatch(/\d+--\d+ \| function multi_line_function\(\)/) + expect(parseResult).toMatch(/\d+--\d+ \| another_function\(\)/) + expect(parseResult).toMatch(/\d+--\d+ \| calculate_sum\(\)/) + debugLog("Function definitions found:", parseResult) + }) + + it("should parse variable declarations and exports", () => { + expect(parseResult).toMatch(/\d+--\d+ \| GLOBAL_VAR="global_value"/) + expect(parseResult).toMatch(/\d+--\d+ \| export PATH_VAR/) + expect(parseResult).toMatch(/\d+--\d+ \| readonly CONSTANT_VAR/) + expect(parseResult).toMatch(/\d+--\d+ \| declare -a array_var/) + debugLog("Variable declarations found:", parseResult) + }) + + it("should parse alias definitions", () => { + expect(parseResult).toMatch(/\d+--\d+ \| alias ll='ls -la'/) + expect(parseResult).toMatch(/\d+--\d+ \| alias grep='grep --color=auto'/) + debugLog("Alias definitions found:", parseResult) + }) + + it("should parse control structures", () => { + expect(parseResult).toMatch(/\d+--\d+ \| case "\$1" in/) + expect(parseResult).toMatch(/\d+--\d+ \| if \[\[ -f "\$CONFIG_FILE"/) + expect(parseResult).toMatch(/\d+--\d+ \| while IFS= read/) + expect(parseResult).toMatch(/\d+--\d+ \| for element in/) + debugLog("Control structures found:", parseResult) + }) + + it("should parse here documents and redirections", () => { + expect(parseResult).toMatch(/\d+--\d+ \| cat < { + expect(parseResult).toMatch(/\d+--\d+ \| source \.\/config.sh/) + expect(parseResult).toMatch(/\d+--\d+ \| \. \.\/utils.sh/) + debugLog("Source commands found:", parseResult) + }) + + it("should parse test commands", () => { + expect(parseResult).toMatch(/\d+--\d+ \| if \[ -z "\$VAR" \]/) + expect(parseResult).toMatch(/\d+--\d+ \| if \[\[ "\$VAR" =~ \^/) + debugLog("Test commands found:", parseResult) + }) + + it("should parse arithmetic operations", () => { + expect(parseResult).toMatch(/\d+--\d+ \|.*\$\(\(/) + debugLog("Arithmetic operations found:", parseResult) + }) + + it("should parse shebang", () => { + expect(parseResult).toMatch(/\d+--\d+ \| #!\/bin\/bash/) + debugLog("Shebang found:", parseResult) + }) +}) diff --git a/src/services/tree-sitter/languageParser.ts b/src/services/tree-sitter/languageParser.ts index a8ac0a9ead..d56a656bb9 100644 --- a/src/services/tree-sitter/languageParser.ts +++ b/src/services/tree-sitter/languageParser.ts @@ -28,6 +28,7 @@ import { embeddedTemplateQuery, elispQuery, elixirQuery, + bashQuery, } from "./queries" export interface LanguageParser { @@ -218,6 +219,11 @@ export async function loadRequiredLanguageParsers(filesToParse: string[], source language = await loadLanguage("elixir", sourceDirectory) query = new Query(language, elixirQuery) break + case "sh": + case "bash": + language = await loadLanguage("bash", sourceDirectory) + query = new Query(language, bashQuery) + break default: throw new Error(`Unsupported language: ${ext}`) } diff --git a/src/services/tree-sitter/queries/bash.ts b/src/services/tree-sitter/queries/bash.ts new file mode 100644 index 0000000000..229ad53bab --- /dev/null +++ b/src/services/tree-sitter/queries/bash.ts @@ -0,0 +1,84 @@ +/* +Bash Tree-sitter Query Patterns +*/ +export default ` +; Function definitions +(function_definition + name: (word) @name.definition.function) @definition.function + +; Variable declarations and assignments +(variable_assignment + name: (variable_name) @name.definition.variable) @definition.variable + +; Export statements +(declaration_command + name: (simple_expansion + (variable_name) @name.definition.export)) @definition.export + +; Alias definitions +(declaration_command + name: "alias" + value: (concatenation + (word) @name.definition.alias)) @definition.alias + +(declaration_command + name: "alias" + value: (word) @name.definition.alias) @definition.alias + +; Source/dot commands (file includes) +(command + name: (command_name (word) @source_cmd (#match? @source_cmd "^(source|\\.)$")) + argument: (_) @name.definition.source) @definition.source + +; Here documents +(redirected_statement + body: (command) + redirect: (heredoc_redirect + (heredoc_start) @name.definition.heredoc)) @definition.heredoc + +; Case statements +(case_statement + value: (_) @name.definition.case) @definition.case + +; If statements +(if_statement) @definition.if_statement + +; While loops +(while_statement) @definition.while_loop + +; For loops +(for_statement + variable: (variable_name) @name.definition.for_variable) @definition.for_loop + +; Array declarations +(variable_assignment + name: (variable_name) @name.definition.array + value: (array)) @definition.array + +; Command substitutions +(command_substitution) @definition.command_substitution + +; Pipeline commands +(pipeline) @definition.pipeline + +; Redirections +(command + redirect: (_)) @definition.redirection + +; Test commands ([ ] and [[ ]]) +(test_command) @definition.test_command + +; Arithmetic expressions +(arithmetic_expansion) @definition.arithmetic + +; Parameter expansions +(expansion + (variable_name) @name.reference.variable) @reference.variable + +; Comments (for documentation purposes) +(comment) @comment + +; Shebang +(program + . (comment) @shebang (#match? @shebang "^#!/")) +` diff --git a/src/services/tree-sitter/queries/index.ts b/src/services/tree-sitter/queries/index.ts index de9b9cafa3..1fca5aec02 100644 --- a/src/services/tree-sitter/queries/index.ts +++ b/src/services/tree-sitter/queries/index.ts @@ -26,3 +26,4 @@ export { zigQuery } from "./zig" export { default as embeddedTemplateQuery } from "./embedded_template" export { elispQuery } from "./elisp" export { scalaQuery } from "./scala" +export { default as bashQuery } from "./bash"