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.
Files changed (79) hide show
  1. argusf/__init__.py +3 -0
  2. argusf/analysis/__init__.py +14 -0
  3. argusf/analysis/engine.py +100 -0
  4. argusf/analysis/rule.py +45 -0
  5. argusf/analysis/rules/__init__.py +5 -0
  6. argusf/analysis/rules/documentation.py +30 -0
  7. argusf/analysis/rules/findings.py +67 -0
  8. argusf/analysis/rules/registry.py +22 -0
  9. argusf/analysis/rules/rulesets/__init__.py +4 -0
  10. argusf/analysis/rules/rulesets/rgus/__init__.py +22 -0
  11. argusf/analysis/rules/rulesets/rgus/rgus001_no_goto.py +54 -0
  12. argusf/analysis/rules/rulesets/rgus/rgus002_common_block.py +55 -0
  13. argusf/analysis/rules/rulesets/rgus/rgus003_equivalence.py +55 -0
  14. argusf/analysis/rules/rulesets/rgus/rgus004_implicit_typing.py +129 -0
  15. argusf/analysis/rules/rulesets/rgus/rgus005_arithmetic_if.py +62 -0
  16. argusf/analysis/rules/rulesets/rgus/rgus006_entry_statement.py +67 -0
  17. argusf/analysis/rules/rulesets/rgus/rgus007_pause_statement.py +93 -0
  18. argusf/analysis/rules/rulesets/rgus/rgus008_todo_comment.py +100 -0
  19. argusf/analysis/rules/rulesets/rgus/rgus009_unsorted_use_statements.py +258 -0
  20. argusf/analysis/rules/selection.py +83 -0
  21. argusf/analysis/suppression.py +92 -0
  22. argusf/analysis/syntax_errors.py +41 -0
  23. argusf/autofix/__init__.py +26 -0
  24. argusf/autofix/applier.py +130 -0
  25. argusf/autofix/context.py +82 -0
  26. argusf/autofix/diff.py +39 -0
  27. argusf/autofix/edits.py +31 -0
  28. argusf/autofix/engine.py +192 -0
  29. argusf/autofix/models.py +39 -0
  30. argusf/autofix/source.py +68 -0
  31. argusf/autofix/writer.py +53 -0
  32. argusf/cache/__init__.py +5 -0
  33. argusf/cache/backend.py +294 -0
  34. argusf/cli.py +428 -0
  35. argusf/config/__init__.py +1 -0
  36. argusf/config/config_resolver.py +212 -0
  37. argusf/config/errors.py +8 -0
  38. argusf/config/file_reader/__init__.py +3 -0
  39. argusf/config/file_reader/reader.py +58 -0
  40. argusf/config/models.py +110 -0
  41. argusf/config/validation.py +113 -0
  42. argusf/constants.py +66 -0
  43. argusf/diagnostics.py +45 -0
  44. argusf/discovery/__init__.py +3 -0
  45. argusf/discovery/backend.py +250 -0
  46. argusf/discovery/gitignore.py +125 -0
  47. argusf/discovery/repo.py +30 -0
  48. argusf/hashing/__init__.py +5 -0
  49. argusf/hashing/hash.py +48 -0
  50. argusf/ir/__init__.py +1 -0
  51. argusf/ir/models/__init__.py +49 -0
  52. argusf/ir/models/findings.py +74 -0
  53. argusf/ir/models/source.py +199 -0
  54. argusf/orchestrator.py +127 -0
  55. argusf/parser/__init__.py +1 -0
  56. argusf/parser/backend.py +24 -0
  57. argusf/parser/treesitter/__init__.py +1 -0
  58. argusf/parser/treesitter/backend.py +157 -0
  59. argusf/parser/treesitter/walker/__init__.py +5 -0
  60. argusf/parser/treesitter/walker/handlers.py +209 -0
  61. argusf/parser/treesitter/walker/walker.py +82 -0
  62. argusf/registry/__init__.py +3 -0
  63. argusf/registry/registry.py +69 -0
  64. argusf/reporting/__init__.py +22 -0
  65. argusf/reporting/formatting.py +162 -0
  66. argusf/reporting/registry.py +20 -0
  67. argusf/reporting/reporters/__init__.py +15 -0
  68. argusf/reporting/reporters/base.py +29 -0
  69. argusf/reporting/reporters/concise.py +35 -0
  70. argusf/reporting/reporters/diff.py +30 -0
  71. argusf/reporting/reporters/json.py +59 -0
  72. argusf/reporting/reporters/null.py +21 -0
  73. argusf/reporting/reporters/standard.py +78 -0
  74. argusf/reporting/reporters/statistics.py +99 -0
  75. argusf-0.1.0.dev0.dist-info/METADATA +166 -0
  76. argusf-0.1.0.dev0.dist-info/RECORD +79 -0
  77. argusf-0.1.0.dev0.dist-info/WHEEL +4 -0
  78. argusf-0.1.0.dev0.dist-info/entry_points.txt +3 -0
  79. 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,5 @@
1
+ # Import handlers to trigger registration with the registry.
2
+ from . import handlers
3
+ from .walker import TreeSitterNodeParserRegistry, TreeSitterNodeType, get_node_span, walk
4
+
5
+ __all__ = ["TreeSitterNodeParserRegistry", "TreeSitterNodeType", "get_node_span", "walk"]
@@ -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,3 @@
1
+ """The generic registry underpinning the plugin registries."""
2
+
3
+ from .registry import Registry as Registry
@@ -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
+ ]