argusf 0.1.0.dev0__tar.gz
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-0.1.0.dev0/LICENSE +21 -0
- argusf-0.1.0.dev0/PKG-INFO +166 -0
- argusf-0.1.0.dev0/README.md +137 -0
- argusf-0.1.0.dev0/pyproject.toml +142 -0
- argusf-0.1.0.dev0/src/argusf/__init__.py +3 -0
- argusf-0.1.0.dev0/src/argusf/analysis/__init__.py +14 -0
- argusf-0.1.0.dev0/src/argusf/analysis/engine.py +100 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rule.py +45 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/__init__.py +5 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/documentation.py +30 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/findings.py +67 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/registry.py +22 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/__init__.py +4 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/__init__.py +22 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus001_no_goto.py +54 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus002_common_block.py +55 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus003_equivalence.py +55 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus004_implicit_typing.py +129 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus005_arithmetic_if.py +62 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus006_entry_statement.py +67 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus007_pause_statement.py +93 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus008_todo_comment.py +100 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus009_unsorted_use_statements.py +258 -0
- argusf-0.1.0.dev0/src/argusf/analysis/rules/selection.py +83 -0
- argusf-0.1.0.dev0/src/argusf/analysis/suppression.py +92 -0
- argusf-0.1.0.dev0/src/argusf/analysis/syntax_errors.py +41 -0
- argusf-0.1.0.dev0/src/argusf/autofix/__init__.py +26 -0
- argusf-0.1.0.dev0/src/argusf/autofix/applier.py +130 -0
- argusf-0.1.0.dev0/src/argusf/autofix/context.py +82 -0
- argusf-0.1.0.dev0/src/argusf/autofix/diff.py +39 -0
- argusf-0.1.0.dev0/src/argusf/autofix/edits.py +31 -0
- argusf-0.1.0.dev0/src/argusf/autofix/engine.py +192 -0
- argusf-0.1.0.dev0/src/argusf/autofix/models.py +39 -0
- argusf-0.1.0.dev0/src/argusf/autofix/source.py +68 -0
- argusf-0.1.0.dev0/src/argusf/autofix/writer.py +53 -0
- argusf-0.1.0.dev0/src/argusf/cache/__init__.py +5 -0
- argusf-0.1.0.dev0/src/argusf/cache/backend.py +294 -0
- argusf-0.1.0.dev0/src/argusf/cli.py +428 -0
- argusf-0.1.0.dev0/src/argusf/config/__init__.py +1 -0
- argusf-0.1.0.dev0/src/argusf/config/config_resolver.py +212 -0
- argusf-0.1.0.dev0/src/argusf/config/errors.py +8 -0
- argusf-0.1.0.dev0/src/argusf/config/file_reader/__init__.py +3 -0
- argusf-0.1.0.dev0/src/argusf/config/file_reader/reader.py +58 -0
- argusf-0.1.0.dev0/src/argusf/config/models.py +110 -0
- argusf-0.1.0.dev0/src/argusf/config/validation.py +113 -0
- argusf-0.1.0.dev0/src/argusf/constants.py +66 -0
- argusf-0.1.0.dev0/src/argusf/diagnostics.py +45 -0
- argusf-0.1.0.dev0/src/argusf/discovery/__init__.py +3 -0
- argusf-0.1.0.dev0/src/argusf/discovery/backend.py +250 -0
- argusf-0.1.0.dev0/src/argusf/discovery/gitignore.py +125 -0
- argusf-0.1.0.dev0/src/argusf/discovery/repo.py +30 -0
- argusf-0.1.0.dev0/src/argusf/hashing/__init__.py +5 -0
- argusf-0.1.0.dev0/src/argusf/hashing/hash.py +48 -0
- argusf-0.1.0.dev0/src/argusf/ir/__init__.py +1 -0
- argusf-0.1.0.dev0/src/argusf/ir/models/__init__.py +49 -0
- argusf-0.1.0.dev0/src/argusf/ir/models/findings.py +74 -0
- argusf-0.1.0.dev0/src/argusf/ir/models/source.py +199 -0
- argusf-0.1.0.dev0/src/argusf/orchestrator.py +127 -0
- argusf-0.1.0.dev0/src/argusf/parser/__init__.py +1 -0
- argusf-0.1.0.dev0/src/argusf/parser/backend.py +24 -0
- argusf-0.1.0.dev0/src/argusf/parser/treesitter/__init__.py +1 -0
- argusf-0.1.0.dev0/src/argusf/parser/treesitter/backend.py +157 -0
- argusf-0.1.0.dev0/src/argusf/parser/treesitter/walker/__init__.py +5 -0
- argusf-0.1.0.dev0/src/argusf/parser/treesitter/walker/handlers.py +209 -0
- argusf-0.1.0.dev0/src/argusf/parser/treesitter/walker/walker.py +82 -0
- argusf-0.1.0.dev0/src/argusf/registry/__init__.py +3 -0
- argusf-0.1.0.dev0/src/argusf/registry/registry.py +69 -0
- argusf-0.1.0.dev0/src/argusf/reporting/__init__.py +22 -0
- argusf-0.1.0.dev0/src/argusf/reporting/formatting.py +162 -0
- argusf-0.1.0.dev0/src/argusf/reporting/registry.py +20 -0
- argusf-0.1.0.dev0/src/argusf/reporting/reporters/__init__.py +15 -0
- argusf-0.1.0.dev0/src/argusf/reporting/reporters/base.py +29 -0
- argusf-0.1.0.dev0/src/argusf/reporting/reporters/concise.py +35 -0
- argusf-0.1.0.dev0/src/argusf/reporting/reporters/diff.py +30 -0
- argusf-0.1.0.dev0/src/argusf/reporting/reporters/json.py +59 -0
- argusf-0.1.0.dev0/src/argusf/reporting/reporters/null.py +21 -0
- argusf-0.1.0.dev0/src/argusf/reporting/reporters/standard.py +78 -0
- argusf-0.1.0.dev0/src/argusf/reporting/reporters/statistics.py +99 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew McAteer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: argusf
|
|
3
|
+
Version: 0.1.0.dev0
|
|
4
|
+
Summary: A ruff-inspired linter for Fortran 90
|
|
5
|
+
Keywords: fortran,linter,static-analysis,static-analyzer,code-quality,fortran90,ruff
|
|
6
|
+
Author: Matthew McAteer
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Fortran
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
18
|
+
Requires-Dist: click>=8.3.3
|
|
19
|
+
Requires-Dist: msgpack>=1.2.0
|
|
20
|
+
Requires-Dist: packaging>=26.2
|
|
21
|
+
Requires-Dist: pathspec>=1.1.1
|
|
22
|
+
Requires-Dist: tree-sitter==0.25.2
|
|
23
|
+
Requires-Dist: tree-sitter-fortran>=0.6.0
|
|
24
|
+
Requires-Python: >=3.14
|
|
25
|
+
Project-URL: Homepage, https://github.com/mmcateer13/argusf
|
|
26
|
+
Project-URL: Repository, https://github.com/mmcateer13/argusf
|
|
27
|
+
Project-URL: Issues, https://github.com/mmcateer13/argusf/issues
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# argusf
|
|
31
|
+
|
|
32
|
+
<!-- Badges -->
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
[](https://www.python.org/)
|
|
35
|
+
<!-- [](https://github.com/mmcateer13/argusf/actions) -->
|
|
36
|
+
|
|
37
|
+
A [ruff](https://github.com/astral-sh/ruff)-inspired linter for Fortran 90.
|
|
38
|
+
|
|
39
|
+
[Issues](https://github.com/mmcateer13/argusf/issues) | [Contributing](CONTRIBUTING.md) | [Security](SECURITY.md)
|
|
40
|
+
|
|
41
|
+
## Table of Contents
|
|
42
|
+
|
|
43
|
+
1. [What is argusf?](#what-is-argusf)
|
|
44
|
+
2. [Current State](#current-state)
|
|
45
|
+
3. [Getting Started](#getting-started)
|
|
46
|
+
4. [Available Rules](#available-rules)
|
|
47
|
+
5. [Contributing & Support](#contributing--support)
|
|
48
|
+
6. [Acknowledgements](#acknowledgements)
|
|
49
|
+
7. [License](#license)
|
|
50
|
+
|
|
51
|
+
## What is argusf?
|
|
52
|
+
|
|
53
|
+
argusf is a linter for Fortran 90 codebases, designed with the philosophy of tools like [ruff](https://github.com/astral-sh/ruff): opinionated, and easy to integrate into existing workflows. It analyses your Fortran 90 source files and reports style and quality issues to help you maintain a consistent, readable codebase.
|
|
54
|
+
|
|
55
|
+
- 🔧 **Zero configuration required** — sensible defaults out of the box; customise via `argusf.toml` when you need to
|
|
56
|
+
- 📁 **Scan a file or a whole directory** — just point it at your code
|
|
57
|
+
- 📍 **Precise output** — violations reported with file, line, and column
|
|
58
|
+
- 🔄 **JSON output support** — easy to integrate into CI pipelines and tooling
|
|
59
|
+
- 🎯 **Fortran 90 focused** — purpose-built for the language
|
|
60
|
+
|
|
61
|
+
## Current State
|
|
62
|
+
|
|
63
|
+
argusf is an early-stage project. The core internals are working, but the available ruleset is still extremely limited. If you try argusf and encounter unexpected behaviour, missing rules you would find useful, or other issues, please [open an issue](https://github.com/mmcateer13/argusf/issues) — early feedback is especially valuable at this stage.
|
|
64
|
+
|
|
65
|
+
## Getting Started
|
|
66
|
+
|
|
67
|
+
### Installation
|
|
68
|
+
|
|
69
|
+
> [!NOTE]
|
|
70
|
+
> If you are planning to contribute to argusf, please see the [contributing guide](CONTRIBUTING.md) and [developer setup instructions](docs/dev_setup.md) instead.
|
|
71
|
+
|
|
72
|
+
argusf runs on macOS, Linux, and Windows. (Note that the [developer setup instructions](docs/dev_setup.md) currently target macOS and Linux only — this applies to setting up a development environment, not to running the tool.)
|
|
73
|
+
|
|
74
|
+
Installing argusf requires [uv](https://github.com/astral-sh/uv); if you do not already have it, follow the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
|
|
75
|
+
|
|
76
|
+
argusf is not yet available on PyPI. To install it, clone the repository and install it from the local checkout with `uv`, which builds it from source (no PyPI release required) and puts the `argusf` command on your `PATH`:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
git clone https://github.com/mmcateer13/argusf.git
|
|
80
|
+
cd argusf
|
|
81
|
+
uv tool install --editable .
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The `--editable` flag means a later `git pull` is picked up without reinstalling. To update after pulling — or if you installed without `--editable` — run `uv tool install --editable . --reinstall`.
|
|
85
|
+
|
|
86
|
+
### Usage
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
argusf check # Analyse the current directory
|
|
90
|
+
argusf check path/to/file.f90 # Analyse a single file
|
|
91
|
+
argusf check path/to/your/code/ # Analyse an entire directory
|
|
92
|
+
argusf check --output-format json path/to/your/code/ # Output findings as JSON
|
|
93
|
+
argusf check --fix path/to/your/code/ # Apply safe fixes automatically
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Configuration
|
|
97
|
+
|
|
98
|
+
argusf works out of the box with no configuration. To customise behaviour, add an `argusf.toml` file to the root of your project.
|
|
99
|
+
|
|
100
|
+
The snippet below is equivalent to argusf's out-of-the-box defaults — a handy central reference for what the tool does before you change anything. You only need to include the keys you want to override. It is a hand-maintained snapshot and may lag the code between releases; `argusf --help` and `argusf check --help` are the authoritative source for current defaults.
|
|
101
|
+
|
|
102
|
+
```toml
|
|
103
|
+
# Top-level options
|
|
104
|
+
include = ["*.F", "*.F77", "*.F90", "*.FOR", "*.FTN", "*.f", "*.f77", "*.f90", "*.for", "*.ftn"]
|
|
105
|
+
exclude = [
|
|
106
|
+
".argusf_cache", ".direnv", ".git", ".hg", ".idea", ".mypy_cache",
|
|
107
|
+
".pytest_cache", ".ruff_cache", ".svn", ".venv", ".vscode",
|
|
108
|
+
"__pycache__", "_build", "bin", "build", "dist", "obj", "venv",
|
|
109
|
+
]
|
|
110
|
+
extend_include = []
|
|
111
|
+
extend_exclude = []
|
|
112
|
+
respect_gitignore = true
|
|
113
|
+
cache_dir = ".argusf_cache"
|
|
114
|
+
no_cache = false
|
|
115
|
+
output_format = "standard" # "standard", "concise", or "json"
|
|
116
|
+
# on_large_file is unset by default (guardrail off); set "warn" or "skip" to enable.
|
|
117
|
+
large_file_threshold = "1MB"
|
|
118
|
+
fix = false
|
|
119
|
+
fix_only = false
|
|
120
|
+
unsafe_fixes = false
|
|
121
|
+
show_fixes = false
|
|
122
|
+
|
|
123
|
+
[lint]
|
|
124
|
+
select = ["RGUS"]
|
|
125
|
+
extend_select = []
|
|
126
|
+
ignore = []
|
|
127
|
+
preview = false
|
|
128
|
+
fixable = ["ALL"]
|
|
129
|
+
unfixable = []
|
|
130
|
+
extend_fixable = []
|
|
131
|
+
task_tags = ["TODO", "FIXME", "XXX"]
|
|
132
|
+
# per_file_ignores is empty by default, e.g.:
|
|
133
|
+
# [lint.per_file_ignores]
|
|
134
|
+
# "tests/**" = ["RGUS001"]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
For the full set of options and flags, run `argusf --help` and `argusf check --help` (and `--help` on any subcommand) — these are the authoritative reference for available arguments and options.
|
|
138
|
+
|
|
139
|
+
## Available Rules
|
|
140
|
+
|
|
141
|
+
Rule information lives in the CLI itself — use the `argusf rule` command to read a rule's documentation, including its rationale, an example, and any available fix:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
argusf rule --all # documentation for every rule
|
|
145
|
+
argusf rule RGUS001 # documentation for a specific rule
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Dedicated, per-rule documentation pages are planned for a later release. In the meantime, `argusf rule` is the authoritative source, and you can always read the current ruleset in the [source](https://github.com/mmcateer13/argusf/tree/development/src/argusf/analysis/rules/rulesets).
|
|
149
|
+
|
|
150
|
+
## Contributing & Support
|
|
151
|
+
|
|
152
|
+
Contributions are welcome. For bug reports, questions, or feature requests, please [open an issue](https://github.com/mmcateer13/argusf/issues).
|
|
153
|
+
|
|
154
|
+
If you would like to contribute code, see [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines.
|
|
155
|
+
|
|
156
|
+
To report a security vulnerability, please follow the process in [SECURITY.md](SECURITY.md) rather than opening a public issue.
|
|
157
|
+
|
|
158
|
+
## Acknowledgements
|
|
159
|
+
|
|
160
|
+
This project is a faithful iteration on the dissertation project that I completed in my final year of study at [Queen's University Belfast](https://www.qub.ac.uk/), under the supervision of [Prof. Austen Rainer](https://pure.qub.ac.uk/en/persons/austen-rainer/) and [Dr. David Cutting](https://pure.qub.ac.uk/en/persons/david-cutting/). Their support throughout the project's initial development is part of the reason I felt so eager to continue this project today.
|
|
161
|
+
|
|
162
|
+
A shoutout to the [Astral](https://astral.sh/) team who designed tools such as [ruff](https://github.com/astral-sh/ruff), [uv](https://github.com/astral-sh/uv), and [ty](https://github.com/astral-sh/ty). As an engineer whose (fairly short) career experience is almost entirely Python, the improvements their tooling has made in developer experience across the Python ecosystem has been nothing short of extraordinary. This tool takes heavy inspiration from the `ruff` linter, in an attempt to take even a fraction of this transformative DevEx improvement into the Fortran community!
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
argusf is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# argusf
|
|
2
|
+
|
|
3
|
+
<!-- Badges -->
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
<!-- [](https://github.com/mmcateer13/argusf/actions) -->
|
|
7
|
+
|
|
8
|
+
A [ruff](https://github.com/astral-sh/ruff)-inspired linter for Fortran 90.
|
|
9
|
+
|
|
10
|
+
[Issues](https://github.com/mmcateer13/argusf/issues) | [Contributing](CONTRIBUTING.md) | [Security](SECURITY.md)
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
1. [What is argusf?](#what-is-argusf)
|
|
15
|
+
2. [Current State](#current-state)
|
|
16
|
+
3. [Getting Started](#getting-started)
|
|
17
|
+
4. [Available Rules](#available-rules)
|
|
18
|
+
5. [Contributing & Support](#contributing--support)
|
|
19
|
+
6. [Acknowledgements](#acknowledgements)
|
|
20
|
+
7. [License](#license)
|
|
21
|
+
|
|
22
|
+
## What is argusf?
|
|
23
|
+
|
|
24
|
+
argusf is a linter for Fortran 90 codebases, designed with the philosophy of tools like [ruff](https://github.com/astral-sh/ruff): opinionated, and easy to integrate into existing workflows. It analyses your Fortran 90 source files and reports style and quality issues to help you maintain a consistent, readable codebase.
|
|
25
|
+
|
|
26
|
+
- 🔧 **Zero configuration required** — sensible defaults out of the box; customise via `argusf.toml` when you need to
|
|
27
|
+
- 📁 **Scan a file or a whole directory** — just point it at your code
|
|
28
|
+
- 📍 **Precise output** — violations reported with file, line, and column
|
|
29
|
+
- 🔄 **JSON output support** — easy to integrate into CI pipelines and tooling
|
|
30
|
+
- 🎯 **Fortran 90 focused** — purpose-built for the language
|
|
31
|
+
|
|
32
|
+
## Current State
|
|
33
|
+
|
|
34
|
+
argusf is an early-stage project. The core internals are working, but the available ruleset is still extremely limited. If you try argusf and encounter unexpected behaviour, missing rules you would find useful, or other issues, please [open an issue](https://github.com/mmcateer13/argusf/issues) — early feedback is especially valuable at this stage.
|
|
35
|
+
|
|
36
|
+
## Getting Started
|
|
37
|
+
|
|
38
|
+
### Installation
|
|
39
|
+
|
|
40
|
+
> [!NOTE]
|
|
41
|
+
> If you are planning to contribute to argusf, please see the [contributing guide](CONTRIBUTING.md) and [developer setup instructions](docs/dev_setup.md) instead.
|
|
42
|
+
|
|
43
|
+
argusf runs on macOS, Linux, and Windows. (Note that the [developer setup instructions](docs/dev_setup.md) currently target macOS and Linux only — this applies to setting up a development environment, not to running the tool.)
|
|
44
|
+
|
|
45
|
+
Installing argusf requires [uv](https://github.com/astral-sh/uv); if you do not already have it, follow the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
|
|
46
|
+
|
|
47
|
+
argusf is not yet available on PyPI. To install it, clone the repository and install it from the local checkout with `uv`, which builds it from source (no PyPI release required) and puts the `argusf` command on your `PATH`:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/mmcateer13/argusf.git
|
|
51
|
+
cd argusf
|
|
52
|
+
uv tool install --editable .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The `--editable` flag means a later `git pull` is picked up without reinstalling. To update after pulling — or if you installed without `--editable` — run `uv tool install --editable . --reinstall`.
|
|
56
|
+
|
|
57
|
+
### Usage
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
argusf check # Analyse the current directory
|
|
61
|
+
argusf check path/to/file.f90 # Analyse a single file
|
|
62
|
+
argusf check path/to/your/code/ # Analyse an entire directory
|
|
63
|
+
argusf check --output-format json path/to/your/code/ # Output findings as JSON
|
|
64
|
+
argusf check --fix path/to/your/code/ # Apply safe fixes automatically
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Configuration
|
|
68
|
+
|
|
69
|
+
argusf works out of the box with no configuration. To customise behaviour, add an `argusf.toml` file to the root of your project.
|
|
70
|
+
|
|
71
|
+
The snippet below is equivalent to argusf's out-of-the-box defaults — a handy central reference for what the tool does before you change anything. You only need to include the keys you want to override. It is a hand-maintained snapshot and may lag the code between releases; `argusf --help` and `argusf check --help` are the authoritative source for current defaults.
|
|
72
|
+
|
|
73
|
+
```toml
|
|
74
|
+
# Top-level options
|
|
75
|
+
include = ["*.F", "*.F77", "*.F90", "*.FOR", "*.FTN", "*.f", "*.f77", "*.f90", "*.for", "*.ftn"]
|
|
76
|
+
exclude = [
|
|
77
|
+
".argusf_cache", ".direnv", ".git", ".hg", ".idea", ".mypy_cache",
|
|
78
|
+
".pytest_cache", ".ruff_cache", ".svn", ".venv", ".vscode",
|
|
79
|
+
"__pycache__", "_build", "bin", "build", "dist", "obj", "venv",
|
|
80
|
+
]
|
|
81
|
+
extend_include = []
|
|
82
|
+
extend_exclude = []
|
|
83
|
+
respect_gitignore = true
|
|
84
|
+
cache_dir = ".argusf_cache"
|
|
85
|
+
no_cache = false
|
|
86
|
+
output_format = "standard" # "standard", "concise", or "json"
|
|
87
|
+
# on_large_file is unset by default (guardrail off); set "warn" or "skip" to enable.
|
|
88
|
+
large_file_threshold = "1MB"
|
|
89
|
+
fix = false
|
|
90
|
+
fix_only = false
|
|
91
|
+
unsafe_fixes = false
|
|
92
|
+
show_fixes = false
|
|
93
|
+
|
|
94
|
+
[lint]
|
|
95
|
+
select = ["RGUS"]
|
|
96
|
+
extend_select = []
|
|
97
|
+
ignore = []
|
|
98
|
+
preview = false
|
|
99
|
+
fixable = ["ALL"]
|
|
100
|
+
unfixable = []
|
|
101
|
+
extend_fixable = []
|
|
102
|
+
task_tags = ["TODO", "FIXME", "XXX"]
|
|
103
|
+
# per_file_ignores is empty by default, e.g.:
|
|
104
|
+
# [lint.per_file_ignores]
|
|
105
|
+
# "tests/**" = ["RGUS001"]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
For the full set of options and flags, run `argusf --help` and `argusf check --help` (and `--help` on any subcommand) — these are the authoritative reference for available arguments and options.
|
|
109
|
+
|
|
110
|
+
## Available Rules
|
|
111
|
+
|
|
112
|
+
Rule information lives in the CLI itself — use the `argusf rule` command to read a rule's documentation, including its rationale, an example, and any available fix:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
argusf rule --all # documentation for every rule
|
|
116
|
+
argusf rule RGUS001 # documentation for a specific rule
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Dedicated, per-rule documentation pages are planned for a later release. In the meantime, `argusf rule` is the authoritative source, and you can always read the current ruleset in the [source](https://github.com/mmcateer13/argusf/tree/development/src/argusf/analysis/rules/rulesets).
|
|
120
|
+
|
|
121
|
+
## Contributing & Support
|
|
122
|
+
|
|
123
|
+
Contributions are welcome. For bug reports, questions, or feature requests, please [open an issue](https://github.com/mmcateer13/argusf/issues).
|
|
124
|
+
|
|
125
|
+
If you would like to contribute code, see [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines.
|
|
126
|
+
|
|
127
|
+
To report a security vulnerability, please follow the process in [SECURITY.md](SECURITY.md) rather than opening a public issue.
|
|
128
|
+
|
|
129
|
+
## Acknowledgements
|
|
130
|
+
|
|
131
|
+
This project is a faithful iteration on the dissertation project that I completed in my final year of study at [Queen's University Belfast](https://www.qub.ac.uk/), under the supervision of [Prof. Austen Rainer](https://pure.qub.ac.uk/en/persons/austen-rainer/) and [Dr. David Cutting](https://pure.qub.ac.uk/en/persons/david-cutting/). Their support throughout the project's initial development is part of the reason I felt so eager to continue this project today.
|
|
132
|
+
|
|
133
|
+
A shoutout to the [Astral](https://astral.sh/) team who designed tools such as [ruff](https://github.com/astral-sh/ruff), [uv](https://github.com/astral-sh/uv), and [ty](https://github.com/astral-sh/ty). As an engineer whose (fairly short) career experience is almost entirely Python, the improvements their tooling has made in developer experience across the Python ecosystem has been nothing short of extraordinary. This tool takes heavy inspiration from the `ruff` linter, in an attempt to take even a fraction of this transformative DevEx improvement into the Fortran community!
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
argusf is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "argusf"
|
|
3
|
+
version = "0.1.0.dev0"
|
|
4
|
+
description = "A ruff-inspired linter for Fortran 90"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Matthew McAteer" },
|
|
11
|
+
]
|
|
12
|
+
keywords = ["fortran", "linter", "static-analysis", "static-analyzer", "code-quality", "fortran90", "ruff"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Fortran",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"click>=8.3.3",
|
|
26
|
+
"msgpack>=1.2.0",
|
|
27
|
+
"packaging>=26.2",
|
|
28
|
+
"pathspec>=1.1.1",
|
|
29
|
+
# Pinned due to SEGFAULTs on v0.26 when testing
|
|
30
|
+
# the package on .F files.
|
|
31
|
+
"tree-sitter==0.25.2",
|
|
32
|
+
"tree-sitter-fortran>=0.6.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
argusf = "argusf.cli:cli"
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/mmcateer13/argusf"
|
|
40
|
+
Repository = "https://github.com/mmcateer13/argusf"
|
|
41
|
+
Issues = "https://github.com/mmcateer13/argusf/issues"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["uv_build>=0.11.27,<0.12"]
|
|
45
|
+
build-backend = "uv_build"
|
|
46
|
+
|
|
47
|
+
[tool.uv.build-backend]
|
|
48
|
+
module-name = "argusf"
|
|
49
|
+
module-root = "src"
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 120
|
|
53
|
+
target-version = "py314"
|
|
54
|
+
src = ["src"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["ALL"]
|
|
58
|
+
ignore = [
|
|
59
|
+
# Type annotations on args/returns — we do our typing-related linting with mypy.
|
|
60
|
+
"ANN",
|
|
61
|
+
# Boolean positional/default args — conflicts with click's decorator pattern
|
|
62
|
+
"FBT001", "FBT002",
|
|
63
|
+
# TODO must include author and issue link — too strict
|
|
64
|
+
"TD002", "TD003",
|
|
65
|
+
# Line contains TODO — flags every TODO as a violation, unhelpful
|
|
66
|
+
"FIX002",
|
|
67
|
+
# Exception messages must be extracted into a variable before raising — adds verbosity for little gain
|
|
68
|
+
"TRY003", "EM102",
|
|
69
|
+
# Conflicts with ruff auto-formatter
|
|
70
|
+
"COM812",
|
|
71
|
+
# Replace 'open()' with 'Path.open()'
|
|
72
|
+
"PTH123",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint.isort]
|
|
76
|
+
known-first-party = ["argusf"]
|
|
77
|
+
|
|
78
|
+
[tool.ruff.lint.pycodestyle]
|
|
79
|
+
max-doc-length = 72
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint.pydocstyle]
|
|
82
|
+
convention = "google"
|
|
83
|
+
|
|
84
|
+
[tool.ruff.lint.per-file-ignores]
|
|
85
|
+
"__init__.py" = [
|
|
86
|
+
# Unused imports
|
|
87
|
+
"F401",
|
|
88
|
+
# Package docstrings — re-export shims don't need them; the few
|
|
89
|
+
# __init__ files worth documenting are documented anyway.
|
|
90
|
+
"D",
|
|
91
|
+
]
|
|
92
|
+
"src/argusf/analysis/rules/rulesets/**" = [
|
|
93
|
+
# Rule docstrings carry long-flowing prose documentation on purpose.
|
|
94
|
+
"W505",
|
|
95
|
+
"D",
|
|
96
|
+
]
|
|
97
|
+
"src/argusf/parser/treesitter/walker/handlers.py" = [
|
|
98
|
+
# Node parsing handlers are fairly self-explanatory without docstrings.
|
|
99
|
+
"D",
|
|
100
|
+
]
|
|
101
|
+
"tests/**" = [
|
|
102
|
+
# Use of assert
|
|
103
|
+
"S101",
|
|
104
|
+
# Private member access — tests legitimately inspect internals
|
|
105
|
+
"SLF001",
|
|
106
|
+
# Magic values in comparisons — expected in test assertions
|
|
107
|
+
"PLR2004",
|
|
108
|
+
# Docstring-related rules
|
|
109
|
+
"D",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
[tool.mypy]
|
|
113
|
+
python_version = "3.14"
|
|
114
|
+
mypy_path = ["src"]
|
|
115
|
+
packages = ["argusf"]
|
|
116
|
+
strict = true
|
|
117
|
+
warn_return_any = true
|
|
118
|
+
warn_unused_configs = true
|
|
119
|
+
|
|
120
|
+
[[tool.mypy.overrides]]
|
|
121
|
+
module = ["tree_sitter.*", "tree_sitter_fortran.*", "msgpack.*"]
|
|
122
|
+
ignore_missing_imports = true
|
|
123
|
+
|
|
124
|
+
[tool.pytest]
|
|
125
|
+
testpaths = ["tests"]
|
|
126
|
+
|
|
127
|
+
[tool.uv]
|
|
128
|
+
exclude-newer = "1 week"
|
|
129
|
+
package = true
|
|
130
|
+
|
|
131
|
+
[[tool.uv.index]]
|
|
132
|
+
name = "testpypi"
|
|
133
|
+
url = "https://test.pypi.org/simple"
|
|
134
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
135
|
+
explicit = true
|
|
136
|
+
|
|
137
|
+
[dependency-groups]
|
|
138
|
+
dev = [
|
|
139
|
+
"mypy>=2.1.0",
|
|
140
|
+
"pytest>=9.0.3",
|
|
141
|
+
"ruff>=0.15.12",
|
|
142
|
+
]
|
|
@@ -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
|
+
]
|
|
@@ -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}"
|