-
Notifications
You must be signed in to change notification settings - Fork 6
feat(markdown): add CommentType.markdown backed by tree-sitter-markdown #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -266,6 +266,41 @@ Features | |
| .. fault:: Sphinx-codelinks hallucinates traceability objects in Bash | ||
| :id: FAULT_BASH_2 | ||
|
|
||
| .. feature:: Markdown Language Support | ||
| :id: FE_MARKDOWN | ||
|
|
||
| Support for defining traceability objects in Markdown files using | ||
| HTML-comment markers (``<!-- @needs … -->``). | ||
|
|
||
| The Markdown language parser leverages tree-sitter to identify and extract | ||
| standalone HTML-comment blocks from Markdown documents, enabling | ||
| requirements traceability in agent definition files, skill files, and other | ||
| Markdown-based implementation artefacts. | ||
|
|
||
| ``.md`` and ``.markdown`` files are auto-discovered when | ||
| ``comment_type = "markdown"``. Because tree-sitter-markdown exposes | ||
| standalone HTML comments as ``html_block`` nodes, only block-level | ||
| ``<!-- … -->`` markers are captured; inline HTML comments inside paragraphs | ||
| are not. | ||
|
|
||
| Key capabilities: | ||
|
|
||
| * HTML-comment (``<!-- … -->``) detection via tree-sitter | ||
| * Auto-discovery of ``.md`` and ``.markdown`` files | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Auto-discovering
pagename = str((file_path.relative_to(app.outdir)).with_suffix(""))Until now no discoverable extension was also a Sphinx source extension. With |
||
| * Oneline-only mode (no scope association needed) | ||
|
|
||
| .. note:: | ||
|
|
||
| Because the captured node text includes the full ``<!-- … -->`` | ||
| delimiters, callers must set ``end_sequence: " -->"`` (not the default | ||
| ``"\n"``) in their ``oneline_comment_style`` config. | ||
|
|
||
| .. fault:: Traceability objects are not detected in Markdown | ||
| :id: FAULT_MARKDOWN_1 | ||
|
|
||
| .. fault:: Sphinx-codelinks hallucinates traceability objects in Markdown | ||
| :id: FAULT_MARKDOWN_2 | ||
|
|
||
| .. feature:: Preprocessor-Aware C/C++ Extraction | ||
| :id: FE_PREPROC | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,21 @@ | |
| Changelog | ||
| ========= | ||
|
|
||
| Unreleased | ||
| ---------- | ||
|
|
||
| New and Improved | ||
| ................ | ||
|
|
||
| - ✨ Added Markdown language support for the ``analyse`` module. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Commit and PR titles don't follow the documented format. AGENTS.md, Commit Message Format: This branch uses Conventional Commits instead:
Also, |
||
|
|
||
| Standalone HTML-comment blocks (``<!-- @needs … -->``) in Markdown files are | ||
| now parsed for need ID references and one-line need definitions. ``.md`` and | ||
| ``.markdown`` files are discovered when ``comment_type = "markdown"``. | ||
| Because ``tree-sitter-markdown`` exposes HTML comments as ``html_block`` | ||
| nodes, callers must set ``end_sequence: " -->"`` in their | ||
| ``oneline_comment_style`` configuration. | ||
|
|
||
| .. _`release:1.4.0`: | ||
|
|
||
| 1.4.0 | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -46,6 +46,11 @@ | |||||
| }, | ||||||
| # @Bash Scope Node Types, IMPL_BASH_2, impl, [FE_BASH] | ||||||
| CommentType.bash: {"function_definition"}, | ||||||
| # Markdown has no function/class scopes relevant for code traceability. | ||||||
| # oneline markers in Markdown are always standalone html_block nodes; | ||||||
| # scope association (find_enclosing_scope / find_next_scope) is never | ||||||
| # invoked when get_oneline_needs=True and get_need_id_refs=False. | ||||||
| # CommentType.markdown is intentionally absent from this table. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This justification is factually wrong: scope association is invoked for Markdown, on every comment, regardless of those flags.
tagged_scope = utils.find_associated_scope(
src_comment.node, self.analyse_config.comment_type
)
if self.analyse_config.get_need_id_refs:
...
if self.analyse_config.get_oneline_needs:
...Because scope_types = SCOPE_NODE_TYPES.get(comment_type, SCOPE_NODE_TYPES[CommentType.cpp])so every |
||||||
| } | ||||||
|
|
||||||
| logger = get_logger(__name__) | ||||||
|
|
@@ -78,6 +83,10 @@ | |||||
| JSONC_QUERY = """(comment) @comment""" | ||||||
| # @Bash comment query for tree-sitter, IMPL_BASH_3, impl, [FE_BASH] | ||||||
| BASH_QUERY = """(comment) @comment""" | ||||||
| # @Markdown HTML-comment query for tree-sitter, IMPL_MD_3, impl, [FE_MARKDOWN] | ||||||
| # Captures block-level HTML nodes (<!-- … -->) as @comment. Inline HTML comments | ||||||
| # inside paragraphs are not captured — only standalone html_block elements. | ||||||
| MARKDOWN_QUERY = """(html_block) @comment""" | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
<details>
<summary>@need-ids: FAKE_1</summary>
body
</details>
A
Suggested change
|
||||||
|
|
||||||
| # JSON value node types that can be associated with a comment. | ||||||
| JSON_STRUCTURE_TYPES = { | ||||||
|
|
@@ -107,7 +116,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, FE_BASH] | ||||||
| # @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, FE_MARKDOWN] | ||||||
| def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]: | ||||||
| if comment_type == CommentType.cpp: | ||||||
| import tree_sitter_cpp # noqa: PLC0415 | ||||||
|
|
@@ -149,6 +158,11 @@ def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]: | |||||
|
|
||||||
| parsed_language = Language(tree_sitter_bash.language()) | ||||||
| query = Query(parsed_language, BASH_QUERY) | ||||||
| elif comment_type == CommentType.markdown: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ninth branch on a chain that a table would replace. Adding a language now means touching five places: A single registry keeps the per-language knowledge in one row and makes "no scope types" an explicit value rather than a missing key: LANGUAGE_REGISTRY = {
CommentType.markdown: ("tree_sitter_markdown", MARKDOWN_QUERY, frozenset()),
...
}with |
||||||
| import tree_sitter_markdown # noqa: PLC0415 | ||||||
|
|
||||||
| parsed_language = Language(tree_sitter_markdown.language()) | ||||||
| query = Query(parsed_language, MARKDOWN_QUERY) | ||||||
| else: | ||||||
| raise ValueError(f"Unsupported comment style: {comment_type}") | ||||||
| parser = Parser(parsed_language) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,12 @@ | |
| # PyPI, so it cannot be wired in here. Track fish separately if a PyPI | ||
| # grammar becomes available. | ||
| "bash": ["sh", "bash", "zsh", "ksh"], | ||
| # Markdown uses block-level HTML comments `<!-- @needs … -->` as traceability | ||
| # markers. tree-sitter-markdown captures them as `html_block` nodes. | ||
| # NOTE: because the node text includes the `<!-- … -->` delimiters, callers | ||
| # must set `end_sequence: " -->"` (not the default `"\n"`) in their | ||
| # oneline_comment_style config to prevent `-->` from leaking into parsed fields. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mandating
if oneline_config.end_sequence == UNIX_NEWLINE and any(
char.isalnum() for char in oneline[:start_idx]
):
return NoneWith
Markdown files are the most prose-heavy input this project accepts and |
||
| "markdown": ["md", "markdown"], | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nothing enforces the A user who configures only
A comment in |
||
| } | ||
|
|
||
|
|
||
|
|
@@ -35,6 +41,8 @@ class CommentType(str, Enum): | |
| jsonc = "jsonc" | ||
| # @Support Bash style comments, IMPL_BASH_1, impl, [FE_BASH]; | ||
| bash = "bash" | ||
| # @Support Markdown HTML-comment style, IMPL_MD_1, impl, [FE_MARKDOWN] | ||
| markdown = "markdown" | ||
|
|
||
|
|
||
| class SourceDiscoverSectionConfigType(TypedDict, total=False): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "needs": [ | ||
| { | ||
| "id": "IMPL_MD", | ||
| "title": "Md Title", | ||
| "type": "impl", | ||
| "links": { | ||
| "links": [ | ||
| "REQ_MD" | ||
| ] | ||
| }, | ||
| "metadata": {}, | ||
| "line": 1 | ||
| } | ||
| ], | ||
| "need_refs": [], | ||
| "marked_rst": [], | ||
| "warnings": [] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,3 +57,10 @@ shebang_oneliner_bash: | |
| #!/bin/bash | ||
| # @Bash Title, IMPL_BASH_SHEBANG, impl, [REQ_BASH] | ||
| function greet { echo hi; } | ||
|
|
||
| default_oneliner_markdown: | ||
| lang: markdown | ||
| config: | ||
| end_sequence: " -->" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Same class of silent loss when two markers share a line: Worth a fixture for the no-space form so the sharp edge is at least pinned by a test. |
||
| source: | | ||
| <!-- @Md Title, IMPL_MD, impl, [REQ_MD] --> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Multi-line HTML comment blocks extract nothing under the mandated The natural way to declare several Markdown markers is one HTML comment holding several lines: <!--
@A Title, IMPL_A, impl, [R1]
@B Title, IMPL_B, impl, [R2]
-->
So the two Markdown comment forms are mutually exclusive per project config — one-line markers need |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |
| import tree_sitter_cpp | ||
| import tree_sitter_go | ||
| import tree_sitter_json | ||
| import tree_sitter_markdown | ||
| import tree_sitter_python | ||
| import tree_sitter_rust | ||
| import tree_sitter_yaml | ||
|
|
@@ -84,6 +85,53 @@ def init_bash_tree_sitter() -> tuple[Parser, Query]: | |
| return parser, query | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def init_markdown_tree_sitter() -> tuple[Parser, Query]: | ||
| parsed_language = Language(tree_sitter_markdown.language()) | ||
| query = Query(parsed_language, utils.MARKDOWN_QUERY) | ||
| parser = Parser(parsed_language) | ||
| return parser, query | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("code", "expected_count"), | ||
| [ | ||
| # standalone block HTML comment is captured | ||
| ( | ||
| b"<!-- @Md Title, IMPL_MD, impl, [REQ_MD] -->\n", | ||
| 1, | ||
| ), | ||
| # multiple HTML comment blocks are each captured | ||
| ( | ||
| b"<!-- @Md1, IMPL_MD_1, impl, [REQ_1] -->\n\nSome paragraph.\n\n<!-- @Md2, IMPL_MD_2, impl, [REQ_2] -->\n", | ||
| 2, | ||
| ), | ||
| # paragraph text without HTML comment produces no comments | ||
| ( | ||
| b"# Heading\n\nJust a paragraph with no markers.\n", | ||
| 0, | ||
| ), | ||
| ], | ||
| ) | ||
| def test_extract_comments_markdown(code, expected_count, init_markdown_tree_sitter): | ||
| parser, query = init_markdown_tree_sitter | ||
| comments = utils.extract_comments(code, parser, query) or [] | ||
| assert len(comments) == expected_count | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The added tests cover only the happy path; every sharp edge of this feature is untested. AGENTS.md: "Test coverage: Write tests for all new functionality and bug fixes." The four behaviours most likely to bite users all pass through untested:
Cases 2–4 are one-line additions to |
||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "code", | ||
| [ | ||
| b"<!-- @Md Title, IMPL_MD, impl, [REQ_MD] -->\n", | ||
| ], | ||
| ) | ||
| def test_init_tree_sitter_markdown(code): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nits in the new tests.
|
||
| """init_tree_sitter returns a working parser/query pair for markdown.""" | ||
| parser, query = utils.init_tree_sitter(CommentType.markdown) | ||
| comments = utils.extract_comments(code, parser, query) | ||
| assert len(comments) == 1 | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("code", "result"), | ||
| [ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This claim is inaccurate and understates the false-positive surface.
"Because tree-sitter-markdown exposes standalone HTML comments as
html_blocknodes, only block-level<!-- … -->markers are captured" —html_blockis not comment-specific.<div>,<details>,<table>,<script>and<br/>blocks are all captured and scanned for markers (verified). What's true is the narrower statement that only block-level HTML is captured; inline HTML is not.Either fix the query (see the comment on
MARKDOWN_QUERY) and keep this sentence, or state plainly that all block-level HTML is scanned so users understand why<details>sections can produce phantom need IDs.