diff --git a/docs/source/components/analyse.rst b/docs/source/components/analyse.rst index 631eafd..59c1b67 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# (``//``, ``/* */``, ``///``), TypeScript (``//``, ``/* */``), Python (``#``), YAML (``#``), Rust (``//``, ``/* */``, ``///``), Go (``//``, ``/* */``) and JSONC (``//``, ``/* */``) 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 8da2f2a..8e6d36d 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"``, ``"ts"``, ``"yaml"``, ``"rust"``, ``"go"``, ``"jsonc"`` .. code-block:: toml @@ -304,6 +304,11 @@ Specifies the comment syntax style used in the source code files. This determine ``/* */`` (multi-line), ``///`` (XML doc comments) - ``.cs`` + * - TypeScript + - ``"ts"`` + - ``//`` (single-line), + ``/* */`` (multi-line) + - ``.ts``, ``.tsx`` * - YAML - ``"yaml"`` - ``#`` (single-line) diff --git a/docs/source/components/discover.rst b/docs/source/components/discover.rst index 33a2d52..8b78645 100644 --- a/docs/source/components/discover.rst +++ b/docs/source/components/discover.rst @@ -38,3 +38,13 @@ Usage Examples include = [] exclude = ["tests/**", "setup.py"] comment_type = "python" + +**TypeScript Project:** + +.. code-block:: toml + + [source_discover] + src_dir = "./frontend" + include = ["**/*.ts", "**/*.tsx"] + exclude = ["**/*.test.ts", "**/*.spec.ts"] + comment_type = "ts" diff --git a/docs/source/development/change_log.rst b/docs/source/development/change_log.rst index 5509b82..cfb8246 100644 --- a/docs/source/development/change_log.rst +++ b/docs/source/development/change_log.rst @@ -3,6 +3,20 @@ Changelog ========= +Under development +----------------- + +New and Improved +................ + +- ✨ Added TypeScript comment type support for source discovery and analysis. + + TypeScript files can now be processed using ``comment_type = "ts"``. + Source discovery supports both ``.ts`` and ``.tsx`` extensions by default. + +Fixes +..... + .. _`release:1.3.0`: 1.3.0 diff --git a/pyproject.toml b/pyproject.toml index 61900c9..c5f73a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ # https://github.com/tree-sitter/py-tree-sitter/issues/386#issuecomment-3101430799 "tree-sitter~=0.25.1", "tree-sitter-c-sharp>=0.23.1", + "tree-sitter-typescript>=0.23.2", "tree-sitter-yaml>=0.7.1", "tree-sitter-rust>=0.23.0", "tree-sitter-go>=0.23.0", diff --git a/src/sphinx_codelinks/analyse/utils.py b/src/sphinx_codelinks/analyse/utils.py index b136be3..8fcef2e 100644 --- a/src/sphinx_codelinks/analyse/utils.py +++ b/src/sphinx_codelinks/analyse/utils.py @@ -28,6 +28,13 @@ # @C and C++ Scope Node Types, IMPL_C_2, impl, [FE_C_SUPPORT, FE_CPP] CommentType.cpp: {"function_definition", "class_definition"}, CommentType.cs: {"method_declaration", "class_declaration", "property_declaration"}, + CommentType.ts: { + "function_declaration", + "class_declaration", + "method_definition", + "lexical_declaration", + "variable_declaration", + }, # @Rust Scope Node Types, IMPL_RUST_2, impl, [FE_RUST]; CommentType.rust: { "function_item", @@ -64,6 +71,7 @@ """ CPP_QUERY = """(comment) @comment""" C_SHARP_QUERY = """(comment) @comment""" +TYPE_SCRIPT_QUERY = """(comment) @comment""" YAML_QUERY = """(comment) @comment""" RUST_QUERY = """ (line_comment) @comment @@ -120,6 +128,11 @@ def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]: parsed_language = Language(tree_sitter_c_sharp.language()) query = Query(parsed_language, C_SHARP_QUERY) + elif comment_type == CommentType.ts: + import tree_sitter_typescript # noqa: PLC0415 + + parsed_language = Language(tree_sitter_typescript.language_typescript()) + query = Query(parsed_language, TYPE_SCRIPT_QUERY) elif comment_type == CommentType.yaml: import tree_sitter_yaml # noqa: PLC0415 diff --git a/src/sphinx_codelinks/source_discover/config.py b/src/sphinx_codelinks/source_discover/config.py index a0ef107..64b3925 100644 --- a/src/sphinx_codelinks/source_discover/config.py +++ b/src/sphinx_codelinks/source_discover/config.py @@ -9,6 +9,7 @@ "cpp": ["c", "ci", "cpp", "cc", "cxx", "h", "hpp", "hxx", "hh", "ihl"], "python": ["py"], "cs": ["cs"], + "ts": ["ts", "tsx"], "yaml": ["yml", "yaml"], "rust": ["rs"], "go": ["go"], @@ -20,6 +21,7 @@ class CommentType(str, Enum): python = "python" cpp = "cpp" cs = "cs" + ts = "ts" yaml = "yaml" # @Support Rust style comments, IMPL_RUST_1, impl, [FE_RUST]; rust = "rust" diff --git a/tests/data/discover_fixtures.json b/tests/data/discover_fixtures.json index 7ffd5d4..5959cab 100644 --- a/tests/data/discover_fixtures.json +++ b/tests/data/discover_fixtures.json @@ -75,7 +75,9 @@ "config": { "src_dir": "src", "include": [], - "exclude": ["**/build/**"], + "exclude": [ + "**/build/**" + ], "gitignore": false, "comment_type": "cpp" }, @@ -94,7 +96,9 @@ }, "config": { "src_dir": "src", - "include": ["**/*.cpp"], + "include": [ + "**/*.cpp" + ], "exclude": [], "gitignore": false, "comment_type": "cpp" @@ -115,8 +119,12 @@ }, "config": { "src_dir": "src", - "include": ["**/*.cpp"], - "exclude": ["**/test_*.cpp"], + "include": [ + "**/*.cpp" + ], + "exclude": [ + "**/test_*.cpp" + ], "gitignore": false, "comment_type": "cpp" }, @@ -280,7 +288,9 @@ "config": { "src_dir": "src", "include": [], - "exclude": ["**/test_*.cpp"], + "exclude": [ + "**/test_*.cpp" + ], "gitignore": true, "comment_type": "cpp" }, @@ -386,5 +396,27 @@ "expected": [ "src/Program.cs" ] + }, + { + "name": "typescript_comment_type", + "description": "TypeScript comment type discovers .ts and .tsx files", + "git_init": false, + "files": { + "src/main.ts": "// main", + "src/component.tsx": "// component", + "src/main.cpp": "// not ts", + "src/util.py": "# not ts" + }, + "config": { + "src_dir": "src", + "include": [], + "exclude": [], + "gitignore": false, + "comment_type": "ts" + }, + "expected": [ + "src/component.tsx", + "src/main.ts" + ] } ] diff --git a/tests/data/typescript/demo.ts b/tests/data/typescript/demo.ts new file mode 100644 index 0000000..66aade0 --- /dev/null +++ b/tests/data/typescript/demo.ts @@ -0,0 +1,17 @@ +// regular comment +function testA() { + // @type,TS_REQ_002,TypeScript one-line test + return 1; +} + +/* regular block comment */ +const testB = () => { + return 2; +}; + +// another comment +class Demo { + methodA() { + return 3; + } +} diff --git a/tests/test_analyse.py b/tests/test_analyse.py index e465a10..fe7098a 100644 --- a/tests/test_analyse.py +++ b/tests/test_analyse.py @@ -56,32 +56,34 @@ def test_analyse(src_dir, src_paths, tmp_path, snapshot_marks): @pytest.mark.parametrize( - "src_dir, src_paths , oneline_comment_style, result", + "case", [ - ( - TEST_DIR / "data" / "dcdc", - [ + { + "src_dir": TEST_DIR / "data" / "dcdc", + "src_paths": [ TEST_DIR / "data" / "dcdc" / "charge" / "demo_1.cpp", TEST_DIR / "data" / "dcdc" / "charge" / "demo_2.cpp", TEST_DIR / "data" / "dcdc" / "discharge" / "demo_3.cpp", TEST_DIR / "data" / "dcdc" / "supercharge.cpp", ], - ONELINE_COMMENT_STYLE, - { + "comment_type": "cpp", + "oneline_comment_style": ONELINE_COMMENT_STYLE, + "result": { "num_src_files": 4, "num_uncached_files": 4, "num_cached_files": 0, "num_comments": 29, "num_oneline_warnings": 0, }, - ), - ( - TEST_DIR / "data" / "oneline_comment_basic", - [ + }, + { + "src_dir": TEST_DIR / "data" / "oneline_comment_basic", + "src_paths": [ TEST_DIR / "data" / "oneline_comment_basic" / "basic_oneliners.c", ], - ONELINE_COMMENT_STYLE, - { + "comment_type": "cpp", + "oneline_comment_style": ONELINE_COMMENT_STYLE, + "result": { "num_src_files": 1, "num_uncached_files": 1, "num_cached_files": 0, @@ -89,14 +91,15 @@ def test_analyse(src_dir, src_paths, tmp_path, snapshot_marks): "num_oneline_warnings": 0, "warnings_path_exists": True, }, - ), - ( - TEST_DIR / "data" / "oneline_comment_default", - [ + }, + { + "src_dir": TEST_DIR / "data" / "oneline_comment_default", + "src_paths": [ TEST_DIR / "data" / "oneline_comment_default" / "default_oneliners.c", ], - ONELINE_COMMENT_STYLE_DEFAULT, - { + "comment_type": "cpp", + "oneline_comment_style": ONELINE_COMMENT_STYLE_DEFAULT, + "result": { "num_src_files": 1, "num_uncached_files": 1, "num_cached_files": 0, @@ -104,53 +107,68 @@ def test_analyse(src_dir, src_paths, tmp_path, snapshot_marks): "num_oneline_warnings": 1, "warnings_path_exists": True, }, - ), - ( - TEST_DIR / "data" / "rust", - [ + }, + { + "src_dir": TEST_DIR / "data" / "rust", + "src_paths": [ TEST_DIR / "data" / "rust" / "demo.rs", ], - ONELINE_COMMENT_STYLE_DEFAULT, - { + "comment_type": "rust", + "oneline_comment_style": ONELINE_COMMENT_STYLE_DEFAULT, + "result": { "num_src_files": 1, "num_uncached_files": 1, "num_cached_files": 0, "num_comments": 6, "num_oneline_warnings": 0, }, - ), - ( - TEST_DIR / "data" / "jsonc", - [ + }, + { + "src_dir": TEST_DIR / "data" / "typescript", + "src_paths": [ + TEST_DIR / "data" / "typescript" / "demo.ts", + ], + "comment_type": "ts", + "oneline_comment_style": ONELINE_COMMENT_STYLE_DEFAULT, + "result": { + "num_src_files": 1, + "num_uncached_files": 1, + "num_cached_files": 0, + "num_comments": 4, + "num_oneline_warnings": 0, + }, + }, + { + "src_dir": TEST_DIR / "data" / "jsonc", + "src_paths": [ TEST_DIR / "data" / "jsonc" / "demo.jsonc", ], - ONELINE_COMMENT_STYLE_DEFAULT, - { + "comment_type": CommentType.jsonc, + "oneline_comment_style": ONELINE_COMMENT_STYLE_DEFAULT, + "result": { "num_src_files": 1, "num_uncached_files": 1, "num_cached_files": 0, "num_comments": 4, "num_oneline_warnings": 0, - "comment_type": CommentType.jsonc, }, - ), + }, ], ) -def test_analyse_oneline_needs( - tmp_path, src_dir, src_paths, oneline_comment_style, result -): +def test_analyse_oneline_needs(tmp_path, case): src_analyse_config = SourceAnalyseConfig( - src_files=src_paths, - src_dir=src_dir, + src_files=case["src_paths"], + src_dir=case["src_dir"], get_need_id_refs=False, get_oneline_needs=True, get_rst=False, - oneline_comment_style=oneline_comment_style, - comment_type=result.get("comment_type", CommentType.cpp), + oneline_comment_style=case["oneline_comment_style"], + comment_type=case["comment_type"], ) src_analyse = SourceAnalyse(src_analyse_config) src_analyse.run() + result = case["result"] assert len(src_analyse.src_files) == result["num_src_files"] assert len(src_analyse.oneline_warnings) == result["num_oneline_warnings"] diff --git a/tests/test_analyse_utils.py b/tests/test_analyse_utils.py index bf896a9..95f5b9b 100644 --- a/tests/test_analyse_utils.py +++ b/tests/test_analyse_utils.py @@ -12,6 +12,7 @@ import tree_sitter_json import tree_sitter_python import tree_sitter_rust +import tree_sitter_typescript import tree_sitter_yaml from sphinx_codelinks.analyse import utils @@ -59,6 +60,14 @@ def init_rust_tree_sitter() -> tuple[Parser, Query]: return parser, query +@pytest.fixture(scope="session") +def init_typescript_tree_sitter() -> tuple[Parser, Query]: + parsed_language = Language(tree_sitter_typescript.language_typescript()) + query = Query(parsed_language, utils.TYPE_SCRIPT_QUERY) + parser = Parser(parsed_language) + return parser, query + + @pytest.fixture(scope="session") def init_go_tree_sitter() -> tuple[Parser, Query]: parsed_language = Language(tree_sitter_go.language()) @@ -425,6 +434,41 @@ def test_find_associated_scope_jsonc(code, result, init_jsonc_tree_sitter): assert result in jsonc_structure +@pytest.mark.parametrize( + ("code", "result"), + [ + ( + b""" + // @req-id: need_001 + function dummyFunc1() { + } + """, + "function dummyFunc1()", + ), + ( + b""" + class DummyClass { + // @req-id: need_001 + method1() { + } + } + """, + "method1()", + ), + ], +) +def test_find_associated_scope_typescript(code, result, init_typescript_tree_sitter): + parser, query = init_typescript_tree_sitter + comments = utils.extract_comments(code, parser, query) + node: TreeSitterNode | None = utils.find_associated_scope( + comments[0], CommentType.ts + ) + assert node + assert node.text + ts_def = node.text.decode("utf-8") + assert result in ts_def + + @pytest.mark.parametrize( ("code", "result"), [ @@ -579,6 +623,29 @@ def test_find_next_scope_csharp(code, result, init_csharp_tree_sitter): assert result in func_def +@pytest.mark.parametrize( + ("code", "result"), + [ + ( + b""" + // @req-id: need_001 + function dummyFunc1() { + } + """, + "function dummyFunc1()", + ), + ], +) +def test_find_next_scope_typescript(code, result, init_typescript_tree_sitter): + parser, query = init_typescript_tree_sitter + comments = utils.extract_comments(code, parser, query) + node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.ts) + assert node + assert node.text + func_def = node.text.decode("utf-8") + assert result in func_def + + @pytest.mark.parametrize( ("code", "result"), [ @@ -833,6 +900,37 @@ def test_csharp_comment(code, num_comments, result, init_csharp_tree_sitter): assert comments[0].text.decode("utf-8") == result +@pytest.mark.parametrize( + ("code", "num_comments", "result"), + [ + ( + b""" + // @req-id: need_001 + function dummyFunc1() { + } + """, + 1, + "// @req-id: need_001", + ), + ( + b""" + /* @req-id: need_001 */ + const value = 1; + """, + 1, + "/* @req-id: need_001 */", + ), + ], +) +def test_typescript_comment(code, num_comments, result, init_typescript_tree_sitter): + parser, query = init_typescript_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", "num_comments", "result"), [ diff --git a/tests/test_source_discover.py b/tests/test_source_discover.py index 063b764..fb3b42d 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 ['cpp', 'cs', 'go', 'jsonc', 'python', 'rust', 'ts', 'yaml']" ], ), ( @@ -99,6 +99,13 @@ def test_schema_negative(config, msgs): "gitignore": True, "comment_type": "python", }, + { + "src_dir": "/path/to/root", + "exclude": ["exclude1", "exclude2"], + "include": ["include1", "include2"], + "gitignore": True, + "comment_type": "ts", + }, { "src_dir": "/path/to/root", "follow_links": True, @@ -182,6 +189,7 @@ def create_source_files(tmp_path: Path) -> Path: [ ("cpp", len(COMMENT_FILETYPE["cpp"])), ("python", len(COMMENT_FILETYPE["python"])), + ("ts", len(COMMENT_FILETYPE["ts"])), ], ) def test_comment_filetype( diff --git a/tests/test_src_trace.py b/tests/test_src_trace.py index b339055..7059d17 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 ['cpp', 'cs', 'go', 'jsonc', 'python', 'rust', 'ts', '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'",