From 26e822d9e0202e85f657811aa00bda96cc143c33 Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Fri, 10 Jul 2026 19:55:51 +0200 Subject: [PATCH 1/3] feat(analyse): add Bash language support (#48) Add `bash` as a supported comment-type for marker extraction, discovering `.sh`, `.bash`, `.zsh` and `.ksh` files. Bash has only `#` line comments, so it reuses the existing hash-comment extraction path (BASH_QUERY + `function_definition` scope nodes) via a tree-sitter-bash grammar. A `default_oneliner_bash` case is added to the shared `oneline.yaml` extraction fixture with its captured snapshot, and the feature is documented under `FE_BASH`. Fish shell is intentionally excluded: no tree-sitter-fish grammar is published on PyPI, so it cannot be wired into this Python package. Closes #48 --- docs/source/components/analyse.rst | 2 +- docs/source/components/configuration.rst | 6 +++- docs/source/components/features.rst | 32 +++++++++++++++++++ docs/source/development/change_log.rst | 13 ++++++++ pyproject.toml | 1 + src/sphinx_codelinks/analyse/utils.py | 11 ++++++- .../source_discover/config.py | 8 +++++ ...ixture[oneline-default_oneliner_bash].json | 19 +++++++++++ tests/data/extraction/README.md | 2 +- tests/data/extraction/oneline.yaml | 7 ++++ tests/test_extraction_fixtures.py | 1 + tests/test_source_discover.py | 3 +- tests/test_src_trace.py | 2 +- 13 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 tests/__snapshots__/test_extraction_fixtures/test_extraction_fixture[oneline-default_oneliner_bash].json diff --git a/docs/source/components/analyse.rst b/docs/source/components/analyse.rst index ce6d997..0f411ed 100644 --- a/docs/source/components/analyse.rst +++ b/docs/source/components/analyse.rst @@ -47,7 +47,7 @@ Limitations **Current Limitations:** -- **Language Support**: C/C++ (``//``, ``/* */``), C# (``//``, ``/* */``, ``///``), Python (``#``), YAML (``#``), Rust (``//``, ``/* */``, ``///``), Go (``//``, ``/* */``) and JSONC (``//``, ``/* */``) comment styles are supported +- **Language Support**: C/C++ (``//``, ``/* */``), C# (``//``, ``/* */``, ``///``), Python (``#``), YAML (``#``), Rust (``//``, ``/* */``, ``///``), Go (``//``, ``/* */``), JSONC (``//``, ``/* */``) and Bash (``#``) comment styles are supported - **Single Comment Style**: Each analysis run processes only one comment style at a time Extraction Examples diff --git a/docs/source/components/configuration.rst b/docs/source/components/configuration.rst index 76d9401..81da585 100644 --- a/docs/source/components/configuration.rst +++ b/docs/source/components/configuration.rst @@ -271,7 +271,7 @@ Specifies the comment syntax style used in the source code files. This determine **Type:** ``str`` **Default:** ``"cpp"`` -**Supported values:** ``"cpp"``, ``"python"``, ``"cs"``, ``"yaml"``, ``"rust"``, ``"go"``, ``"jsonc"`` +**Supported values:** ``"cpp"``, ``"python"``, ``"cs"``, ``"yaml"``, ``"rust"``, ``"go"``, ``"jsonc"``, ``"bash"`` .. code-block:: toml @@ -325,6 +325,10 @@ Specifies the comment syntax style used in the source code files. This determine - ``//`` (single-line), ``/* */`` (multi-line) - ``.jsonc`` (always); ``.json`` only when the file opens with a comment + * - Bash / POSIX shell + - ``"bash"`` + - ``#`` (single-line) + - ``.sh``, ``.bash``, ``.zsh``, ``.ksh`` (e.g. the mode line ``// -*- mode: jsonc -*-``) .. note:: Future versions may support additional programming languages. diff --git a/docs/source/components/features.rst b/docs/source/components/features.rst index c513ba1..601b975 100644 --- a/docs/source/components/features.rst +++ b/docs/source/components/features.rst @@ -234,6 +234,38 @@ Features .. fault:: Sphinx-codelinks hallucinates traceability objects in JSONC :id: FAULT_JSONC_2 +.. feature:: Bash Language Support + :id: FE_BASH + + Support for defining traceability objects in Bash and POSIX-shell scripts. + + The Bash language parser leverages tree-sitter to identify and extract + single-line (``#``) comments from shell scripts, associating each marker with + the surrounding function definition it annotates. + + ``.sh``, ``.bash``, ``.zsh``, and ``.ksh`` files are auto-discovered when + ``comment_type = "bash"``. zsh and ksh share Bash's ``#`` comment syntax and + are parsed with the same grammar. + + Key capabilities: + + * Hash-style comment (``#``) detection + * Association of comments with function definitions + * Support for standard shell comment conventions + * Shebang lines (``#!/bin/bash``) never produce spurious markers + + .. note:: + + Fish shell is not supported. Fish is not POSIX-compatible and no + ``tree-sitter-fish`` grammar is published on PyPI, so it cannot be wired + into the Python package. + + .. fault:: Traceability objects are not detected in Bash language + :id: FAULT_BASH_1 + + .. fault:: Sphinx-codelinks hallucinates traceability objects in Bash + :id: FAULT_BASH_2 + .. feature:: Customized comment styles :id: FE_CMT diff --git a/docs/source/development/change_log.rst b/docs/source/development/change_log.rst index 5509b82..b3a2e2e 100644 --- a/docs/source/development/change_log.rst +++ b/docs/source/development/change_log.rst @@ -3,6 +3,19 @@ Changelog ========= +Unreleased +---------- + +New and Improved +................ + +- ✨ Added Bash language support for the ``analyse`` module. + + Comments in shell scripts are now parsed for need ID references and one-line need + definitions. ``.sh``, ``.bash``, ``.zsh``, and ``.ksh`` files are discovered when + ``comment_type = "bash"``. The supported comment style is ``#``. Fish shell is not + supported (no ``tree-sitter-fish`` grammar is published for the Python package). + .. _`release:1.3.0`: 1.3.0 diff --git a/pyproject.toml b/pyproject.toml index d42bddc..015f5ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "tree-sitter-rust>=0.23.0", "tree-sitter-go>=0.23.0", "tree-sitter-json>=0.24.8", + "tree-sitter-bash>=0.25.1", ] [project.optional-dependencies] diff --git a/src/sphinx_codelinks/analyse/utils.py b/src/sphinx_codelinks/analyse/utils.py index b136be3..48e726a 100644 --- a/src/sphinx_codelinks/analyse/utils.py +++ b/src/sphinx_codelinks/analyse/utils.py @@ -44,6 +44,8 @@ "type_declaration", "type_spec", }, + # @Bash Scope Node Types, IMPL_BASH_2, impl, [FE_BASH] + CommentType.bash: {"function_definition"}, } logger = get_logger(__name__) @@ -74,6 +76,8 @@ (comment) @comment """ JSONC_QUERY = """(comment) @comment""" +# @Bash comment query for tree-sitter, IMPL_BASH_3, impl, [FE_BASH] +BASH_QUERY = """(comment) @comment""" # JSON value node types that can be associated with a comment. JSON_STRUCTURE_TYPES = { @@ -103,7 +107,7 @@ def is_text_file(filepath: Path, sample_size: int = 2048) -> bool: return False -# @Tree-sitter parser initialization for multiple languages, IMPL_LANG_1, impl, [FE_C_SUPPORT, FE_CPP, FE_PY, FE_YAML, FE_RUST, FE_GO, FE_JSONC] +# @Tree-sitter parser initialization for multiple languages, IMPL_LANG_1, impl, [FE_C_SUPPORT, FE_CPP, FE_PY, FE_YAML, FE_RUST, FE_GO, FE_JSONC, FE_BASH] def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]: if comment_type == CommentType.cpp: import tree_sitter_cpp # noqa: PLC0415 @@ -140,6 +144,11 @@ def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]: parsed_language = Language(tree_sitter_json.language()) query = Query(parsed_language, JSONC_QUERY) + elif comment_type == CommentType.bash: + import tree_sitter_bash # noqa: PLC0415 + + parsed_language = Language(tree_sitter_bash.language()) + query = Query(parsed_language, BASH_QUERY) else: raise ValueError(f"Unsupported comment style: {comment_type}") parser = Parser(parsed_language) diff --git a/src/sphinx_codelinks/source_discover/config.py b/src/sphinx_codelinks/source_discover/config.py index a0ef107..0c6d295 100644 --- a/src/sphinx_codelinks/source_discover/config.py +++ b/src/sphinx_codelinks/source_discover/config.py @@ -13,6 +13,12 @@ "rust": ["rs"], "go": ["go"], "jsonc": ["jsonc", "json"], + # Bash uses `#` line comments; zsh and ksh share the same comment syntax and + # are scanned with the bash grammar. Fish is intentionally excluded: it is + # not POSIX-compatible and no tree-sitter-fish distribution is published on + # PyPI, so it cannot be wired in here. Track fish separately if a PyPI + # grammar becomes available. + "bash": ["sh", "bash", "zsh", "ksh"], } @@ -27,6 +33,8 @@ class CommentType(str, Enum): go = "go" # @Support JSONC style comments, IMPL_JSONC_1, impl, [FE_JSONC]; jsonc = "jsonc" + # @Support Bash style comments, IMPL_BASH_1, impl, [FE_BASH]; + bash = "bash" class SourceDiscoverSectionConfigType(TypedDict, total=False): diff --git a/tests/__snapshots__/test_extraction_fixtures/test_extraction_fixture[oneline-default_oneliner_bash].json b/tests/__snapshots__/test_extraction_fixtures/test_extraction_fixture[oneline-default_oneliner_bash].json new file mode 100644 index 0000000..f32efb6 --- /dev/null +++ b/tests/__snapshots__/test_extraction_fixtures/test_extraction_fixture[oneline-default_oneliner_bash].json @@ -0,0 +1,19 @@ +{ + "needs": [ + { + "id": "IMPL_BASH", + "title": "Bash Title", + "type": "impl", + "links": { + "links": [ + "REQ_BASH" + ] + }, + "metadata": {}, + "line": 1 + } + ], + "need_refs": [], + "marked_rst": [], + "warnings": [] +} \ No newline at end of file diff --git a/tests/data/extraction/README.md b/tests/data/extraction/README.md index d8ef412..dd845c7 100644 --- a/tests/data/extraction/README.md +++ b/tests/data/extraction/README.md @@ -12,7 +12,7 @@ Each `*.yaml` file in this directory is a map of `case_name → case`: ```yaml default_oneliner_cpp: - lang: cpp # cpp | c | python | csharp | rust | yaml | go | jsonc + lang: cpp # cpp | c | python | csharp | rust | yaml | go | jsonc | bash config: default # "default", or an inline config block (see below) source: | // @My Title, IMPL_1, impl, [REQ_1] diff --git a/tests/data/extraction/oneline.yaml b/tests/data/extraction/oneline.yaml index afd9c70..fc38741 100644 --- a/tests/data/extraction/oneline.yaml +++ b/tests/data/extraction/oneline.yaml @@ -40,3 +40,10 @@ default_oneliner_yaml: source: | # @Yaml Title, IMPL_YAML, impl, [REQ_YAML] key: value + +default_oneliner_bash: + lang: bash + config: default + source: | + # @Bash Title, IMPL_BASH, impl, [REQ_BASH] + greet() { echo hi; } diff --git a/tests/test_extraction_fixtures.py b/tests/test_extraction_fixtures.py index 2372a07..1a0e560 100644 --- a/tests/test_extraction_fixtures.py +++ b/tests/test_extraction_fixtures.py @@ -35,6 +35,7 @@ "yaml": (CommentType.yaml, "yaml"), "go": (CommentType.go, "go"), "jsonc": (CommentType.jsonc, "jsonc"), + "bash": (CommentType.bash, "sh"), } diff --git a/tests/test_source_discover.py b/tests/test_source_discover.py index 063b764..4e8ec8a 100644 --- a/tests/test_source_discover.py +++ b/tests/test_source_discover.py @@ -49,7 +49,7 @@ "comment_type": "java", }, [ - "Schema validation error in field 'comment_type': 'java' is not one of ['cpp', 'cs', 'go', 'jsonc', 'python', 'rust', 'yaml']" + "Schema validation error in field 'comment_type': 'java' is not one of ['bash', 'cpp', 'cs', 'go', 'jsonc', 'python', 'rust', 'yaml']" ], ), ( @@ -182,6 +182,7 @@ def create_source_files(tmp_path: Path) -> Path: [ ("cpp", len(COMMENT_FILETYPE["cpp"])), ("python", len(COMMENT_FILETYPE["python"])), + ("bash", len(COMMENT_FILETYPE["bash"])), ], ) def test_comment_filetype( diff --git a/tests/test_src_trace.py b/tests/test_src_trace.py index b339055..7389e81 100644 --- a/tests/test_src_trace.py +++ b/tests/test_src_trace.py @@ -59,7 +59,7 @@ [ "Project 'dcdc' has the following errors:", "Schema validation error in field 'exclude': 123 is not of type 'string'", - "Schema validation error in field 'comment_type': 'java' is not one of ['cpp', 'cs', 'go', 'jsonc', 'python', 'rust', 'yaml']", + "Schema validation error in field 'comment_type': 'java' is not one of ['bash', 'cpp', 'cs', 'go', 'jsonc', 'python', 'rust', 'yaml']", "Schema validation error in field 'gitignore': '_true' is not of type 'boolean'", "Schema validation error in field 'include': 345 is not of type 'string'", "Schema validation error in field 'src_dir': ['../dcdc'] is not of type 'string'", From c4b1e47954864e3f79e5295790a23ef67fdeb2f8 Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Sat, 11 Jul 2026 23:06:41 +0200 Subject: [PATCH 2/3] fix(docs): restore the JSONC example line displaced by the bash table row The bash row was inserted between the JSONC row's last cell and its continuation line, so the mode-line example rendered inside the bash "discovered file types" cell and the JSONC cell lost it. --- docs/source/components/configuration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/components/configuration.rst b/docs/source/components/configuration.rst index 81da585..2b2b10a 100644 --- a/docs/source/components/configuration.rst +++ b/docs/source/components/configuration.rst @@ -325,11 +325,11 @@ Specifies the comment syntax style used in the source code files. This determine - ``//`` (single-line), ``/* */`` (multi-line) - ``.jsonc`` (always); ``.json`` only when the file opens with a comment + (e.g. the mode line ``// -*- mode: jsonc -*-``) * - Bash / POSIX shell - ``"bash"`` - ``#`` (single-line) - ``.sh``, ``.bash``, ``.zsh``, ``.ksh`` - (e.g. the mode line ``// -*- mode: jsonc -*-``) .. note:: Future versions may support additional programming languages. From f73507f77e80025c931830012de37e842c1cab57 Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Sat, 11 Jul 2026 23:06:41 +0200 Subject: [PATCH 3/3] test(analyse): cover bash shebang, function keyword form, and scope association - shebang_oneliner_bash extraction fixture: the shebang comment yields no marker or warning, the marker anchors to line 2, and `function name { }` definitions parse - test_find_associated_scope_bash: comments associate with the function_definition for both definition syntaxes, including the enclosing-scope fallback for comments inside a body --- ...ixture[oneline-shebang_oneliner_bash].json | 19 +++++++ tests/data/extraction/oneline.yaml | 10 ++++ tests/test_analyse_utils.py | 56 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 tests/__snapshots__/test_extraction_fixtures/test_extraction_fixture[oneline-shebang_oneliner_bash].json diff --git a/tests/__snapshots__/test_extraction_fixtures/test_extraction_fixture[oneline-shebang_oneliner_bash].json b/tests/__snapshots__/test_extraction_fixtures/test_extraction_fixture[oneline-shebang_oneliner_bash].json new file mode 100644 index 0000000..d16fd2c --- /dev/null +++ b/tests/__snapshots__/test_extraction_fixtures/test_extraction_fixture[oneline-shebang_oneliner_bash].json @@ -0,0 +1,19 @@ +{ + "needs": [ + { + "id": "IMPL_BASH_SHEBANG", + "title": "Bash Title", + "type": "impl", + "links": { + "links": [ + "REQ_BASH" + ] + }, + "metadata": {}, + "line": 2 + } + ], + "need_refs": [], + "marked_rst": [], + "warnings": [] +} \ No newline at end of file diff --git a/tests/data/extraction/oneline.yaml b/tests/data/extraction/oneline.yaml index fc38741..9a423ae 100644 --- a/tests/data/extraction/oneline.yaml +++ b/tests/data/extraction/oneline.yaml @@ -47,3 +47,13 @@ default_oneliner_bash: source: | # @Bash Title, IMPL_BASH, impl, [REQ_BASH] greet() { echo hi; } + +# the shebang is a comment node but must not yield a marker or warning; +# the marker anchors to line 2 and the `function` keyword form also parses +shebang_oneliner_bash: + lang: bash + config: default + source: | + #!/bin/bash + # @Bash Title, IMPL_BASH_SHEBANG, impl, [REQ_BASH] + function greet { echo hi; } diff --git a/tests/test_analyse_utils.py b/tests/test_analyse_utils.py index bf896a9..bbf348f 100644 --- a/tests/test_analyse_utils.py +++ b/tests/test_analyse_utils.py @@ -6,6 +6,7 @@ import pytest from tree_sitter import Language, Parser, Query from tree_sitter import Node as TreeSitterNode +import tree_sitter_bash import tree_sitter_c_sharp import tree_sitter_cpp import tree_sitter_go @@ -75,6 +76,14 @@ def init_jsonc_tree_sitter() -> tuple[Parser, Query]: return parser, query +@pytest.fixture(scope="session") +def init_bash_tree_sitter() -> tuple[Parser, Query]: + parsed_language = Language(tree_sitter_bash.language()) + query = Query(parsed_language, utils.BASH_QUERY) + parser = Parser(parsed_language) + return parser, query + + @pytest.mark.parametrize( ("code", "result"), [ @@ -425,6 +434,53 @@ def test_find_associated_scope_jsonc(code, result, init_jsonc_tree_sitter): assert result in jsonc_structure +@pytest.mark.parametrize( + ("code", "result"), + [ + # comment above a POSIX-style function definition + ( + b""" + # @req-id: need_001 + greet() { + echo hi + } + """, + "greet()", + ), + # comment above the `function` keyword form + ( + b""" + # @req-id: need_002 + function greet { + echo hi + } + """, + "function greet", + ), + # comment inside a function body falls back to the enclosing function + ( + b""" + greet() { + # @req-id: need_003 + echo hi + } + """, + "greet()", + ), + ], +) +def test_find_associated_scope_bash(code, result, init_bash_tree_sitter): + parser, query = init_bash_tree_sitter + comments = utils.extract_comments(code, parser, query) + node: TreeSitterNode | None = utils.find_associated_scope( + comments[0], CommentType.bash + ) + assert node + assert node.text + func_def = node.text.decode("utf-8") + assert func_def.startswith(result) + + @pytest.mark.parametrize( ("code", "result"), [