diff --git a/docs/source/components/analyse.rst b/docs/source/components/analyse.rst index 2b47c5a1..a6271b72 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 (``#``) and Rust (``//``, ``/* */``, ``///``) comment styles are supported +- **Language Support**: C/C++ (``//``, ``/* */``), C# (``//``, ``/* */``, ``///``), Python (``#``), YAML (``#``), Rust (``//``, ``/* */``, ``///``) and Go (``//``, ``/* */``) 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 0d354dd5..d92650e0 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"`` +**Supported values:** ``"cpp"``, ``"python"``, ``"cs"``, ``"yaml"``, ``"rust"``, ``"go"`` .. code-block:: toml @@ -315,6 +315,11 @@ Specifies the comment syntax style used in the source code files. This determine ``///`` (doc comments), ``//!`` (inner doc comments) - ``.rs`` + * - Go + - ``"go"`` + - ``//`` (single-line), + ``/* */`` (multi-line) + - ``.go`` .. note:: Future versions may support additional programming languages. diff --git a/docs/source/components/features.rst b/docs/source/components/features.rst index 9fc9ca30..cd089db4 100644 --- a/docs/source/components/features.rst +++ b/docs/source/components/features.rst @@ -158,6 +158,31 @@ Features .. fault:: Sphinx-codelinks halucinates traceability objects in Rust :id: FAULT_RUST_2 +.. feature:: Go Language Support + :id: FE_GO + + Support for defining traceability objects in Go source files via one-line comment annotations. + + The Go language parser leverages tree-sitter to accurately identify and extract + comments from Go source files, including single-line (``//``) and multi-line + (``/* */``) comment styles. This ensures that traceability markers are correctly + associated with the appropriate code structures such as functions, methods, types, + and structs. + + Key capabilities: + + * Detection of inline and block comments + * Association of comments with function and method declarations + * Support for standard Go comment conventions + * Accurate scope detection for nested structures + * File extension ``.go`` auto-discovered when ``comment_type = "go"`` + + .. fault:: Traceability objects are not detected in Go language + :id: FAULT_GO_1 + + .. fault:: Sphinx-codelinks halucinates traceability objects in Go + :id: FAULT_GO_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 e9f20e27..b34724f0 100644 --- a/docs/source/development/change_log.rst +++ b/docs/source/development/change_log.rst @@ -7,6 +7,10 @@ Upcoming -------- - ⬆️ Support and test sphinx-needs v5-8 +- ✨ Added Go parser for the ``analyse`` module. + + Need ID references and one-line need definitions can now be extracted from Go source files. + The supported comment styles are ``//`` and ``/* */``. .. _`release:1.2.0`: diff --git a/pyproject.toml b/pyproject.toml index 93cb917e..b4c1244c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "tree-sitter-c-sharp>=0.23.1", "tree-sitter-yaml>=0.7.1", "tree-sitter-rust>=0.23.0", + "tree-sitter-go>=0.23.0", ] [build-system] diff --git a/src/sphinx_codelinks/analyse/utils.py b/src/sphinx_codelinks/analyse/utils.py index 5a11fddb..762d9e55 100644 --- a/src/sphinx_codelinks/analyse/utils.py +++ b/src/sphinx_codelinks/analyse/utils.py @@ -29,6 +29,13 @@ "trait_item", "mod_item", }, + # @Go Scope Node Types, IMPL_GO_4, impl, [FE_GO] + CommentType.go: { + "function_declaration", + "method_declaration", + "type_declaration", + "type_spec", + }, } # initialize logger @@ -60,6 +67,10 @@ (line_comment) @comment (block_comment) @comment """ +# @Go comment query for tree-sitter, IMPL_GO_3, impl, [FE_GO] +GO_QUERY = """ + (comment) @comment +""" def is_text_file(filepath: Path, sample_size: int = 2048) -> bool: @@ -77,7 +88,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] +# @Tree-sitter parser initialization for multiple languages, IMPL_LANG_1, impl, [FE_C_SUPPORT, FE_CPP, FE_PY, FE_YAML, FE_RUST, FE_GO] def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]: if comment_type == CommentType.cpp: import tree_sitter_cpp # noqa: PLC0415 @@ -104,6 +115,11 @@ def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]: parsed_language = Language(tree_sitter_rust.language()) query = Query(parsed_language, RUST_QUERY) + elif comment_type == CommentType.go: + import tree_sitter_go # noqa: PLC0415 + + parsed_language = Language(tree_sitter_go.language()) + query = Query(parsed_language, GO_QUERY) else: raise ValueError(f"Unsupported comment style: {comment_type}") parser = Parser(parsed_language) diff --git a/src/sphinx_codelinks/config.py b/src/sphinx_codelinks/config.py index 60fe9275..f32de434 100644 --- a/src/sphinx_codelinks/config.py +++ b/src/sphinx_codelinks/config.py @@ -24,6 +24,8 @@ # @Support Python style comments, IMPL_PY_1, impl, [FE_PY] CommentType.python: ["#"], CommentType.cs: ["//", "/*", "///"], + # @Support Go style comments, IMPL_GO_2, impl, [FE_GO] + CommentType.go: ["//", "/*"], } ESCAPE = "\\" diff --git a/src/sphinx_codelinks/source_discover/config.py b/src/sphinx_codelinks/source_discover/config.py index 51ae3c08..aad64e41 100644 --- a/src/sphinx_codelinks/source_discover/config.py +++ b/src/sphinx_codelinks/source_discover/config.py @@ -11,6 +11,7 @@ "cs": ["cs"], "yaml": ["yml", "yaml"], "rust": ["rs"], + "go": ["go"], } @@ -21,6 +22,8 @@ class CommentType(str, Enum): yaml = "yaml" # @Support Rust style comments, IMPL_RUST_1, impl, [FE_RUST]; rust = "rust" + # @Support Go style comments, IMPL_GO_1, impl, [FE_GO]; + go = "go" class SourceDiscoverSectionConfigType(TypedDict, total=False): diff --git a/tests/__snapshots__/test_src_trace/test_build_html[sphinx_project4-source_code4].doctree.xml b/tests/__snapshots__/test_src_trace/test_build_html[sphinx_project4-source_code4].doctree.xml new file mode 100644 index 00000000..c3361fad --- /dev/null +++ b/tests/__snapshots__/test_src_trace/test_build_html[sphinx_project4-source_code4].doctree.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/doc_test/go_basic/conf.py b/tests/doc_test/go_basic/conf.py new file mode 100644 index 00000000..d67a5bb1 --- /dev/null +++ b/tests/doc_test/go_basic/conf.py @@ -0,0 +1,27 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "source-tracing-demo" +copyright = "2025, useblocks" +author = "useblocks" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ["sphinx_needs", "sphinx_codelinks"] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +src_trace_config_from_toml = "src_trace.toml" + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "alabaster" +html_static_path = ["_static"] diff --git a/tests/doc_test/go_basic/dummy_src.go b/tests/doc_test/go_basic/dummy_src.go new file mode 100644 index 00000000..03f78ee1 --- /dev/null +++ b/tests/doc_test/go_basic/dummy_src.go @@ -0,0 +1,8 @@ +package main + +import "fmt" + +// @ title here, IMPL_1, impl +func singleLineExample() { + fmt.Println("Single-line comment example") +} diff --git a/tests/doc_test/go_basic/index.rst b/tests/doc_test/go_basic/index.rst new file mode 100644 index 00000000..1750ff79 --- /dev/null +++ b/tests/doc_test/go_basic/index.rst @@ -0,0 +1,2 @@ +.. src-trace:: + :project: src diff --git a/tests/doc_test/go_basic/src_trace.toml b/tests/doc_test/go_basic/src_trace.toml new file mode 100644 index 00000000..1a182555 --- /dev/null +++ b/tests/doc_test/go_basic/src_trace.toml @@ -0,0 +1,5 @@ +[codelinks.projects.src] +remote_url_pattern = "https://github.com/useblocks/sphinx-codelinks/blob/{commit}/{path}#L{line}" + +[codelinks.projects.src.source_discover] +comment_type = "go" diff --git a/tests/test_analyse_utils.py b/tests/test_analyse_utils.py index 207b1f9a..6bc5f94c 100644 --- a/tests/test_analyse_utils.py +++ b/tests/test_analyse_utils.py @@ -8,6 +8,7 @@ from tree_sitter import Node as TreeSitterNode import tree_sitter_c_sharp import tree_sitter_cpp +import tree_sitter_go import tree_sitter_python import tree_sitter_rust import tree_sitter_yaml @@ -57,6 +58,14 @@ def init_rust_tree_sitter() -> tuple[Parser, Query]: return parser, query +@pytest.fixture(scope="session") +def init_go_tree_sitter() -> tuple[Parser, Query]: + parsed_language = Language(tree_sitter_go.language()) + query = Query(parsed_language, utils.GO_QUERY) + parser = Parser(parsed_language) + return parser, query + + @pytest.mark.parametrize( ("code", "result"), [ @@ -815,6 +824,166 @@ def test_yaml_comment(code, num_comments, result, init_yaml_tree_sitter): assert comments[0].text.decode("utf-8") == result +@pytest.mark.parametrize( + ("code", "num_comments", "result"), + [ + ( + b""" + // @req-id: need_001 + func dummyFunc1() { + } + """, + 1, + "// @req-id: need_001", + ), + ( + b""" + func dummyFunc1() { + // @req-id: need_001 + } + """, + 1, + "// @req-id: need_001", + ), + ( + b""" + /* @req-id: need_001 */ + func dummyFunc1() { + } + """, + 1, + "/* @req-id: need_001 */", + ), + ( + b""" + // @req-id: need_001 + // + // + func dummyFunc1() { + } + """, + 3, + "// @req-id: need_001", + ), + ], +) +def test_go_comment(code, num_comments, result, init_go_tree_sitter): + parser, query = init_go_tree_sitter + comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query) + comments.sort(key=lambda x: x.start_point.row) + assert len(comments) == num_comments + assert comments[0].text + assert comments[0].text.decode("utf-8") == result + + +@pytest.mark.parametrize( + ("code", "result"), + [ + ( + b""" + // @req-id: need_001 + func dummyFunc1() { + } + """, + "func dummyFunc1()", + ), + ( + b""" + func dummyFunc2() { + } + // @req-id: need_001 + func dummyFunc1() { + } + """, + "func dummyFunc1()", + ), + ( + b""" + /* @req-id: need_001 */ + func dummyFunc1() { + } + """, + "func dummyFunc1()", + ), + ], +) +def test_find_associated_scope_go(code, result, init_go_tree_sitter): + parser, query = init_go_tree_sitter + comments = utils.extract_comments(code, parser, query) + node: TreeSitterNode | None = utils.find_associated_scope( + comments[0], CommentType.go + ) + assert node + assert node.text + go_def = node.text.decode("utf-8") + assert result in go_def + + +@pytest.mark.parametrize( + ("code", "result"), + [ + ( + b""" + // @req-id: need_001 + func dummyFunc1() { + } + """, + "func dummyFunc1()", + ), + ( + b""" + // @req-id: need_001 + type DummyStruct struct { + Field int + } + """, + "type DummyStruct struct", + ), + ], +) +def test_find_next_scope_go(code, result, init_go_tree_sitter): + parser, query = init_go_tree_sitter + comments = utils.extract_comments(code, parser, query) + node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.go) + assert node + assert node.text + go_def = node.text.decode("utf-8") + assert result in go_def + + +@pytest.mark.parametrize( + ("code", "result"), + [ + ( + b""" + func dummyFunc1() { + // @req-id: need_001 + } + """, + "func dummyFunc1()", + ), + ( + b""" + func dummyFunc1() { + /* @req-id: need_001 */ + } + """, + "func dummyFunc1()", + ), + ], +) +def test_find_enclosing_scope_go(code, result, init_go_tree_sitter): + parser, query = init_go_tree_sitter + comments = utils.extract_comments(code, parser, query) + node: TreeSitterNode | None = utils.find_enclosing_scope( + comments[0], CommentType.go + ) + assert node + assert node.text + go_def = node.text.decode("utf-8") + assert result in go_def + + @pytest.mark.parametrize( ("git_url", "rev", "project_path", "filepath", "lineno", "result"), [ diff --git a/tests/test_source_discover.py b/tests/test_source_discover.py index 5a6f5c1f..d533829a 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', 'python', 'rust', 'yaml']" + "Schema validation error in field 'comment_type': 'java' is not one of ['cpp', 'cs', 'go', 'python', 'rust', 'yaml']" ], ), ( diff --git a/tests/test_src_trace.py b/tests/test_src_trace.py index 8e87a71e..4eb3a474 100644 --- a/tests/test_src_trace.py +++ b/tests/test_src_trace.py @@ -58,7 +58,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', 'python', 'rust', 'yaml']", + "Schema validation error in field 'comment_type': 'java' is not one of ['cpp', 'cs', 'go', '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'", @@ -192,6 +192,10 @@ def test_src_tracing_config_positive(make_app: Callable[..., SphinxTestApp], tmp Path("doc_test") / "id_required", Path("doc_test") / "id_required", ), + ( + Path("doc_test") / "go_basic", + Path("doc_test") / "go_basic", + ), ], ) def test_build_html(