Skip to content

for_each_file

For every matching directory / file, evaluate a nested require: block with the entry as context. Template tokens ({dir}, {stem}, {ext}, {basename}, {path}, {parent_name}) expand against each match. select: is a single glob or a list with !-prefixed excludes (e.g. ["src/*", "!src/internal"]).

when_iter: — per-iteration filter. Optional expression in the when: grammar, with one extra namespace: iter.* references the entry currently being iterated. Iterations whose verdict is false are skipped before any nested rule is built — the canonical use case for monorepos shaped like Cargo / pnpm / Bazel workspaces.

The iter namespace exposes:

ReferenceTypeNotes
iter.pathstringRelative path of the iterated entry.
iter.basenamestringBasename.
iter.parent_namestringParent dir name.
iter.stemstringBasename minus the final extension (mainly useful for files).
iter.extstringFinal extension without the dot.
iter.is_dirboolTrue for for_each_dir, false for for_each_file; always available.
iter.has_file(pattern)boolGlob match relative to the iterated dir. iter.has_file("Cargo.toml"), iter.has_file("**/*.bzl"). Always false for file iteration.

when_iter: composes with the rule's outer when: (whole-rule gate, evaluated once) and with each nested rule's when: (which now also sees the same iter.* context). Same field is available on for_each_file and every_matching_has.

Token values are literal paths. When a token expands into a nested option that alint compiles as a glob (paths:, and per kind options such as dir_contains's select / require, dir_only_contains's select / allow, unique_by's select, pair's primary, or a nested for_each_*'s own select), its value is glob-escaped, so a directory named app/[slug] or pkgs/* matches only itself rather than acting as a character class or wildcard. The template text around the token keeps its glob meaning ("{path}/*.tsx"). Options that are regexes (for_each_match's select, pattern) or literal paths (pair's partner) receive the value unescaped. Messages show the real path, not its escaped form.

OptionTypeRequiredDefaultDescription
requirelist of nested ruleyesNested rules evaluated against each matched file.
selectstring or list of stringyesGlob(s) selecting the files to iterate - a single glob, or a list with !-prefixed excludes.
when_iterstringPer-iteration when: filter - see rule_for_each_dir.when_iter. iter.has_file(...) always evaluates to false on file iteration; useful predicates here include iter.basename, iter.ext, iter.parent_name.

Plus the common level, id, and when fields. This rule analyses the whole repository, so it takes no paths or expect_matches. This table is generated from the JSON Schema; option types and defaults are authoritative.

The rule fires on this repository:

  • tests/
  • tests/snapshots/
  • tests/snapshots/parser.snap✓
  • tests/unit/
  • tests/unit/lexer.rs

    ⚠ warning unit-has-snapshot

    expected a file matching [tests/snapshots/lexer.snap]

  • tests/unit/parser.rs✓
tests/snapshots/parser.snap
output
tests/unit/lexer.rs
fn t2() {}
tests/unit/parser.rs
fn t1() {}

With this .alint.yml:

version: 1
rules:
- id: unit-has-snapshot
kind: for_each_file
select: "tests/unit/*.rs"
require:
- kind: file_exists
paths: "tests/snapshots/{stem}.snap"
level: warning

alint check reports:

Terminal window
--- tests/unit/lexer.rs --------------------------------------------------------
! warning unit-has-snapshot
expected a file matching [tests/snapshots/lexer.snap]
Summary (1 violation):
! 1 warning
0 passing * 1 failing

Every unit test has a snapshot file

Section titled "Every unit test has a snapshot file"

This repository is compliant:

  • tests/
  • tests/snapshots/
  • tests/snapshots/lexer.snap✓
  • tests/snapshots/parser.snap✓
  • tests/unit/
  • tests/unit/lexer.rs✓
  • tests/unit/parser.rs✓

With this .alint.yml:

version: 1
rules:
- id: unit-has-snapshot
kind: for_each_file
select: "tests/unit/*.rs"
require:
- kind: file_exists
paths: "tests/snapshots/{stem}.snap"
level: warning

alint check reports:

Terminal window
v All 1 rule(s) passed.