245 lines
7.3 KiB
Python
245 lines
7.3 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from docforge.errors import DocForgeError
|
|
from docforge.treesitter_logic import (
|
|
TreeSitterLogicOwner,
|
|
analyze_cpp_source,
|
|
analyze_javascript_source,
|
|
analyze_typescript_source,
|
|
discover_typescript_functions,
|
|
)
|
|
|
|
|
|
class JavaScriptLogicTests(unittest.TestCase):
|
|
def test_consecutive_comments_become_one_logic_comment_block(self) -> None:
|
|
source = """
|
|
function choose(enabled) {
|
|
// Explain the condition.
|
|
// Continue the explanation.
|
|
/* A block comment is trivia too. */
|
|
if (enabled) {
|
|
accept();
|
|
}
|
|
}
|
|
""".strip()
|
|
projection = analyze_javascript_source(
|
|
source,
|
|
source_id="source.javascript",
|
|
owners=(TreeSitterLogicOwner("js.symbol.choose", "choose", 1),),
|
|
)[0]
|
|
|
|
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 = """
|
|
function choose(enabled, ready) {
|
|
if (enabled && ready()) {
|
|
accept();
|
|
} else {
|
|
reject();
|
|
}
|
|
return enabled;
|
|
}
|
|
""".strip()
|
|
projection = analyze_javascript_source(
|
|
source,
|
|
source_id="source.javascript",
|
|
owners=(TreeSitterLogicOwner("js.symbol.choose", "choose", 1),),
|
|
)[0]
|
|
|
|
kinds = {node.kind for node in projection.nodes}
|
|
labels = {node.label for node in projection.nodes}
|
|
relations = {edge.relation for edge in projection.edges}
|
|
self.assertIn("condition", kinds)
|
|
self.assertIn("convergence", kinds)
|
|
self.assertIn("Decision convergence", labels)
|
|
self.assertIn("when_true", relations)
|
|
self.assertIn("when_false", relations)
|
|
self.assertIn("return", relations)
|
|
|
|
def test_arrow_function_expression_is_a_returning_projection(self) -> None:
|
|
source = "const choose = (enabled) => enabled ? accept() : reject();"
|
|
projection = analyze_javascript_source(
|
|
source,
|
|
source_id="source.javascript",
|
|
owners=(TreeSitterLogicOwner("js.symbol.choose", "choose", 1),),
|
|
)[0]
|
|
|
|
labels = {node.label for node in projection.nodes}
|
|
self.assertIn("enabled", labels)
|
|
self.assertIn("return accept()", labels)
|
|
self.assertIn("return reject()", labels)
|
|
|
|
def test_loops_switch_and_exception_paths_are_preserved(self) -> None:
|
|
source = """
|
|
function process(items, mode) {
|
|
for (const item of items) {
|
|
if (!item.ready) continue;
|
|
use(item);
|
|
}
|
|
switch (mode) {
|
|
case 1:
|
|
one();
|
|
break;
|
|
default:
|
|
fallback();
|
|
}
|
|
try {
|
|
risk();
|
|
} catch (error) {
|
|
recover(error);
|
|
} finally {
|
|
clean();
|
|
}
|
|
}
|
|
""".strip()
|
|
projection = analyze_javascript_source(
|
|
source,
|
|
source_id="source.javascript",
|
|
owners=(TreeSitterLogicOwner("js.symbol.process", "process", 1),),
|
|
)[0]
|
|
|
|
kinds = {node.kind for node in projection.nodes}
|
|
labels = {node.label for node in projection.nodes}
|
|
relations = {edge.relation for edge in projection.edges}
|
|
self.assertTrue({"loop", "continue", "case", "try", "except", "finally"} <= kinds)
|
|
self.assertIn("Case convergence", labels)
|
|
self.assertIn("Exception convergence", labels)
|
|
self.assertTrue({"loop", "continue", "case", "exception"} <= relations)
|
|
|
|
|
|
class CppLogicTests(unittest.TestCase):
|
|
def test_consecutive_comments_become_one_logic_comment_block(self) -> None:
|
|
source = """
|
|
int choose(bool enabled) {
|
|
// Explain the condition.
|
|
/* A block comment is trivia too. */
|
|
if (enabled) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
""".strip()
|
|
projection = analyze_cpp_source(
|
|
source,
|
|
source_id="source.cpp",
|
|
owners=(TreeSitterLogicOwner("cpp.symbol.choose", "choose", 1),),
|
|
)[0]
|
|
|
|
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 = """
|
|
int choose(bool enabled) {
|
|
if (enabled) {
|
|
return 1;
|
|
}
|
|
throw Error();
|
|
}
|
|
""".strip()
|
|
projection = analyze_cpp_source(
|
|
source,
|
|
source_id="source.cpp",
|
|
owners=(TreeSitterLogicOwner("cpp.symbol.choose", "choose", 1),),
|
|
)[0]
|
|
|
|
kinds = {node.kind for node in projection.nodes}
|
|
relations = {edge.relation for edge in projection.edges}
|
|
self.assertTrue({"entry", "condition", "return", "raise", "exit"} <= kinds)
|
|
self.assertTrue({"when_true", "when_false", "return", "raise"} <= relations)
|
|
|
|
def test_cpp_qualified_method_owner_is_resolved(self) -> None:
|
|
source = """
|
|
class Worker {
|
|
public:
|
|
int run(bool ready) {
|
|
while (ready) {
|
|
ready = tick();
|
|
}
|
|
return 0;
|
|
}
|
|
};
|
|
""".strip()
|
|
projection = analyze_cpp_source(
|
|
source,
|
|
source_id="source.cpp",
|
|
owners=(TreeSitterLogicOwner("cpp.symbol.worker.run", "Worker.run", 3),),
|
|
)[0]
|
|
|
|
labels = {node.label for node in projection.nodes}
|
|
self.assertIn("Loop exit", labels)
|
|
self.assertIn("return 0", labels)
|
|
|
|
def test_invalid_source_and_missing_owner_fail_closed(self) -> None:
|
|
with self.assertRaises(DocForgeError) as invalid:
|
|
analyze_cpp_source(
|
|
"int broken( {",
|
|
source_id="source.cpp",
|
|
owners=(),
|
|
)
|
|
self.assertEqual(invalid.exception.code, "invalid_logic_source")
|
|
|
|
with self.assertRaises(DocForgeError) as missing:
|
|
analyze_javascript_source(
|
|
"function exists() {}",
|
|
source_id="source.javascript",
|
|
owners=(TreeSitterLogicOwner("js.symbol.missing", "missing", 1),),
|
|
)
|
|
self.assertEqual(missing.exception.code, "missing_logic_owner")
|
|
|
|
|
|
class TypeScriptLogicTests(unittest.TestCase):
|
|
def test_typed_function_and_method_use_the_typescript_grammar(self) -> None:
|
|
source = """
|
|
interface Choice {
|
|
enabled: boolean;
|
|
}
|
|
|
|
function choose(choice: Choice): number {
|
|
if (choice.enabled) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
class Worker {
|
|
run(choice: Choice): number {
|
|
return choose(choice);
|
|
}
|
|
}
|
|
""".strip()
|
|
|
|
discovered = discover_typescript_functions(source)
|
|
self.assertEqual(
|
|
[("Worker.run", 13), ("choose", 5)],
|
|
[(item.qualified_name, item.line) for item in discovered],
|
|
)
|
|
projection = analyze_typescript_source(
|
|
source,
|
|
source_id="source.typescript",
|
|
owners=(TreeSitterLogicOwner("ts.symbol.choose", "choose", 5),),
|
|
)[0]
|
|
|
|
self.assertIn("choice.enabled", {node.label for node in projection.nodes})
|
|
self.assertEqual(
|
|
{"return"},
|
|
{edge.relation for edge in projection.edges if edge.relation == "return"},
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|