To install from source, you must first have a working Rust environment (see
rustup). The project may then be installed from the project
root directory using either pip
:
python -m venv venv # Or use your preferred virtual environment method...
source venv/bin/activate
pip install .[lint]
Or using cargo
:
cargo install --path fortitude
Unit tests can be run by calling:
cargo test
You'll also need Insta to update snapshot tests:
cargo install cargo-insta
When contributing, please use cargo clippy
for linting and cargo fmt
for formatting.
If you edit any Python code, please also use ruff check
and ruff format
. To avoid
accidentally pushing unlinted/unformatted code to GitHub, we recommend using the Git
pre-commit hook provided:
git config --local core.hooksPath .githooks
We're always open to new rule suggestions, and would be very grateful for any assistance in implementing them. Before raising an issue to suggest a new rule, please check to see if it has already been suggested.
There are several steps required to add a new rule to Fortitude:
- Decide on a name and category following our naming rules.
- Create a new file
fortitude/src/rules/category/rule_name.rs
, wherecategory
is your chosen rule category andrule_name
is its name. If there is already a file for a similar rule, you may also choose to add your rule there. - In that file, define a
Violation
struct. This defines the diagnostic messages raised when your rule is violated. - Implement one of
TextRule
,AstRule
orPathRule
for yourViolation
. These are, respectively, rules that check a file line-by-line, rules that analyse the AST, and rules that analyse the path to a Fortran file directly.- Most rules are
AstRules
, which usetree_sitter
to analyse the code. If you want to see howtree_sitter
parses a given file, we recommended installingtree_sitter
, cloningtree_sitter_fortran
, and then running the following from thetree_sitter_fortran
root directory:
- Most rules are
tree-sitter build
tree-sitter parse /path/to/fortran/file.f90
- Map the
Violation
struct to a rule code infortitude/src/rules/mod.rs
.code_to_rule
is never called directly, but the match statement within is analysed by the macrofortitude_macros::map_codes
to define aRule
enum and many associated utilities.- The first two digits for a rule code normally define a subcategory, while the
last digit denotes the specific rule within that subcategory. The last digit
should not be zero. For example,
T04x
defines rules related to the use of assumed-size arrays and character strings. This isn't stringently enforced, but you may be asked to renumber the rule if other developers think it would better fit somewhere else. - New rules should be in
RuleGroup::Preview
.
- Add a test for your rule. Try to consider edge cases and any scenarios where false positives could occur.
- Update the generated documentation using
cargo dev generate-all
.
For some rules, it may be possible to automatically apply a fix for the user,
though it isn't essential to include a fix when adding a new rule. These are
typically applied using
Fix
and
Edit
from Ruff. A fix may be one of:
- 'Safe': Applying the fix is guaranteed to not change the behaviour of the user's program. This will normally only apply to stylistic changes.
- 'Unsafe': Applying the fix may change the behaviour of the user's program in some edge cases.
- 'Display only': Fortitude can guess at a solution, but makes no guarantees to its correctness or safety.
If you help writing rules, we recommend checking the implementation of existing rules to see if anything similar already exists. You can also raise a draft pull request to ask for assistance from other developers.
Similarly to
Ruff, the name
of a rule should describe the pattern the rule is intended to fix. Words such as
'forbid' should be omitted. For example, the name for the rule that warns of
overly long lines is LineTooLong
, and not something like AvoidLineTooLong
or
KeepLinesShort
.
Rules should also be categorised appropriately. For example, if a rule is
intended to discourage the use of outdated features, it may go under
Obsolescent
, or perhaps a more specific category such as Modules
or
Typing
. If the rule only affects code readability, it should go under Style
.
The boundaries between categories are not always clear, so the exact name and category of a rule is often determined following a discussion after a pull request has been raised.
To test rules, Fortitude uses snapshots of Fortitude's output for a given file (fixture). Generally, there
will be one file per rule (e.g., E402.f90
), and each file will contain all necessary examples of
both violations and non-violations. cargo insta review
will generate a snapshot file containing
Fortitude's output for each fixture, which you can then commit alongside your changes.
Once you've completed the code for the rule itself, you can define tests with the following steps:
-
Add a Fortran file to
fortitude/resources/test/fixtures/[category]
that contains the code you want to test. The file name should match the rule name (e.g.,E402.f90
), and it should include examples of both violations and non-violations. -
Run Fortitude locally against your file and verify the output is as expected. Once you're satisfied with the output (you see the violations you expect, and no others), proceed to the next step. For example, if you're adding a new rule named
E402
, you would run:cargo run -- check fortitude/resources/test/fixtures/typing/E402.f90 --select E402
Note: Only a subset of rules are enabled by default. When testing a new rule, ensure that you activate it by adding
--select ${rule_code}
to the command, and if the rule is in thePreview
category, add--preview
as well. -
Add the test to the relevant
fortitude/src/rules/[category]/mod.rs
file. If you're contributing a rule to a pre-existing set, you should be able to find a similar example to pattern-match against. If you're adding a new category, you'll need to create a newmod.rs
file (see, e.g.,fortitude/src/rules/typing/mod.rs
) -
Run
cargo test
. Your test will fail, but you'll be prompted to follow-up withcargo insta review
. Runcargo insta review
, review and accept the generated snapshot, then commit the snapshot file alongside the rest of your changes. -
Run
cargo test
again to ensure that your test passes.
The documentation can be built locally using:
pip install mkdocs-material
mkdocs serve
To make a new release, the following steps must be completed in order:
-
Move rules out of preview mode/into deprecated mode (if applicable).
-
Make sure the generated docs are up-to-date:
cargo dev generate-all
-
Install
uv
:curl -LsSf https://astral.sh/uv/install.sh | sh
-
Run
./scripts/release.sh
; this command will:- Generate a temporary virtual environment with
rooster
- Generate a changelog entry in
CHANGELOG.md
- Update versions in
pyproject.toml
andCargo.toml
- Update references to versions in the
README.md
and documentation - Display contributors for the release
- Generate a temporary virtual environment with
-
rooster
currently doesn't updateCITATION.cff
, so this needs to be done manually for now -
The changelog should then be editorialised for consistency
- Often labels will be missing from pull requests they will need to be manually organized into the proper section
- Changes should be edited to be user-facing descriptions, avoiding internal details
-
Highlight any breaking changes in
BREAKING_CHANGES.md
-
Run
cargo check
. This should update the lock file with new versions. -
Create a pull request with the changelog and version updates
-
Merge the PR
-
Run the release workflow with:
- The new version number
-
The release workflow will do the following:
- Build all the assets. If this fails (even though we tested in step 4), we haven't tagged or uploaded anything, you can restart after pushing a fix. If you just need to rerun the build, make sure you're re-running all the failed jobs and not just a single failed job.
- Upload to PyPI.
- Create and push the Git tag (as extracted from
pyproject.toml
). We create the Git tag only after building the wheels and uploading to PyPI, since we can't delete or modify the tag (#4468). - Attach artifacts to draft GitHub release
-
Verify the GitHub release:
- The Changelog should match the content of
CHANGELOG.md
- Append the contributors from the
scripts/release.sh
script
- The Changelog should match the content of
Pushing to crates.io
is currently not possible as some of our dependencies point
to GitHub repositories. We'll be able to restart using crates.io
if Ruff starts
publishing there.