From b37685b9fcd1feeb28b8f5006424633b1d2db237 Mon Sep 17 00:00:00 2001 From: Tomas Fonseca Date: Sat, 23 Aug 2025 12:32:34 +0200 Subject: [PATCH 1/6] feat: Find comments associated with symbol function. --- src/malls/ts/utils.py | 65 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/malls/ts/utils.py b/src/malls/ts/utils.py index a1f5aea..3cf7a4c 100644 --- a/src/malls/ts/utils.py +++ b/src/malls/ts/utils.py @@ -105,6 +105,14 @@ ) +COMMENTS_QUERY = Query( + MAL_LANGUAGE, + """ + ((comment) @comment_node)* + """, +) + + def find_variable_query(variable_name: str): query = Query( MAL_LANGUAGE, @@ -1335,3 +1343,60 @@ def find_meta_comment_function( # terminate if there are no more parents if node is None: return [] + + +def find_comments_function( + node: Node, symbol: str, document_uri: str = "", storage: dict = {} +) -> list: + """ + Given a node and a symbol, this function will find the comments + associated to that symbol. Comments are considered to be associated + with a symbol if they appear in consecutive lines above the symbol. + + E.g.: + // but not this + + // this as well + // this comment is connected + let myVar = ... + // technically this *could* be connected to above but its a potentially complex scenario + // so only count it towards the node below, if there is one + + + The easiest way to do this is to find all comments in the file and only keep those + which appear in consecutive lines above the symbol. + """ + + start_row = node.start_point.row + + # find comments + captures = run_query(storage[document_uri].tree.root_node, COMMENTS_QUERY) + if not captures: + return [] # there are no comments + + # sort captures by row + sorted_comments = sorted(captures["comment_node"], key=lambda item: item.start_point.row) + + comments = [sorted_comments[0].text] + previous_row = sorted_comments[0].end_point.row + + for comment_node in sorted_comments[1:]: + current_row = comment_node.start_point.row + + # if we have exceeded the row the symbol is in, + # we return what we have (as long as they are in + # consecutive rows) + if current_row > start_row: + break + + # if the comment is in a consecutive row, + # we keep it + if current_row == previous_row + 1: + comments.append(comment_node.text) + previous_row = current_row # update row + else: + # otherwise, restart the count + comments = [comment_node.text] + previous_row = comment_node.end_point.row + + return comments if previous_row == start_row - 1 else [] From ddc57c6b38be96803edff33651ac576cb74af5ca Mon Sep 17 00:00:00 2001 From: Tomas Fonseca Date: Sat, 23 Aug 2025 12:33:30 +0200 Subject: [PATCH 2/6] tests: Find comments associated with symbol function. --- .../mal/find_comments_for_symbol_function.mal | 37 ++++++++++++ .../test_find_comments_for_symbol_function.py | 59 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 tests/fixtures/mal/find_comments_for_symbol_function.mal create mode 100644 tests/unit/test_find_comments_for_symbol_function.py diff --git a/tests/fixtures/mal/find_comments_for_symbol_function.mal b/tests/fixtures/mal/find_comments_for_symbol_function.mal new file mode 100644 index 0000000..4e13194 --- /dev/null +++ b/tests/fixtures/mal/find_comments_for_symbol_function.mal @@ -0,0 +1,37 @@ +#id: "org.mal-lang.testAnalyzer" +#version:"0.0.0" + +category Example { + + // asset comment 1 + // asset comment 2 + asset Asset1 + { + | compromise + -> b.compromise + } + + /* + * MULTI-LINE COMMENT + */ + // followed by single comment + asset Asset2 + { + | compromise + // attack_step comment + & attack + } + + // asset3 comment + asset + Asset3 { } + + asset + // asset4 comment + Asset4 { } +} +associations +{ + // association comment + Asset1 [a] * <-- L --> * [c] Asset2 +} diff --git a/tests/unit/test_find_comments_for_symbol_function.py b/tests/unit/test_find_comments_for_symbol_function.py new file mode 100644 index 0000000..f2aeca2 --- /dev/null +++ b/tests/unit/test_find_comments_for_symbol_function.py @@ -0,0 +1,59 @@ +from pathlib import Path + +import pytest +import tree_sitter_mal as ts_mal +from tree_sitter import Language, Parser + +from malls.lsp.classes import Document +from malls.lsp.utils import recursive_parsing +from malls.ts.utils import INCLUDED_FILES_QUERY, find_comments_function, run_query + +MAL_LANGUAGE = Language(ts_mal.language()) +PARSER = Parser(MAL_LANGUAGE) +FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" + +parameters = [ + ((7, 12), [b"// asset comment 1", b"// asset comment 2"]), + ((17, 12), [b"/* \n * MULTI-LINE COMMENT\n */", b"// followed by single comment"]), + ((21, 12), [b"// attack_step comment"]), + ((26, 8), []), + ((30, 6), [b"// asset4 comment"]), + ((35, 21), [b"// association comment"]), +] + + +@pytest.mark.parametrize( + "point,comments", + parameters, +) +def test_find_comments_for_symbol_function(mal_find_comments_for_symbol_function, point, comments): + # build the storage (mimicks the file parsing in the server) + storage = {} + + doc_uri = FILE_PATH + "find_comments_for_symbol_function.mal" + source_encoded = mal_find_comments_for_symbol_function.read() + tree = PARSER.parse(source_encoded) + + storage[doc_uri] = Document(tree, source_encoded, doc_uri) + + # obtain the included files + root_node = tree.root_node + + captures = run_query(root_node, INCLUDED_FILES_QUERY) + if "file_name" in captures: + recursive_parsing(FILE_PATH, captures["file_name"], storage, doc_uri, []) + + ################################### + + # get the node + cursor = tree.walk() + while cursor.goto_first_child_for_point(point) is not None: + continue + + # confirm it's an identifier + assert cursor.node.type == "identifier" + + # we use sets to ensure order does not matter + returned_comments = find_comments_function(cursor.node, cursor.node.text, doc_uri, storage) + + assert set(returned_comments) == set(comments) From 19172e535d7adeaa97901ae3d30dcff6004169a2 Mon Sep 17 00:00:00 2001 From: Tomas Fonseca Date: Mon, 25 Aug 2025 15:55:40 +0200 Subject: [PATCH 3/6] chore: Filter comments based on rows. --- src/malls/ts/utils.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/malls/ts/utils.py b/src/malls/ts/utils.py index 3cf7a4c..c9541c3 100644 --- a/src/malls/ts/utils.py +++ b/src/malls/ts/utils.py @@ -1375,7 +1375,9 @@ def find_comments_function( return [] # there are no comments # sort captures by row - sorted_comments = sorted(captures["comment_node"], key=lambda item: item.start_point.row) + sorted_comments = sorted([ + item for item in captures["comment_node"] if item.start_point.row < start_row + ], key=lambda item: item.start_point.row) comments = [sorted_comments[0].text] previous_row = sorted_comments[0].end_point.row @@ -1383,12 +1385,6 @@ def find_comments_function( for comment_node in sorted_comments[1:]: current_row = comment_node.start_point.row - # if we have exceeded the row the symbol is in, - # we return what we have (as long as they are in - # consecutive rows) - if current_row > start_row: - break - # if the comment is in a consecutive row, # we keep it if current_row == previous_row + 1: From 09c156179cf3f84a6779f2810653daf57240bba6 Mon Sep 17 00:00:00 2001 From: Tomas Fonseca Date: Mon, 25 Aug 2025 16:05:18 +0200 Subject: [PATCH 4/6] tests: Negative test cases. --- .../mal/find_comments_for_symbol_function.mal | 20 +++++++++++++++++++ .../test_find_comments_for_symbol_function.py | 13 ++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/tests/fixtures/mal/find_comments_for_symbol_function.mal b/tests/fixtures/mal/find_comments_for_symbol_function.mal index 4e13194..f1c4fea 100644 --- a/tests/fixtures/mal/find_comments_for_symbol_function.mal +++ b/tests/fixtures/mal/find_comments_for_symbol_function.mal @@ -1,8 +1,16 @@ #id: "org.mal-lang.testAnalyzer" #version:"0.0.0" +// should not appear +/* + * SHOULD NOT APPEAR + */ + +// category comment category Example { + // should not appear + // asset comment 1 // asset comment 2 asset Asset1 @@ -11,6 +19,10 @@ category Example { -> b.compromise } + /* + * SHOULD NOT APPEAR + */ + /* * MULTI-LINE COMMENT */ @@ -22,6 +34,11 @@ category Example { & attack } + /* + * SHOULD NOT APPEAR + */ + // should not appear + // asset3 comment asset Asset3 { } @@ -32,6 +49,9 @@ category Example { } associations { + // should not appear + // should not appear + // association comment Asset1 [a] * <-- L --> * [c] Asset2 } diff --git a/tests/unit/test_find_comments_for_symbol_function.py b/tests/unit/test_find_comments_for_symbol_function.py index f2aeca2..f5b86fb 100644 --- a/tests/unit/test_find_comments_for_symbol_function.py +++ b/tests/unit/test_find_comments_for_symbol_function.py @@ -13,12 +13,13 @@ FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" parameters = [ - ((7, 12), [b"// asset comment 1", b"// asset comment 2"]), - ((17, 12), [b"/* \n * MULTI-LINE COMMENT\n */", b"// followed by single comment"]), - ((21, 12), [b"// attack_step comment"]), - ((26, 8), []), - ((30, 6), [b"// asset4 comment"]), - ((35, 21), [b"// association comment"]), + ((9, 13), [b"// category comment"]), + ((15, 12), [b"// asset comment 1", b"// asset comment 2"]), + ((29, 12), [b"/* \n * MULTI-LINE COMMENT\n */", b"// followed by single comment"]), + ((33, 12), [b"// attack_step comment"]), + ((43, 8), []), + ((47, 6), [b"// asset4 comment"]), + ((55, 21), [b"// association comment"]), ] From 41789a7523819dd7e978f6596755de3d9b5afd9b Mon Sep 17 00:00:00 2001 From: Tomas Fonseca Date: Mon, 25 Aug 2025 16:05:57 +0200 Subject: [PATCH 5/6] fix: Correct linter errors. --- src/malls/ts/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/malls/ts/utils.py b/src/malls/ts/utils.py index c9541c3..359a46c 100644 --- a/src/malls/ts/utils.py +++ b/src/malls/ts/utils.py @@ -1375,9 +1375,10 @@ def find_comments_function( return [] # there are no comments # sort captures by row - sorted_comments = sorted([ - item for item in captures["comment_node"] if item.start_point.row < start_row - ], key=lambda item: item.start_point.row) + sorted_comments = sorted( + [item for item in captures["comment_node"] if item.start_point.row < start_row], + key=lambda item: item.start_point.row, + ) comments = [sorted_comments[0].text] previous_row = sorted_comments[0].end_point.row From 60167429d7e18eb638b736a7b802bf296fd2b41b Mon Sep 17 00:00:00 2001 From: Tomas Fonseca Date: Wed, 27 Aug 2025 14:11:43 +0200 Subject: [PATCH 6/6] chore: Use filter function. --- src/malls/ts/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/malls/ts/utils.py b/src/malls/ts/utils.py index 359a46c..5969101 100644 --- a/src/malls/ts/utils.py +++ b/src/malls/ts/utils.py @@ -1376,7 +1376,7 @@ def find_comments_function( # sort captures by row sorted_comments = sorted( - [item for item in captures["comment_node"] if item.start_point.row < start_row], + filter(lambda item: item.start_point.row < start_row, captures["comment_node"]), key=lambda item: item.start_point.row, )