argusf 0.1.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- argusf/__init__.py +3 -0
- argusf/analysis/__init__.py +14 -0
- argusf/analysis/engine.py +100 -0
- argusf/analysis/rule.py +45 -0
- argusf/analysis/rules/__init__.py +5 -0
- argusf/analysis/rules/documentation.py +30 -0
- argusf/analysis/rules/findings.py +67 -0
- argusf/analysis/rules/registry.py +22 -0
- argusf/analysis/rules/rulesets/__init__.py +4 -0
- argusf/analysis/rules/rulesets/rgus/__init__.py +22 -0
- argusf/analysis/rules/rulesets/rgus/rgus001_no_goto.py +54 -0
- argusf/analysis/rules/rulesets/rgus/rgus002_common_block.py +55 -0
- argusf/analysis/rules/rulesets/rgus/rgus003_equivalence.py +55 -0
- argusf/analysis/rules/rulesets/rgus/rgus004_implicit_typing.py +129 -0
- argusf/analysis/rules/rulesets/rgus/rgus005_arithmetic_if.py +62 -0
- argusf/analysis/rules/rulesets/rgus/rgus006_entry_statement.py +67 -0
- argusf/analysis/rules/rulesets/rgus/rgus007_pause_statement.py +93 -0
- argusf/analysis/rules/rulesets/rgus/rgus008_todo_comment.py +100 -0
- argusf/analysis/rules/rulesets/rgus/rgus009_unsorted_use_statements.py +258 -0
- argusf/analysis/rules/selection.py +83 -0
- argusf/analysis/suppression.py +92 -0
- argusf/analysis/syntax_errors.py +41 -0
- argusf/autofix/__init__.py +26 -0
- argusf/autofix/applier.py +130 -0
- argusf/autofix/context.py +82 -0
- argusf/autofix/diff.py +39 -0
- argusf/autofix/edits.py +31 -0
- argusf/autofix/engine.py +192 -0
- argusf/autofix/models.py +39 -0
- argusf/autofix/source.py +68 -0
- argusf/autofix/writer.py +53 -0
- argusf/cache/__init__.py +5 -0
- argusf/cache/backend.py +294 -0
- argusf/cli.py +428 -0
- argusf/config/__init__.py +1 -0
- argusf/config/config_resolver.py +212 -0
- argusf/config/errors.py +8 -0
- argusf/config/file_reader/__init__.py +3 -0
- argusf/config/file_reader/reader.py +58 -0
- argusf/config/models.py +110 -0
- argusf/config/validation.py +113 -0
- argusf/constants.py +66 -0
- argusf/diagnostics.py +45 -0
- argusf/discovery/__init__.py +3 -0
- argusf/discovery/backend.py +250 -0
- argusf/discovery/gitignore.py +125 -0
- argusf/discovery/repo.py +30 -0
- argusf/hashing/__init__.py +5 -0
- argusf/hashing/hash.py +48 -0
- argusf/ir/__init__.py +1 -0
- argusf/ir/models/__init__.py +49 -0
- argusf/ir/models/findings.py +74 -0
- argusf/ir/models/source.py +199 -0
- argusf/orchestrator.py +127 -0
- argusf/parser/__init__.py +1 -0
- argusf/parser/backend.py +24 -0
- argusf/parser/treesitter/__init__.py +1 -0
- argusf/parser/treesitter/backend.py +157 -0
- argusf/parser/treesitter/walker/__init__.py +5 -0
- argusf/parser/treesitter/walker/handlers.py +209 -0
- argusf/parser/treesitter/walker/walker.py +82 -0
- argusf/registry/__init__.py +3 -0
- argusf/registry/registry.py +69 -0
- argusf/reporting/__init__.py +22 -0
- argusf/reporting/formatting.py +162 -0
- argusf/reporting/registry.py +20 -0
- argusf/reporting/reporters/__init__.py +15 -0
- argusf/reporting/reporters/base.py +29 -0
- argusf/reporting/reporters/concise.py +35 -0
- argusf/reporting/reporters/diff.py +30 -0
- argusf/reporting/reporters/json.py +59 -0
- argusf/reporting/reporters/null.py +21 -0
- argusf/reporting/reporters/standard.py +78 -0
- argusf/reporting/reporters/statistics.py +99 -0
- argusf-0.1.0.dev0.dist-info/METADATA +166 -0
- argusf-0.1.0.dev0.dist-info/RECORD +79 -0
- argusf-0.1.0.dev0.dist-info/WHEEL +4 -0
- argusf-0.1.0.dev0.dist-info/entry_points.txt +3 -0
- argusf-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tree-sitter implementation of the parsing engine."""
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Tree-sitter parsing engine producing SourceFile IR."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import tree_sitter_fortran as tsfortran
|
|
6
|
+
from tree_sitter import Language, Node, Parser, Tree
|
|
7
|
+
|
|
8
|
+
from argusf.ir.models import Comment, ProgramUnit, SourceFile, SourceFileMetadata, SourceSpan
|
|
9
|
+
from argusf.parser.backend import ParseError, ParsingEngine
|
|
10
|
+
|
|
11
|
+
from .walker import TreeSitterNodeParserRegistry, TreeSitterNodeType, get_node_span, walk
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from argusf.diagnostics import Diagnostics
|
|
17
|
+
|
|
18
|
+
FORTRAN_LANGUAGE = Language(tsfortran.language())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TreesitterParsingEngine(ParsingEngine):
|
|
22
|
+
"""Parses Fortran source into IR via tree-sitter."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, diagnostics: Diagnostics) -> None:
|
|
25
|
+
"""Build the engine with a diagnostics channel.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
diagnostics: Channel for reporting skipped top-level
|
|
29
|
+
nodes encountered during the walk.
|
|
30
|
+
"""
|
|
31
|
+
self._parser = Parser(FORTRAN_LANGUAGE)
|
|
32
|
+
self._diagnostics = diagnostics
|
|
33
|
+
|
|
34
|
+
def parse(self, file: SourceFileMetadata) -> SourceFile:
|
|
35
|
+
"""Read `file` from disk and parse it into a `SourceFile`.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
file: Identity metadata of the file to parse.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The parsed source file.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ParseError: If the file can no longer be read.
|
|
45
|
+
"""
|
|
46
|
+
# This re-read is an accepted TOCTOU window against the
|
|
47
|
+
# discovery hash (metadata is deliberately lean, so the bytes
|
|
48
|
+
# don't travel with it): a file modified mid-run is parsed in
|
|
49
|
+
# its newer form. Harm is bounded — the fix stage re-verifies
|
|
50
|
+
# the content hash before editing, and a cache entry keyed to
|
|
51
|
+
# the stale identity fails validation next run, forcing
|
|
52
|
+
# re-analysis.
|
|
53
|
+
try:
|
|
54
|
+
source = file.file_path.read_bytes()
|
|
55
|
+
except OSError as error:
|
|
56
|
+
# Readable at discovery but not now (deleted, permissions
|
|
57
|
+
# flipped): surface through the normal parse-failure path.
|
|
58
|
+
raise ParseError(f"could not read {file.file_path}: {error}") from error
|
|
59
|
+
return self.parse_source(file.file_path, source)
|
|
60
|
+
|
|
61
|
+
def parse_source(self, path: Path, source: bytes) -> SourceFile:
|
|
62
|
+
"""Parse in-memory `source` into a `SourceFile`.
|
|
63
|
+
|
|
64
|
+
Collects syntax-error spans, walks the tree into IR program
|
|
65
|
+
units and comments, then orders the errors by document
|
|
66
|
+
position.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
path: Path recorded on the resulting SourceFile.
|
|
70
|
+
source: The exact bytes to parse.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The parsed source file.
|
|
74
|
+
"""
|
|
75
|
+
tree = self._parser.parse(source)
|
|
76
|
+
source_file = SourceFile(file_path=path, source=source)
|
|
77
|
+
source_file.syntax_errors = self._collect_error_spans(tree.root_node)
|
|
78
|
+
self._walk_tree(source_file, tree)
|
|
79
|
+
# Unit-level failures are appended during the walk, after the
|
|
80
|
+
# error-node spans, so restore document order.
|
|
81
|
+
source_file.syntax_errors.sort(key=lambda span: span.start_byte)
|
|
82
|
+
return source_file
|
|
83
|
+
|
|
84
|
+
def _collect_error_spans(self, root: Node) -> list[SourceSpan]:
|
|
85
|
+
"""Collect one span per unparseable region of the tree.
|
|
86
|
+
|
|
87
|
+
Covers ERROR nodes (tree-sitter's error recovery wraps
|
|
88
|
+
unrecognised input in them) and missing nodes (zero-width
|
|
89
|
+
tokens inserted to complete a rule, e.g. a lost `end`). Descent
|
|
90
|
+
is pruned to error-carrying subtrees, so clean files pay a
|
|
91
|
+
single flag check and error regions are never descended into
|
|
92
|
+
(nested errors collapse into the enclosing region's span).
|
|
93
|
+
"""
|
|
94
|
+
if not root.has_error:
|
|
95
|
+
return []
|
|
96
|
+
if root.is_error:
|
|
97
|
+
return [get_node_span(root)]
|
|
98
|
+
|
|
99
|
+
spans: list[SourceSpan] = []
|
|
100
|
+
stack = [root]
|
|
101
|
+
while stack:
|
|
102
|
+
node = stack.pop()
|
|
103
|
+
for child in reversed(node.children):
|
|
104
|
+
if child.is_error or child.is_missing:
|
|
105
|
+
spans.append(get_node_span(child))
|
|
106
|
+
elif child.has_error:
|
|
107
|
+
stack.append(child)
|
|
108
|
+
return spans
|
|
109
|
+
|
|
110
|
+
def _walk_tree(self, source_file: SourceFile, tree: Tree) -> None:
|
|
111
|
+
if tree.root_node.type != TreeSitterNodeType.TRANSLATION_UNIT:
|
|
112
|
+
# The grammar's root rule matches any input (including
|
|
113
|
+
# none), so this should be unreachable; the error walk has
|
|
114
|
+
# already recorded the whole file as a syntax error if it
|
|
115
|
+
# fires.
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
for child_node in tree.root_node.children:
|
|
119
|
+
# A failure inside one unit skips that unit only; the rest
|
|
120
|
+
# of the file still gets analysed (tree-sitter is
|
|
121
|
+
# error-tolerant, so one malformed region must not reject
|
|
122
|
+
# the whole file).
|
|
123
|
+
try:
|
|
124
|
+
result = walk(child_node)
|
|
125
|
+
except ParseError:
|
|
126
|
+
self._record_failed_unit(source_file, child_node)
|
|
127
|
+
continue
|
|
128
|
+
if isinstance(result, ProgramUnit):
|
|
129
|
+
source_file.add_program_unit(result)
|
|
130
|
+
continue
|
|
131
|
+
if isinstance(result, Comment):
|
|
132
|
+
source_file.add_comment(result)
|
|
133
|
+
continue
|
|
134
|
+
if not child_node.is_named:
|
|
135
|
+
continue
|
|
136
|
+
if result is None and TreeSitterNodeParserRegistry.get(child_node.type) is not None:
|
|
137
|
+
continue
|
|
138
|
+
if child_node.type == TreeSitterNodeType.ERROR:
|
|
139
|
+
continue # already recorded as a syntax error by the error walk
|
|
140
|
+
# A named node with no handler or an unexpected bare
|
|
141
|
+
# statement: surface what was dropped instead of vanishing
|
|
142
|
+
# silently.
|
|
143
|
+
self._diagnostics.emit(
|
|
144
|
+
f"skipped top-level node '{child_node.type}': {source_file.file_path}:{child_node.start_point.row + 1}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _record_failed_unit(self, source_file: SourceFile, node: Node) -> None:
|
|
148
|
+
"""Record a failed unit as one whole-unit syntax error.
|
|
149
|
+
|
|
150
|
+
Skipped when the error walk already recorded a region inside
|
|
151
|
+
the unit, in which case that span is the better (narrower)
|
|
152
|
+
report.
|
|
153
|
+
"""
|
|
154
|
+
for span in source_file.syntax_errors:
|
|
155
|
+
if node.start_byte <= span.start_byte and span.end_byte <= node.end_byte:
|
|
156
|
+
return
|
|
157
|
+
source_file.syntax_errors.append(get_node_span(node))
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from argusf.ir.models import (
|
|
4
|
+
ArithmeticIfStatement,
|
|
5
|
+
BlockData,
|
|
6
|
+
Comment,
|
|
7
|
+
CommonStatement,
|
|
8
|
+
EntryStatement,
|
|
9
|
+
EquivalenceStatement,
|
|
10
|
+
Function,
|
|
11
|
+
GotoStatement,
|
|
12
|
+
ImplicitStatement,
|
|
13
|
+
Module,
|
|
14
|
+
PauseStatement,
|
|
15
|
+
Program,
|
|
16
|
+
ProgramUnit,
|
|
17
|
+
Statement,
|
|
18
|
+
Submodule,
|
|
19
|
+
Subroutine,
|
|
20
|
+
UseStatement,
|
|
21
|
+
)
|
|
22
|
+
from argusf.parser.backend import ParseError
|
|
23
|
+
|
|
24
|
+
from .walker import TreeSitterNodeParserRegistry, TreeSitterNodeType, get_node_span, walk
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from tree_sitter import Node
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _collect_body(node: Node, unit: ProgramUnit) -> None:
|
|
31
|
+
"""Walk `node`'s children into `unit`'s statements and subunits.
|
|
32
|
+
|
|
33
|
+
Statements can sit arbitrarily deep inside a program unit (e.g. a
|
|
34
|
+
GOTO nested in an IF block), so for nodes with no registered handler
|
|
35
|
+
we keep recursing down their children looking for ones that do. Only
|
|
36
|
+
named children are walked: anonymous keyword tokens such as the
|
|
37
|
+
`program` in "END PROGRAM" share a node type with their container
|
|
38
|
+
and would otherwise be dispatched to the wrong handler. Contained
|
|
39
|
+
procedures (a `contains` section) walk to ProgramUnits and become
|
|
40
|
+
subunits of the enclosing unit. Interface blocks are never descended
|
|
41
|
+
into: their bodies are procedure *signatures* represented as full
|
|
42
|
+
subroutine/function nodes, which would otherwise become phantom
|
|
43
|
+
empty subunits.
|
|
44
|
+
"""
|
|
45
|
+
for child in node.named_children:
|
|
46
|
+
if child.type == TreeSitterNodeType.INTERFACE:
|
|
47
|
+
continue
|
|
48
|
+
result = walk(child)
|
|
49
|
+
if isinstance(result, ProgramUnit):
|
|
50
|
+
unit.add_subunit(result)
|
|
51
|
+
elif isinstance(result, Statement):
|
|
52
|
+
unit.add_statement(result)
|
|
53
|
+
else:
|
|
54
|
+
_collect_body(child, unit)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _extract_unit_name(node: Node, name_statement_type: TreeSitterNodeType) -> str | None:
|
|
58
|
+
"""Extract a unit's declared name, or None if anonymous.
|
|
59
|
+
|
|
60
|
+
The unit's *_statement child holds its name as a direct child of
|
|
61
|
+
node type `name`. Position is not reliable: function_statement puts
|
|
62
|
+
prefix tokens (purity, result type) before the name, and
|
|
63
|
+
submodule_statement wraps the parent module's name in a
|
|
64
|
+
`module_name` node before the submodule's own. The statement is
|
|
65
|
+
metadata for the enclosing unit rather than an IR node, so it is
|
|
66
|
+
extracted here instead of through a registered handler. Returns None
|
|
67
|
+
for anonymous units (a main program without a PROGRAM statement has
|
|
68
|
+
no program_statement child at all).
|
|
69
|
+
"""
|
|
70
|
+
for child in node.named_children:
|
|
71
|
+
if child.type != name_statement_type:
|
|
72
|
+
continue
|
|
73
|
+
for statement_child in child.named_children:
|
|
74
|
+
if statement_child.type == TreeSitterNodeType.NAME and statement_child.text is not None:
|
|
75
|
+
return statement_child.text.decode()
|
|
76
|
+
return None
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _parse_program_unit[UnitT: ProgramUnit](
|
|
81
|
+
node: Node, name_statement_type: TreeSitterNodeType, unit_cls: type[UnitT]
|
|
82
|
+
) -> UnitT:
|
|
83
|
+
if not node.children:
|
|
84
|
+
raise ParseError(f"{unit_cls.__name__} node at {node.start_point} has no children")
|
|
85
|
+
|
|
86
|
+
name = _extract_unit_name(node, name_statement_type)
|
|
87
|
+
unit = unit_cls(name=name, span=get_node_span(node))
|
|
88
|
+
_collect_body(node, unit)
|
|
89
|
+
return unit
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.PROGRAM)
|
|
93
|
+
def parse_program(node: Node) -> Program:
|
|
94
|
+
return _parse_program_unit(node, TreeSitterNodeType.PROGRAM_STATEMENT, Program)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.MODULE)
|
|
98
|
+
def parse_module(node: Node) -> Module:
|
|
99
|
+
return _parse_program_unit(node, TreeSitterNodeType.MODULE_STATEMENT, Module)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.SUBROUTINE)
|
|
103
|
+
def parse_subroutine(node: Node) -> Subroutine:
|
|
104
|
+
return _parse_program_unit(node, TreeSitterNodeType.SUBROUTINE_STATEMENT, Subroutine)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.FUNCTION)
|
|
108
|
+
def parse_function(node: Node) -> Function:
|
|
109
|
+
return _parse_program_unit(node, TreeSitterNodeType.FUNCTION_STATEMENT, Function)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.SUBMODULE)
|
|
113
|
+
def parse_submodule(node: Node) -> Submodule:
|
|
114
|
+
return _parse_program_unit(node, TreeSitterNodeType.SUBMODULE_STATEMENT, Submodule)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.BLOCK_DATA)
|
|
118
|
+
def parse_block_data(node: Node) -> BlockData:
|
|
119
|
+
return _parse_program_unit(node, TreeSitterNodeType.BLOCK_DATA_STATEMENT, BlockData)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _leading_keyword(node: Node) -> str:
|
|
123
|
+
"""Return the type of a node's leading keyword child.
|
|
124
|
+
|
|
125
|
+
Several grammar node types bundle multiple Fortran statements under
|
|
126
|
+
one node, discriminated by the leading keyword child (GOTO under
|
|
127
|
+
keyword_statement, PAUSE under file_position_statement). Raises if
|
|
128
|
+
the node is unexpectedly childless.
|
|
129
|
+
"""
|
|
130
|
+
keyword_node = node.child(0)
|
|
131
|
+
if keyword_node is None:
|
|
132
|
+
raise ParseError(f"Statement at {node.start_point} has no keyword node")
|
|
133
|
+
|
|
134
|
+
return keyword_node.type
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# GOTO has no dedicated node type; the grammar groups it with other
|
|
138
|
+
# simple statements (CONTINUE, RETURN, STOP, ...) under
|
|
139
|
+
# keyword_statement, and the leading keyword child tells them apart.
|
|
140
|
+
# "GOTO 10" yields a single `goto` child while "GO TO 10" yields a `go`
|
|
141
|
+
# child followed by a `to` child.
|
|
142
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.KEYWORD_STATEMENT)
|
|
143
|
+
def parse_keyword_statement(node: Node) -> GotoStatement | None:
|
|
144
|
+
if _leading_keyword(node) in (TreeSitterNodeType.GOTO, TreeSitterNodeType.GO):
|
|
145
|
+
return GotoStatement(span=get_node_span(node))
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.COMMON_STATEMENT)
|
|
150
|
+
def parse_common_statement(node: Node) -> CommonStatement:
|
|
151
|
+
return CommonStatement(span=get_node_span(node))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.EQUIVALENCE_STATEMENT)
|
|
155
|
+
def parse_equivalence_statement(node: Node) -> EquivalenceStatement:
|
|
156
|
+
return EquivalenceStatement(span=get_node_span(node))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.ARITHMETIC_IF_STATEMENT)
|
|
160
|
+
def parse_arithmetic_if_statement(node: Node) -> ArithmeticIfStatement:
|
|
161
|
+
return ArithmeticIfStatement(span=get_node_span(node))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.ENTRY_STATEMENT)
|
|
165
|
+
def parse_entry_statement(node: Node) -> EntryStatement:
|
|
166
|
+
return EntryStatement(span=get_node_span(node))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# IMPLICIT NONE and implicit typing rules (IMPLICIT REAL(A-H)) share the
|
|
170
|
+
# implicit_statement node type; a named `none` child tells them apart. A
|
|
171
|
+
# parenthesised spec list after NONE narrows what it disables: bare NONE
|
|
172
|
+
# and NONE (TYPE, ...) disable implicit typing, NONE (EXTERNAL) alone
|
|
173
|
+
# does not (it only forbids implicit procedure interfaces). The spec
|
|
174
|
+
# keywords are anonymous tokens, so all children are scanned, not just
|
|
175
|
+
# named ones.
|
|
176
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.IMPLICIT_STATEMENT)
|
|
177
|
+
def parse_implicit_statement(node: Node) -> ImplicitStatement:
|
|
178
|
+
has_none = any(child.type == TreeSitterNodeType.NONE for child in node.named_children)
|
|
179
|
+
has_spec_list = any(child.type == "(" for child in node.children)
|
|
180
|
+
has_type_spec = any(child.type == TreeSitterNodeType.TYPE for child in node.children)
|
|
181
|
+
is_none = has_none and (not has_spec_list or has_type_spec)
|
|
182
|
+
return ImplicitStatement(span=get_node_span(node), is_none=is_none)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# The module name is a dedicated module_name child; submodule extensions
|
|
186
|
+
# (`use, non_intrinsic :: m`) and ONLY clauses are sibling nodes the
|
|
187
|
+
# span already covers, so only the name needs extracting.
|
|
188
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.USE_STATEMENT)
|
|
189
|
+
def parse_use_statement(node: Node) -> UseStatement:
|
|
190
|
+
for child in node.named_children:
|
|
191
|
+
if child.type == TreeSitterNodeType.MODULE_NAME and child.text is not None:
|
|
192
|
+
return UseStatement(span=get_node_span(node), module_name=child.text.decode())
|
|
193
|
+
raise ParseError(f"Use statement at {node.start_point} has no module name")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# PAUSE has no dedicated node type; the grammar groups it with REWIND,
|
|
197
|
+
# BACKSPACE and ENDFILE under file_position_statement, and the leading
|
|
198
|
+
# keyword child tells them apart (same trick as GOTO above).
|
|
199
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.FILE_POSITION_STATEMENT)
|
|
200
|
+
def parse_file_position_statement(node: Node) -> PauseStatement | None:
|
|
201
|
+
if _leading_keyword(node) == TreeSitterNodeType.PAUSE:
|
|
202
|
+
return PauseStatement(span=get_node_span(node))
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@TreeSitterNodeParserRegistry.register(TreeSitterNodeType.COMMENT)
|
|
207
|
+
def parse_comment(node: Node) -> Comment:
|
|
208
|
+
text = node.text.decode(errors="replace") if node.text is not None else ""
|
|
209
|
+
return Comment(span=get_node_span(node), text=text)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Tree-sitter node walking: registry, node types, and span helper."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from argusf.ir.models import ProgramUnit, SourceSpan, Statement
|
|
7
|
+
from argusf.registry import Registry
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
from tree_sitter import Node
|
|
13
|
+
|
|
14
|
+
TreeSitterNodeParserRegistry: Registry[str, Callable[[Node], ProgramUnit | Statement | None]] = Registry()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_node_span(node: Node) -> SourceSpan:
|
|
18
|
+
"""Return the source span of a tree-sitter `node`."""
|
|
19
|
+
start_row, start_col = node.start_point
|
|
20
|
+
end_row, end_col = node.end_point
|
|
21
|
+
|
|
22
|
+
return SourceSpan(
|
|
23
|
+
start_row=start_row,
|
|
24
|
+
start_col=start_col,
|
|
25
|
+
end_row=end_row,
|
|
26
|
+
end_col=end_col,
|
|
27
|
+
start_byte=node.start_byte,
|
|
28
|
+
end_byte=node.end_byte,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TreeSitterNodeType(StrEnum):
|
|
33
|
+
"""Tree-sitter node type names used by the parser handlers."""
|
|
34
|
+
|
|
35
|
+
ARITHMETIC_IF_STATEMENT = "arithmetic_if_statement"
|
|
36
|
+
BLOCK_DATA = "block_data"
|
|
37
|
+
BLOCK_DATA_STATEMENT = "block_data_statement"
|
|
38
|
+
COMMENT = "comment"
|
|
39
|
+
COMMON_STATEMENT = "common_statement"
|
|
40
|
+
ENTRY_STATEMENT = "entry_statement"
|
|
41
|
+
EQUIVALENCE_STATEMENT = "equivalence_statement"
|
|
42
|
+
ERROR = "ERROR"
|
|
43
|
+
FILE_POSITION_STATEMENT = "file_position_statement"
|
|
44
|
+
FUNCTION = "function"
|
|
45
|
+
FUNCTION_STATEMENT = "function_statement"
|
|
46
|
+
GO = "go"
|
|
47
|
+
GOTO = "goto"
|
|
48
|
+
IMPLICIT_STATEMENT = "implicit_statement"
|
|
49
|
+
INTERFACE = "interface"
|
|
50
|
+
KEYWORD_STATEMENT = "keyword_statement"
|
|
51
|
+
MODULE = "module"
|
|
52
|
+
MODULE_NAME = "module_name"
|
|
53
|
+
MODULE_STATEMENT = "module_statement"
|
|
54
|
+
NAME = "name"
|
|
55
|
+
NONE = "none"
|
|
56
|
+
PAUSE = "pause"
|
|
57
|
+
PROGRAM = "program"
|
|
58
|
+
PROGRAM_STATEMENT = "program_statement"
|
|
59
|
+
SUBMODULE = "submodule"
|
|
60
|
+
SUBMODULE_STATEMENT = "submodule_statement"
|
|
61
|
+
SUBROUTINE = "subroutine"
|
|
62
|
+
SUBROUTINE_STATEMENT = "subroutine_statement"
|
|
63
|
+
TRANSLATION_UNIT = "translation_unit"
|
|
64
|
+
TYPE = "type"
|
|
65
|
+
USE_STATEMENT = "use_statement"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def walk(node: Node) -> ProgramUnit | Statement | None:
|
|
69
|
+
"""Dispatch `node` to its registered handler, if any.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
node: Tree-sitter node to parse.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The handler's IR result, or None when no handler is
|
|
76
|
+
registered for the node type.
|
|
77
|
+
"""
|
|
78
|
+
handler = TreeSitterNodeParserRegistry.get(node.type)
|
|
79
|
+
if handler is None:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
return handler(node)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""A generic registry mapping keys to registered values."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from collections.abc import Callable, KeysView, ValuesView
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Registry[Key, Value]:
|
|
10
|
+
"""Maps keys to values via a `@register`-style decorator.
|
|
11
|
+
|
|
12
|
+
The base of the plugin registries (rules, reporters, config
|
|
13
|
+
readers, parser handlers): subclasses instantiate it with their
|
|
14
|
+
own key/value types and share this registration and lookup
|
|
15
|
+
surface.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
"""Create an empty registry."""
|
|
20
|
+
self._store: dict[Key, Value] = {}
|
|
21
|
+
|
|
22
|
+
def register(self, key: Key) -> Callable[[Value], Value]:
|
|
23
|
+
"""Return a decorator that registers a value under `key`.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
key: Key to register the decorated value under.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A decorator that stores the value and returns it
|
|
30
|
+
unchanged.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If `key` is already registered.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def decorator(func: Value) -> Value:
|
|
37
|
+
if key in self._store:
|
|
38
|
+
raise ValueError(f"Key {key} is already registered.")
|
|
39
|
+
|
|
40
|
+
self._store[key] = func
|
|
41
|
+
return func
|
|
42
|
+
|
|
43
|
+
return decorator
|
|
44
|
+
|
|
45
|
+
def get(self, key: Key) -> Value | None:
|
|
46
|
+
"""Return the value registered under `key`, or None.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
key: Key to look up.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The registered value, or None when `key` is unknown.
|
|
53
|
+
"""
|
|
54
|
+
if key in self._store:
|
|
55
|
+
return self._store[key]
|
|
56
|
+
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def keys(self) -> KeysView[Key]:
|
|
60
|
+
"""Return a view of the registered keys."""
|
|
61
|
+
return self._store.keys()
|
|
62
|
+
|
|
63
|
+
def values(self) -> ValuesView[Value]:
|
|
64
|
+
"""Return a view of the registered values."""
|
|
65
|
+
return self._store.values()
|
|
66
|
+
|
|
67
|
+
def __contains__(self, key: Key) -> bool:
|
|
68
|
+
"""Return whether `key` is registered."""
|
|
69
|
+
return key in self._store
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Reporters that render a run's findings."""
|
|
2
|
+
|
|
3
|
+
from .registry import Reporter, ReporterRegistry
|
|
4
|
+
from .reporters import (
|
|
5
|
+
ConciseReporter,
|
|
6
|
+
DiffReporter,
|
|
7
|
+
JsonReporter,
|
|
8
|
+
NullReporter,
|
|
9
|
+
StandardReporter,
|
|
10
|
+
StatisticsReporter,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ConciseReporter",
|
|
15
|
+
"DiffReporter",
|
|
16
|
+
"JsonReporter",
|
|
17
|
+
"NullReporter",
|
|
18
|
+
"Reporter",
|
|
19
|
+
"ReporterRegistry",
|
|
20
|
+
"StandardReporter",
|
|
21
|
+
"StatisticsReporter",
|
|
22
|
+
]
|