From a30f021a525e041a8f93f194e08fc51445e2b1f9 Mon Sep 17 00:00:00 2001 From: Andraxion Date: Sat, 25 Jul 2026 22:51:56 -0400 Subject: [PATCH] Group source comments in logic views --- src/docforge/assets/graph.js | 6 +++- src/docforge/treesitter_logic.py | 49 +++++++++++++++++++++++++++++--- tests/test_treesitter_logic.py | 24 ++++++++++------ tests/test_visualization.py | 1 + 4 files changed, 67 insertions(+), 13 deletions(-) diff --git a/src/docforge/assets/graph.js b/src/docforge/assets/graph.js index 8cf3a15..00a029c 100644 --- a/src/docforge/assets/graph.js +++ b/src/docforge/assets/graph.js @@ -209,6 +209,9 @@ const contributionStyles = Object.freeze({ "logic-action": { label: "Action", section: "Actions & calls", color: "#34d399", fill: "#15372e", }, + "logic-comment": { + label: "Comment", section: "Source commentary", color: "#7dd3fc", fill: "#173447", + }, "logic-control": { label: "Control", section: "Loops & exception handling", color: "#c084fc", fill: "#302044", }, @@ -223,7 +226,7 @@ const contributionOrder = Object.freeze([ "focus", "composition", "behavior", "dependency", "execution", "data", "evidence", "context", "related", "logic-entry", "logic-condition", "logic-action", "logic-control", - "logic-convergence", "logic-terminal", + "logic-comment", "logic-convergence", "logic-terminal", ]); const compositionRelations = new Set(["contains", "defines", "defined_in"]); const behaviorRelations = new Set(["inherits", "implemented_by"]); @@ -939,6 +942,7 @@ function nodeContributionCategory(nodeId, data, topology) { if (kind === "entry") return "logic-entry"; if (["condition", "case"].includes(kind)) return "logic-condition"; if (["action", "call"].includes(kind)) return "logic-action"; + if (kind === "comment") return "logic-comment"; if (["loop", "try", "except", "finally", "break", "continue"].includes(kind)) { return "logic-control"; } diff --git a/src/docforge/treesitter_logic.py b/src/docforge/treesitter_logic.py index fec8443..41f981a 100644 --- a/src/docforge/treesitter_logic.py +++ b/src/docforge/treesitter_logic.py @@ -20,7 +20,7 @@ from tree_sitter import Language, Node, Parser from .errors import DocForgeError from .models import LogicEdge, LogicNode, LogicProjection -_TRIVIA_NODE_TYPES = frozenset({"comment"}) +_COMMENT_NODE_TYPES = frozenset({"comment"}) @dataclass(frozen=True) @@ -421,10 +421,29 @@ class _TreeSitterFunctionBuilder: control: _Control | None, ) -> tuple[_Tail, ...]: tails = incoming - for statement in statements: + items = tuple(statements) + index = 0 + while index < len(items): if not tails: break + statement = items[index] + if statement.type in _COMMENT_NODE_TYPES: + comments = [statement] + index += 1 + while index < len(items): + candidate = items[index] + previous = comments[-1] + if ( + candidate.type not in _COMMENT_NODE_TYPES + or candidate.start_point.row > previous.end_point.row + 1 + ): + break + comments.append(candidate) + index += 1 + tails = self._comment_block(tuple(comments), tails) + continue tails = self._statement(statement, tails, control=control) + index += 1 return tails def _statement( @@ -434,8 +453,8 @@ class _TreeSitterFunctionBuilder: *, control: _Control | None, ) -> tuple[_Tail, ...]: - if statement.type in _TRIVIA_NODE_TYPES: - return incoming + if statement.type in _COMMENT_NODE_TYPES: + return self._comment_block((statement,), incoming) if statement.type in self.profile.block_types: return self._statements(statement.named_children, incoming, control=control) if statement.type == "if_statement": @@ -493,6 +512,19 @@ class _TreeSitterFunctionBuilder: self._connect(incoming, node_id) return (_Tail(node_id),) + def _comment_block( + self, + comments: tuple[Node, ...], + incoming: tuple[_Tail, ...], + ) -> tuple[_Tail, ...]: + node_id = self._node( + "comment", + _compact(" ".join(_comment_text(comment, self.raw) for comment in comments), 480), + comments[0], + ) + self._connect(incoming, node_id) + return (_Tail(node_id),) + def _if( self, statement: Node, @@ -830,6 +862,15 @@ def _text(node: Node, raw: bytes) -> str: return raw[node.start_byte : node.end_byte].decode("utf-8", errors="replace") +def _comment_text(node: Node, raw: bytes) -> str: + value = _text(node, raw).strip() + if value.startswith("//"): + return value[2:].strip() + if value.startswith("/*") and value.endswith("*/"): + value = value[2:-2] + return " ".join(line.strip().removeprefix("*").strip() for line in value.splitlines()).strip() + + def _compact(value: str, limit: int = 240) -> str: compact = " ".join(value.strip().split()) return compact if len(compact) <= limit else f"{compact[: limit - 1]}…" diff --git a/tests/test_treesitter_logic.py b/tests/test_treesitter_logic.py index ad8c84f..51913b3 100644 --- a/tests/test_treesitter_logic.py +++ b/tests/test_treesitter_logic.py @@ -11,7 +11,7 @@ from docforge.treesitter_logic import ( class JavaScriptLogicTests(unittest.TestCase): - def test_comments_do_not_become_logic_actions(self) -> None: + def test_consecutive_comments_become_one_logic_comment_block(self) -> None: source = """ function choose(enabled) { // Explain the condition. @@ -28,9 +28,13 @@ function choose(enabled) { owners=(TreeSitterLogicOwner("js.symbol.choose", "choose", 1),), )[0] - labels = {node.label for node in projection.nodes} - self.assertFalse(any(label.startswith(("//", "/*")) for label in labels)) - self.assertIn("accept();", labels) + comments = [node for node in projection.nodes if node.kind == "comment"] + self.assertEqual(len(comments), 1) + self.assertEqual( + comments[0].label, + "Explain the condition. Continue the explanation. A block comment is trivia too.", + ) + self.assertIn("accept();", {node.label for node in projection.nodes}) def test_branches_short_circuit_and_converge(self) -> None: source = """ @@ -111,7 +115,7 @@ function process(items, mode) { class CppLogicTests(unittest.TestCase): - def test_comments_do_not_become_logic_actions(self) -> None: + def test_consecutive_comments_become_one_logic_comment_block(self) -> None: source = """ int choose(bool enabled) { // Explain the condition. @@ -128,9 +132,13 @@ int choose(bool enabled) { owners=(TreeSitterLogicOwner("cpp.symbol.choose", "choose", 1),), )[0] - labels = {node.label for node in projection.nodes} - self.assertFalse(any(label.startswith(("//", "/*")) for label in labels)) - self.assertIn("return 1", labels) + comments = [node for node in projection.nodes if node.kind == "comment"] + self.assertEqual(len(comments), 1) + self.assertEqual( + comments[0].label, + "Explain the condition. A block comment is trivia too.", + ) + self.assertIn("return 1", {node.label for node in projection.nodes}) def test_cpp_function_branches_and_throws(self) -> None: source = """ diff --git a/tests/test_visualization.py b/tests/test_visualization.py index 0080042..18cc73c 100644 --- a/tests/test_visualization.py +++ b/tests/test_visualization.py @@ -499,6 +499,7 @@ if (pruned.prunedCount !== 2) fail("pruned node count"); self.assertIn("Structure & containment", javascript) self.assertIn("Inherited & implemented behavior", javascript) self.assertIn("Required dependencies", javascript) + self.assertIn('"logic-comment"', javascript) self.assertNotIn(">Children<", html) self.assertIn("distanceShade", javascript) self.assertIn("nodeDisplayName", javascript)