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
argusf/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Rule-based analysis of parsed source into findings."""
|
|
2
|
+
|
|
3
|
+
from .engine import AnalysisEngine
|
|
4
|
+
from .rule import Rule, RuleMetadata
|
|
5
|
+
from .rules import RuleRegistry
|
|
6
|
+
from .suppression import SuppressionFilter
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AnalysisEngine",
|
|
10
|
+
"Rule",
|
|
11
|
+
"RuleMetadata",
|
|
12
|
+
"RuleRegistry",
|
|
13
|
+
"SuppressionFilter",
|
|
14
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""The analysis engine: runs rules over a parsed source file."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from argusf.analysis.rule import ConfigurableRule, Rule
|
|
7
|
+
from argusf.analysis.rules import RuleRegistry
|
|
8
|
+
from argusf.analysis.rules.selection import RuleSelectionResolver
|
|
9
|
+
from argusf.analysis.syntax_errors import syntax_error_finding
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from argusf.analysis.suppression import SuppressionFilter
|
|
13
|
+
from argusf.config.models import ArgusConfig
|
|
14
|
+
from argusf.ir.models import Finding, SourceFile
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AnalysisEngine:
|
|
20
|
+
"""Runs the configured rules over a source file."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, config: ArgusConfig, suppression_filter: SuppressionFilter) -> None:
|
|
23
|
+
"""Build the engine's rule set from config.
|
|
24
|
+
|
|
25
|
+
Constructs every registered rule, configures the ones that
|
|
26
|
+
read settings, filters by select/ignore and preview, and
|
|
27
|
+
resolves which rules may attach fixes.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
config: Resolved configuration; its lint section drives
|
|
31
|
+
rule selection and fixability.
|
|
32
|
+
suppression_filter: Filter applied to findings after the
|
|
33
|
+
rules run.
|
|
34
|
+
"""
|
|
35
|
+
self._config = config.lint
|
|
36
|
+
self._suppression_filter = suppression_filter
|
|
37
|
+
rules: list[Rule] = []
|
|
38
|
+
for rule_cls in RuleRegistry.values():
|
|
39
|
+
rule = rule_cls()
|
|
40
|
+
if isinstance(rule, ConfigurableRule):
|
|
41
|
+
rule.configure(self._config)
|
|
42
|
+
rules.append(rule)
|
|
43
|
+
self._rules = self._filter_rules(rules)
|
|
44
|
+
# A rule's fix survives only if the rule declares itself fixable
|
|
45
|
+
# (metadata is authoritative) AND the fixable/unfixable
|
|
46
|
+
# selectors resolve it in.
|
|
47
|
+
declared_fixable = {rule.metadata.id for rule in rules if rule.metadata.fixable}
|
|
48
|
+
self._fixable = declared_fixable & RuleSelectionResolver().resolve(
|
|
49
|
+
[*self._config.fixable, *self._config.extend_fixable], self._config.unfixable
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def analyse(self, source_file: SourceFile) -> list[Finding]:
|
|
53
|
+
"""Produce all findings for a parsed source file.
|
|
54
|
+
|
|
55
|
+
Seeds syntax-error findings from the parser, runs each enabled
|
|
56
|
+
rule, strips fixes from rules outside the fixable set, and
|
|
57
|
+
marks suppressed findings.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
source_file: The parsed file to analyse.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Findings in document order, some flagged suppressed.
|
|
64
|
+
"""
|
|
65
|
+
# Syntax errors precede rule findings; they come from the
|
|
66
|
+
# parser, not a rule, so rule selection never applies to them.
|
|
67
|
+
findings: list[Finding] = [
|
|
68
|
+
syntax_error_finding(source_file.file_path, span) for span in source_file.syntax_errors
|
|
69
|
+
]
|
|
70
|
+
for rule in self._rules:
|
|
71
|
+
rule_findings = rule.check(source_file)
|
|
72
|
+
if rule_findings:
|
|
73
|
+
logger.debug(
|
|
74
|
+
"%s: %d finding(s) from %s",
|
|
75
|
+
source_file.file_path,
|
|
76
|
+
len(rule_findings),
|
|
77
|
+
rule.metadata.id,
|
|
78
|
+
)
|
|
79
|
+
findings.extend(rule_findings)
|
|
80
|
+
|
|
81
|
+
# Strip fixes for rules outside the resolved fixable set before
|
|
82
|
+
# findings are cached or reported, so a finding's payload always
|
|
83
|
+
# matches what fixable/unfixable said at analysis time — an
|
|
84
|
+
# unfixable rule's findings carry no fix anywhere, including the
|
|
85
|
+
# JSON report's `fix: null`.
|
|
86
|
+
for finding in findings:
|
|
87
|
+
if finding.fix is not None and finding.rule_id not in self._fixable:
|
|
88
|
+
finding.fix = None
|
|
89
|
+
|
|
90
|
+
self._suppression_filter.apply(source_file, findings)
|
|
91
|
+
return findings
|
|
92
|
+
|
|
93
|
+
def _filter_rules(self, rules: list[Rule]) -> list[Rule]:
|
|
94
|
+
select = [*self._config.select, *self._config.extend_select]
|
|
95
|
+
enabled = RuleSelectionResolver().resolve(select, self._config.ignore)
|
|
96
|
+
return [
|
|
97
|
+
rule
|
|
98
|
+
for rule in rules
|
|
99
|
+
if rule.metadata.id in enabled and (self._config.preview or not rule.metadata.preview)
|
|
100
|
+
]
|
argusf/analysis/rule.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Rule protocols and the metadata attached to each rule."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from argusf.config.models import LintConfig
|
|
8
|
+
from argusf.ir.models import Finding, Severity, SourceFile
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class RuleMetadata:
|
|
13
|
+
"""Static metadata describing a rule."""
|
|
14
|
+
|
|
15
|
+
id: str
|
|
16
|
+
name: str
|
|
17
|
+
message: str
|
|
18
|
+
description: str
|
|
19
|
+
default_severity: Severity
|
|
20
|
+
suggestion: str | None = None
|
|
21
|
+
preview: bool = False
|
|
22
|
+
# The fixable-rule contract. A rule that offers fixes declares it
|
|
23
|
+
# here and must document the classification in a `## Fix safety`
|
|
24
|
+
# docstring section (a registry test enforces both directions). The
|
|
25
|
+
# declaration is authoritative: the engine strips fixes from any
|
|
26
|
+
# rule that does not make it, so an undeclared fix can never reach a
|
|
27
|
+
# report, the cache, or a file.
|
|
28
|
+
fixable: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Rule(Protocol):
|
|
32
|
+
"""A lint rule: metadata plus a check over a source file."""
|
|
33
|
+
|
|
34
|
+
metadata: RuleMetadata
|
|
35
|
+
|
|
36
|
+
def check(self, source_file: SourceFile) -> list[Finding]:
|
|
37
|
+
"""Return the findings this rule detects in `source_file`."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@runtime_checkable
|
|
41
|
+
class ConfigurableRule(Rule, Protocol):
|
|
42
|
+
"""A rule that reads lint settings via `configure`."""
|
|
43
|
+
|
|
44
|
+
def configure(self, config: LintConfig) -> None:
|
|
45
|
+
"""Receive the resolved lint config before analysis."""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Rendering a rule's long-form documentation for the CLI."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from argusf.analysis.rule import Rule
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RuleDocumentationRenderer:
|
|
11
|
+
"""Renders a rule's docstring for the `argusf rule` command."""
|
|
12
|
+
|
|
13
|
+
def render(self, rule_cls: type[Rule]) -> str:
|
|
14
|
+
"""Render a rule's documentation as markdown.
|
|
15
|
+
|
|
16
|
+
Uses the rule class's own docstring, falling back to its
|
|
17
|
+
metadata description when it has none.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
rule_cls: The rule class to document.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
A markdown document with a heading, severity, and body.
|
|
24
|
+
"""
|
|
25
|
+
metadata = rule_cls.metadata
|
|
26
|
+
# Read __doc__ rather than inspect.getdoc(): getdoc walks the
|
|
27
|
+
# MRO and would surface an inherited docstring for an
|
|
28
|
+
# undocumented rule.
|
|
29
|
+
documentation = inspect.cleandoc(rule_cls.__doc__) if rule_cls.__doc__ else metadata.description
|
|
30
|
+
return f"# {metadata.name} ({metadata.id})\n\nDefault severity: {metadata.default_severity}\n\n{documentation}"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Shared finding constructors used by rules."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from argusf.ir.models import Finding, ProgramUnit, SourceFile, Statement
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from argusf.analysis.rule import RuleMetadata
|
|
9
|
+
from argusf.autofix.models import Fix
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_statement_finding(
|
|
13
|
+
metadata: RuleMetadata, source_file: SourceFile, statement: Statement, fix: Fix | None = None
|
|
14
|
+
) -> Finding:
|
|
15
|
+
"""Build a finding anchored on a statement's span.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
metadata: The reporting rule's metadata.
|
|
19
|
+
source_file: File the statement belongs to.
|
|
20
|
+
statement: Statement the finding points at.
|
|
21
|
+
fix: Optional fix to attach.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
A finding located at the statement.
|
|
25
|
+
"""
|
|
26
|
+
return Finding(
|
|
27
|
+
rule_id=metadata.id,
|
|
28
|
+
message=metadata.message,
|
|
29
|
+
severity=metadata.default_severity,
|
|
30
|
+
file_path=source_file.file_path,
|
|
31
|
+
line=statement.span.start_row,
|
|
32
|
+
column=statement.span.start_col,
|
|
33
|
+
end_line=statement.span.end_row,
|
|
34
|
+
end_column=statement.span.end_col,
|
|
35
|
+
suggestion=metadata.suggestion,
|
|
36
|
+
fix=fix,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_unit_finding(
|
|
41
|
+
metadata: RuleMetadata, source_file: SourceFile, unit: ProgramUnit, fix: Fix | None = None
|
|
42
|
+
) -> Finding:
|
|
43
|
+
"""Build a finding anchored on a program unit's first line.
|
|
44
|
+
|
|
45
|
+
Anchoring on the first line (rather than the whole unit) keeps the
|
|
46
|
+
report and inline suppressions on the declaration statement,
|
|
47
|
+
instead of underlining the entire body.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
metadata: The reporting rule's metadata.
|
|
51
|
+
source_file: File the unit belongs to.
|
|
52
|
+
unit: Program unit the finding points at.
|
|
53
|
+
fix: Optional fix to attach.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
A finding located at the unit's declaration line.
|
|
57
|
+
"""
|
|
58
|
+
return Finding(
|
|
59
|
+
rule_id=metadata.id,
|
|
60
|
+
message=metadata.message,
|
|
61
|
+
severity=metadata.default_severity,
|
|
62
|
+
file_path=source_file.file_path,
|
|
63
|
+
line=unit.span.start_row,
|
|
64
|
+
column=unit.span.start_col,
|
|
65
|
+
suggestion=metadata.suggestion,
|
|
66
|
+
fix=fix,
|
|
67
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""The rule registry and its id-aware `register` decorator."""
|
|
2
|
+
|
|
3
|
+
from argusf.analysis.rule import Rule
|
|
4
|
+
from argusf.registry import Registry
|
|
5
|
+
|
|
6
|
+
RuleRegistry: Registry[str, type[Rule]] = Registry()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register[T: type[Rule]](cls: T) -> T:
|
|
10
|
+
"""Register a rule class under its metadata id.
|
|
11
|
+
|
|
12
|
+
A thin wrapper over the registry's own decorator that reads the id
|
|
13
|
+
from ``cls.metadata``, so it need not be written a second time.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
cls: The rule class to register.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The same class, unchanged.
|
|
20
|
+
"""
|
|
21
|
+
RuleRegistry.register(cls.metadata.id)(cls)
|
|
22
|
+
return cls
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Importing the rule modules registers each rule with the RuleRegistry.
|
|
2
|
+
from .rgus001_no_goto import NoGoto
|
|
3
|
+
from .rgus002_common_block import NoCommonBlock
|
|
4
|
+
from .rgus003_equivalence import NoEquivalence
|
|
5
|
+
from .rgus004_implicit_typing import ImplicitTyping
|
|
6
|
+
from .rgus005_arithmetic_if import NoArithmeticIf
|
|
7
|
+
from .rgus006_entry_statement import NoEntryStatement
|
|
8
|
+
from .rgus007_pause_statement import NoPauseStatement
|
|
9
|
+
from .rgus008_todo_comment import TodoComment
|
|
10
|
+
from .rgus009_unsorted_use_statements import UnsortedUseStatements
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ImplicitTyping",
|
|
14
|
+
"NoArithmeticIf",
|
|
15
|
+
"NoCommonBlock",
|
|
16
|
+
"NoEntryStatement",
|
|
17
|
+
"NoEquivalence",
|
|
18
|
+
"NoGoto",
|
|
19
|
+
"NoPauseStatement",
|
|
20
|
+
"TodoComment",
|
|
21
|
+
"UnsortedUseStatements",
|
|
22
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from argusf.analysis.rule import Rule, RuleMetadata
|
|
2
|
+
from argusf.analysis.rules.findings import build_statement_finding
|
|
3
|
+
from argusf.analysis.rules.registry import register
|
|
4
|
+
from argusf.ir.models import Finding, GotoStatement, Severity, SourceFile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register
|
|
8
|
+
class NoGoto(Rule):
|
|
9
|
+
"""## What it does
|
|
10
|
+
|
|
11
|
+
Checks for use of the `GO TO` statement.
|
|
12
|
+
|
|
13
|
+
## Why is this bad?
|
|
14
|
+
|
|
15
|
+
`GO TO` jumps to a labelled statement unconditionally, so the flow of
|
|
16
|
+
control cannot be understood from the program's block structure alone.
|
|
17
|
+
Code that accumulates `GO TO` jumps degrades into paths that are hard
|
|
18
|
+
to follow, test, and modify. Fortran 90's structured constructs
|
|
19
|
+
(`DO`, `SELECT CASE`, `EXIT`, `CYCLE`) express the same logic without
|
|
20
|
+
labels.
|
|
21
|
+
|
|
22
|
+
## Example
|
|
23
|
+
|
|
24
|
+
```fortran
|
|
25
|
+
i = 0
|
|
26
|
+
10 continue
|
|
27
|
+
i = i + 1
|
|
28
|
+
if (i < 10) go to 10
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use instead:
|
|
32
|
+
|
|
33
|
+
```fortran
|
|
34
|
+
do i = 1, 10
|
|
35
|
+
end do
|
|
36
|
+
```
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
metadata = RuleMetadata(
|
|
40
|
+
id="RGUS001",
|
|
41
|
+
name="no-goto",
|
|
42
|
+
message="Use of GOTO statement detected",
|
|
43
|
+
description="Flags any use of GOTO, which hinders readability and structured control flow.",
|
|
44
|
+
default_severity=Severity.MEDIUM,
|
|
45
|
+
suggestion="Replace GOTO with structured control flow (DO, IF, EXIT)",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def check(self, source_file: SourceFile) -> list[Finding]:
|
|
49
|
+
return [
|
|
50
|
+
build_statement_finding(self.metadata, source_file, statement)
|
|
51
|
+
for unit in source_file.walk_units()
|
|
52
|
+
for statement in unit.statements
|
|
53
|
+
if isinstance(statement, GotoStatement)
|
|
54
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from argusf.analysis.rule import Rule, RuleMetadata
|
|
2
|
+
from argusf.analysis.rules.findings import build_statement_finding
|
|
3
|
+
from argusf.analysis.rules.registry import register
|
|
4
|
+
from argusf.ir.models import CommonStatement, Finding, Severity, SourceFile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register
|
|
8
|
+
class NoCommonBlock(Rule):
|
|
9
|
+
"""## What it does
|
|
10
|
+
|
|
11
|
+
Checks for use of `COMMON` blocks.
|
|
12
|
+
|
|
13
|
+
## Why is this bad?
|
|
14
|
+
|
|
15
|
+
A `COMMON` block shares storage between program units by position
|
|
16
|
+
rather than by name: every declaration must repeat the block's layout,
|
|
17
|
+
and a mismatch in type, kind, or order silently corrupts data instead
|
|
18
|
+
of failing to compile. The result is implicit, untyped global state.
|
|
19
|
+
Module variables provide the same sharing with explicit names and
|
|
20
|
+
compiler-checked types.
|
|
21
|
+
|
|
22
|
+
## Example
|
|
23
|
+
|
|
24
|
+
```fortran
|
|
25
|
+
subroutine step()
|
|
26
|
+
common /state/ x, y
|
|
27
|
+
x = x + y
|
|
28
|
+
end subroutine step
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use instead:
|
|
32
|
+
|
|
33
|
+
```fortran
|
|
34
|
+
module state
|
|
35
|
+
real :: x, y
|
|
36
|
+
end module state
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
metadata = RuleMetadata(
|
|
41
|
+
id="RGUS002",
|
|
42
|
+
name="common-block",
|
|
43
|
+
message="Use of COMMON block detected",
|
|
44
|
+
description="Flags any use of COMMON blocks, which introduce implicit shared state.",
|
|
45
|
+
default_severity=Severity.MEDIUM,
|
|
46
|
+
suggestion="Replace COMMON block with MODULE variables",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def check(self, source_file: SourceFile) -> list[Finding]:
|
|
50
|
+
return [
|
|
51
|
+
build_statement_finding(self.metadata, source_file, statement)
|
|
52
|
+
for unit in source_file.walk_units()
|
|
53
|
+
for statement in unit.statements
|
|
54
|
+
if isinstance(statement, CommonStatement)
|
|
55
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from argusf.analysis.rule import Rule, RuleMetadata
|
|
2
|
+
from argusf.analysis.rules.findings import build_statement_finding
|
|
3
|
+
from argusf.analysis.rules.registry import register
|
|
4
|
+
from argusf.ir.models import EquivalenceStatement, Finding, Severity, SourceFile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register
|
|
8
|
+
class NoEquivalence(Rule):
|
|
9
|
+
"""## What it does
|
|
10
|
+
|
|
11
|
+
Checks for use of `EQUIVALENCE` statements.
|
|
12
|
+
|
|
13
|
+
## Why is this bad?
|
|
14
|
+
|
|
15
|
+
`EQUIVALENCE` forces two or more variables to share the same storage,
|
|
16
|
+
creating hidden aliases: writing through one name silently changes the
|
|
17
|
+
value read through another, and the sharing is invisible at every use
|
|
18
|
+
site. It also defeats compiler optimisation and type checking. Modern
|
|
19
|
+
Fortran expresses the underlying intent directly — `TRANSFER` for type
|
|
20
|
+
reinterpretation, `POINTER` association for aliasing, allocatables or
|
|
21
|
+
derived types for storage reuse.
|
|
22
|
+
|
|
23
|
+
## Example
|
|
24
|
+
|
|
25
|
+
```fortran
|
|
26
|
+
real :: buffer(100)
|
|
27
|
+
integer :: ibuffer(100)
|
|
28
|
+
equivalence (buffer, ibuffer)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use instead:
|
|
32
|
+
|
|
33
|
+
```fortran
|
|
34
|
+
real :: buffer(100)
|
|
35
|
+
integer :: ibuffer(100)
|
|
36
|
+
ibuffer = transfer(buffer, ibuffer)
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
metadata = RuleMetadata(
|
|
41
|
+
id="RGUS003",
|
|
42
|
+
name="equivalence",
|
|
43
|
+
message="Use of EQUIVALENCE statement detected",
|
|
44
|
+
description="Flags any use of EQUIVALENCE, which aliases storage between variables.",
|
|
45
|
+
default_severity=Severity.MEDIUM,
|
|
46
|
+
suggestion="Replace EQUIVALENCE with TRANSFER, POINTER association, or a derived type",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def check(self, source_file: SourceFile) -> list[Finding]:
|
|
50
|
+
return [
|
|
51
|
+
build_statement_finding(self.metadata, source_file, statement)
|
|
52
|
+
for unit in source_file.walk_units()
|
|
53
|
+
for statement in unit.statements
|
|
54
|
+
if isinstance(statement, EquivalenceStatement)
|
|
55
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from argusf.analysis.rule import Rule, RuleMetadata
|
|
2
|
+
from argusf.analysis.rules.findings import build_unit_finding
|
|
3
|
+
from argusf.analysis.rules.registry import register
|
|
4
|
+
from argusf.autofix.edits import insertion
|
|
5
|
+
from argusf.autofix.models import Applicability, Fix
|
|
6
|
+
from argusf.autofix.source import line_end, line_indent, span_fits
|
|
7
|
+
from argusf.ir.models import Finding, ImplicitStatement, ProgramUnit, Severity, SourceFile, Submodule, UseStatement
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@register
|
|
11
|
+
class ImplicitTyping(Rule):
|
|
12
|
+
"""## What it does
|
|
13
|
+
|
|
14
|
+
Checks for program units that do not disable implicit typing with
|
|
15
|
+
`IMPLICIT NONE`.
|
|
16
|
+
|
|
17
|
+
## Why is this bad?
|
|
18
|
+
|
|
19
|
+
Without `IMPLICIT NONE`, any undeclared name is implicitly typed from
|
|
20
|
+
its first letter (`I` to `N` integer, everything else real). A typo in a
|
|
21
|
+
variable name then compiles cleanly as a brand-new variable instead of
|
|
22
|
+
failing, which is a classic source of silent numerical bugs.
|
|
23
|
+
|
|
24
|
+
A unit is not flagged when a host scope in the same file already
|
|
25
|
+
declares `IMPLICIT NONE` (contained procedures inherit the host's
|
|
26
|
+
implicit rules). Submodules and the procedures they contain are never
|
|
27
|
+
flagged: their implicit rules come from the ancestor module, which
|
|
28
|
+
usually lives in another file. Note that `IMPLICIT NONE (EXTERNAL)`
|
|
29
|
+
does not disable implicit typing and so does not satisfy this rule.
|
|
30
|
+
|
|
31
|
+
## Example
|
|
32
|
+
|
|
33
|
+
```fortran
|
|
34
|
+
program simulate
|
|
35
|
+
total = 0.0
|
|
36
|
+
end program simulate
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use instead:
|
|
40
|
+
|
|
41
|
+
```fortran
|
|
42
|
+
program simulate
|
|
43
|
+
implicit none
|
|
44
|
+
real :: total
|
|
45
|
+
total = 0.0
|
|
46
|
+
end program simulate
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Fix safety
|
|
50
|
+
|
|
51
|
+
The fix inserts `IMPLICIT NONE` after the unit's USE statements. Any
|
|
52
|
+
name the unit was implicitly typing then becomes a compile error until
|
|
53
|
+
it is declared — deliberate, but it means the fixed code may not
|
|
54
|
+
compile as-is, so the fix is always marked unsafe.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
metadata = RuleMetadata(
|
|
58
|
+
id="RGUS004",
|
|
59
|
+
name="implicit-typing",
|
|
60
|
+
message="Program unit does not declare IMPLICIT NONE",
|
|
61
|
+
description="Flags program units without IMPLICIT NONE, which leaves undeclared names implicitly typed.",
|
|
62
|
+
default_severity=Severity.HIGH,
|
|
63
|
+
suggestion="Add IMPLICIT NONE to the unit's specification section",
|
|
64
|
+
preview=True, # Must confirm rule is stable on nested units
|
|
65
|
+
fixable=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def check(self, source_file: SourceFile) -> list[Finding]:
|
|
69
|
+
findings: list[Finding] = []
|
|
70
|
+
for unit in source_file.program_units:
|
|
71
|
+
self._check_unit(unit, source_file, host_covered=False, findings=findings)
|
|
72
|
+
return findings
|
|
73
|
+
|
|
74
|
+
def _check_unit(
|
|
75
|
+
self, unit: ProgramUnit, source_file: SourceFile, *, host_covered: bool, findings: list[Finding]
|
|
76
|
+
) -> None:
|
|
77
|
+
covered = host_covered or any(
|
|
78
|
+
isinstance(statement, ImplicitStatement) and statement.is_none for statement in unit.statements
|
|
79
|
+
)
|
|
80
|
+
# A submodule's implicit rules come from its ancestor module, which
|
|
81
|
+
# usually lives in another file: treat the whole subtree as covered
|
|
82
|
+
# rather than risk false positives.
|
|
83
|
+
if isinstance(unit, Submodule):
|
|
84
|
+
covered = True
|
|
85
|
+
elif not covered:
|
|
86
|
+
findings.append(
|
|
87
|
+
build_unit_finding(self.metadata, source_file, unit, fix=self._build_fix(source_file, unit))
|
|
88
|
+
)
|
|
89
|
+
for subunit in unit.subunits:
|
|
90
|
+
self._check_unit(subunit, source_file, host_covered=covered, findings=findings)
|
|
91
|
+
|
|
92
|
+
def _build_fix(self, source_file: SourceFile, unit: ProgramUnit) -> Fix | None:
|
|
93
|
+
"""Build the `implicit none` insertion fix, or None.
|
|
94
|
+
|
|
95
|
+
Fortran's statement ordering puts USE statements before the
|
|
96
|
+
implicit part, so the insertion point is the line after the
|
|
97
|
+
last USE statement, or after the unit's declaration line when
|
|
98
|
+
there are none.
|
|
99
|
+
"""
|
|
100
|
+
source = source_file.source
|
|
101
|
+
if not span_fits(source, unit.span):
|
|
102
|
+
return None
|
|
103
|
+
uses = [statement for statement in unit.statements if isinstance(statement, UseStatement)]
|
|
104
|
+
if uses:
|
|
105
|
+
last_use = max(uses, key=lambda statement: statement.span.end_byte)
|
|
106
|
+
anchor = line_end(source, max(last_use.span.start_byte, last_use.span.end_byte - 1))
|
|
107
|
+
else:
|
|
108
|
+
anchor = line_end(source, unit.span.start_byte)
|
|
109
|
+
indent = self._probe_indent(source, anchor, unit)
|
|
110
|
+
return Fix(
|
|
111
|
+
applicability=Applicability.UNSAFE,
|
|
112
|
+
edits=(insertion(source, anchor, f"{indent}implicit none\n"),),
|
|
113
|
+
message="Insert `implicit none`",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _probe_indent(self, source: bytes, anchor: int, unit: ProgramUnit) -> str:
|
|
117
|
+
"""Probe the indentation to give the inserted statement.
|
|
118
|
+
|
|
119
|
+
Matches the indentation of the first non-blank line after the
|
|
120
|
+
insertion point (the unit's own body style); falls back to the
|
|
121
|
+
declaration line.
|
|
122
|
+
"""
|
|
123
|
+
offset = anchor
|
|
124
|
+
while offset < min(len(source), unit.span.end_byte):
|
|
125
|
+
end = line_end(source, offset)
|
|
126
|
+
if source[offset:end].strip():
|
|
127
|
+
return line_indent(source, offset)
|
|
128
|
+
offset = end
|
|
129
|
+
return line_indent(source, unit.span.start_byte)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from argusf.analysis.rule import Rule, RuleMetadata
|
|
2
|
+
from argusf.analysis.rules.findings import build_statement_finding
|
|
3
|
+
from argusf.analysis.rules.registry import register
|
|
4
|
+
from argusf.ir.models import ArithmeticIfStatement, Finding, Severity, SourceFile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register
|
|
8
|
+
class NoArithmeticIf(Rule):
|
|
9
|
+
"""## What it does
|
|
10
|
+
|
|
11
|
+
Checks for use of the arithmetic `IF` statement.
|
|
12
|
+
|
|
13
|
+
## Why is this bad?
|
|
14
|
+
|
|
15
|
+
The arithmetic `IF` — `IF (expr) 10, 20, 30` — branches to one of
|
|
16
|
+
three labels depending on the sign of the expression. It is a labelled
|
|
17
|
+
jump in disguise, with all of `GO TO`'s readability problems plus an
|
|
18
|
+
easily-misread three-way convention. It has been obsolescent since
|
|
19
|
+
Fortran 90 and was deleted from the standard in Fortran 2018. Block
|
|
20
|
+
`IF`/`ELSE IF` constructs express the same logic structurally.
|
|
21
|
+
|
|
22
|
+
## Example
|
|
23
|
+
|
|
24
|
+
```fortran
|
|
25
|
+
if (x) 10, 20, 30
|
|
26
|
+
10 y = -1.0
|
|
27
|
+
go to 40
|
|
28
|
+
20 y = 0.0
|
|
29
|
+
go to 40
|
|
30
|
+
30 y = 1.0
|
|
31
|
+
40 continue
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use instead:
|
|
35
|
+
|
|
36
|
+
```fortran
|
|
37
|
+
if (x < 0.0) then
|
|
38
|
+
y = -1.0
|
|
39
|
+
else if (x == 0.0) then
|
|
40
|
+
y = 0.0
|
|
41
|
+
else
|
|
42
|
+
y = 1.0
|
|
43
|
+
end if
|
|
44
|
+
```
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
metadata = RuleMetadata(
|
|
48
|
+
id="RGUS005",
|
|
49
|
+
name="arithmetic-if",
|
|
50
|
+
message="Use of arithmetic IF statement detected",
|
|
51
|
+
description="Flags the three-way arithmetic IF, deleted from the standard in Fortran 2018.",
|
|
52
|
+
default_severity=Severity.MEDIUM,
|
|
53
|
+
suggestion="Replace arithmetic IF with an IF/ELSE IF construct",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def check(self, source_file: SourceFile) -> list[Finding]:
|
|
57
|
+
return [
|
|
58
|
+
build_statement_finding(self.metadata, source_file, statement)
|
|
59
|
+
for unit in source_file.walk_units()
|
|
60
|
+
for statement in unit.statements
|
|
61
|
+
if isinstance(statement, ArithmeticIfStatement)
|
|
62
|
+
]
|