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.
Files changed (78) hide show
  1. argusf-0.1.0.dev0/LICENSE +21 -0
  2. argusf-0.1.0.dev0/PKG-INFO +166 -0
  3. argusf-0.1.0.dev0/README.md +137 -0
  4. argusf-0.1.0.dev0/pyproject.toml +142 -0
  5. argusf-0.1.0.dev0/src/argusf/__init__.py +3 -0
  6. argusf-0.1.0.dev0/src/argusf/analysis/__init__.py +14 -0
  7. argusf-0.1.0.dev0/src/argusf/analysis/engine.py +100 -0
  8. argusf-0.1.0.dev0/src/argusf/analysis/rule.py +45 -0
  9. argusf-0.1.0.dev0/src/argusf/analysis/rules/__init__.py +5 -0
  10. argusf-0.1.0.dev0/src/argusf/analysis/rules/documentation.py +30 -0
  11. argusf-0.1.0.dev0/src/argusf/analysis/rules/findings.py +67 -0
  12. argusf-0.1.0.dev0/src/argusf/analysis/rules/registry.py +22 -0
  13. argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/__init__.py +4 -0
  14. argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/__init__.py +22 -0
  15. argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus001_no_goto.py +54 -0
  16. argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus002_common_block.py +55 -0
  17. argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus003_equivalence.py +55 -0
  18. argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus004_implicit_typing.py +129 -0
  19. argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus005_arithmetic_if.py +62 -0
  20. argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus006_entry_statement.py +67 -0
  21. argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus007_pause_statement.py +93 -0
  22. argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus008_todo_comment.py +100 -0
  23. argusf-0.1.0.dev0/src/argusf/analysis/rules/rulesets/rgus/rgus009_unsorted_use_statements.py +258 -0
  24. argusf-0.1.0.dev0/src/argusf/analysis/rules/selection.py +83 -0
  25. argusf-0.1.0.dev0/src/argusf/analysis/suppression.py +92 -0
  26. argusf-0.1.0.dev0/src/argusf/analysis/syntax_errors.py +41 -0
  27. argusf-0.1.0.dev0/src/argusf/autofix/__init__.py +26 -0
  28. argusf-0.1.0.dev0/src/argusf/autofix/applier.py +130 -0
  29. argusf-0.1.0.dev0/src/argusf/autofix/context.py +82 -0
  30. argusf-0.1.0.dev0/src/argusf/autofix/diff.py +39 -0
  31. argusf-0.1.0.dev0/src/argusf/autofix/edits.py +31 -0
  32. argusf-0.1.0.dev0/src/argusf/autofix/engine.py +192 -0
  33. argusf-0.1.0.dev0/src/argusf/autofix/models.py +39 -0
  34. argusf-0.1.0.dev0/src/argusf/autofix/source.py +68 -0
  35. argusf-0.1.0.dev0/src/argusf/autofix/writer.py +53 -0
  36. argusf-0.1.0.dev0/src/argusf/cache/__init__.py +5 -0
  37. argusf-0.1.0.dev0/src/argusf/cache/backend.py +294 -0
  38. argusf-0.1.0.dev0/src/argusf/cli.py +428 -0
  39. argusf-0.1.0.dev0/src/argusf/config/__init__.py +1 -0
  40. argusf-0.1.0.dev0/src/argusf/config/config_resolver.py +212 -0
  41. argusf-0.1.0.dev0/src/argusf/config/errors.py +8 -0
  42. argusf-0.1.0.dev0/src/argusf/config/file_reader/__init__.py +3 -0
  43. argusf-0.1.0.dev0/src/argusf/config/file_reader/reader.py +58 -0
  44. argusf-0.1.0.dev0/src/argusf/config/models.py +110 -0
  45. argusf-0.1.0.dev0/src/argusf/config/validation.py +113 -0
  46. argusf-0.1.0.dev0/src/argusf/constants.py +66 -0
  47. argusf-0.1.0.dev0/src/argusf/diagnostics.py +45 -0
  48. argusf-0.1.0.dev0/src/argusf/discovery/__init__.py +3 -0
  49. argusf-0.1.0.dev0/src/argusf/discovery/backend.py +250 -0
  50. argusf-0.1.0.dev0/src/argusf/discovery/gitignore.py +125 -0
  51. argusf-0.1.0.dev0/src/argusf/discovery/repo.py +30 -0
  52. argusf-0.1.0.dev0/src/argusf/hashing/__init__.py +5 -0
  53. argusf-0.1.0.dev0/src/argusf/hashing/hash.py +48 -0
  54. argusf-0.1.0.dev0/src/argusf/ir/__init__.py +1 -0
  55. argusf-0.1.0.dev0/src/argusf/ir/models/__init__.py +49 -0
  56. argusf-0.1.0.dev0/src/argusf/ir/models/findings.py +74 -0
  57. argusf-0.1.0.dev0/src/argusf/ir/models/source.py +199 -0
  58. argusf-0.1.0.dev0/src/argusf/orchestrator.py +127 -0
  59. argusf-0.1.0.dev0/src/argusf/parser/__init__.py +1 -0
  60. argusf-0.1.0.dev0/src/argusf/parser/backend.py +24 -0
  61. argusf-0.1.0.dev0/src/argusf/parser/treesitter/__init__.py +1 -0
  62. argusf-0.1.0.dev0/src/argusf/parser/treesitter/backend.py +157 -0
  63. argusf-0.1.0.dev0/src/argusf/parser/treesitter/walker/__init__.py +5 -0
  64. argusf-0.1.0.dev0/src/argusf/parser/treesitter/walker/handlers.py +209 -0
  65. argusf-0.1.0.dev0/src/argusf/parser/treesitter/walker/walker.py +82 -0
  66. argusf-0.1.0.dev0/src/argusf/registry/__init__.py +3 -0
  67. argusf-0.1.0.dev0/src/argusf/registry/registry.py +69 -0
  68. argusf-0.1.0.dev0/src/argusf/reporting/__init__.py +22 -0
  69. argusf-0.1.0.dev0/src/argusf/reporting/formatting.py +162 -0
  70. argusf-0.1.0.dev0/src/argusf/reporting/registry.py +20 -0
  71. argusf-0.1.0.dev0/src/argusf/reporting/reporters/__init__.py +15 -0
  72. argusf-0.1.0.dev0/src/argusf/reporting/reporters/base.py +29 -0
  73. argusf-0.1.0.dev0/src/argusf/reporting/reporters/concise.py +35 -0
  74. argusf-0.1.0.dev0/src/argusf/reporting/reporters/diff.py +30 -0
  75. argusf-0.1.0.dev0/src/argusf/reporting/reporters/json.py +59 -0
  76. argusf-0.1.0.dev0/src/argusf/reporting/reporters/null.py +21 -0
  77. argusf-0.1.0.dev0/src/argusf/reporting/reporters/standard.py +78 -0
  78. 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: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
34
+ [![Python Version](https://img.shields.io/badge/python-3.14+-blue.svg)](https://www.python.org/)
35
+ <!-- [![CI](https://github.com/mmcateer13/argusf/actions/workflows/YOUR_WORKFLOW.yml/badge.svg)](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: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ [![Python Version](https://img.shields.io/badge/python-3.14+-blue.svg)](https://www.python.org/)
6
+ <!-- [![CI](https://github.com/mmcateer13/argusf/actions/workflows/YOUR_WORKFLOW.yml/badge.svg)](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,3 @@
1
+ """Argusf: a static code analyzer for Fortran 90."""
2
+
3
+ __version__ = "0.1.0"
@@ -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,5 @@
1
+ # Import rulesets to trigger registration of the built-in rules.
2
+ from . import rulesets
3
+ from .registry import RuleRegistry, register
4
+
5
+ __all__ = ["RuleRegistry", "register", "rulesets"]
@@ -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}"