Skip to content

Add per-file ignores and update tags - #13

Merged
shelkesays merged 4 commits into
mainfrom
development
Apr 24, 2026
Merged

shelkesays merged 4 commits into
mainfrom
development

Conversation

@shelkesays

@shelkesays shelkesays commented Apr 24, 2026 •

Copy link
Copy Markdown
Owner

This pull request introduces support for per-file rule suppression in safelint, allowing specific rules to be ignored for files matching given glob patterns. It updates the configuration schema, engine logic, and documentation to explain and enable this feature. There are also minor improvements to suppression reporting and documentation clarity.

Per-file ignore feature:

  • Added support for a per_file_ignores key in the safelint configuration, allowing specific rules to be suppressed for files matching glob patterns. This is implemented in the engine and documented in both TOML and YAML formats. [1] [2] [3] [4] [5] [6]

Documentation updates:

  • Expanded documentation in CONFIGURATION.md and README.md to explain the new per-file ignore mechanism, including configuration examples, comparison with other suppression methods, and references. [1] [2] [3]

Suppression reporting:

  • Updated CLI summary wording to reflect that suppressed violations may come from both # nosafe and per-file ignores, making suppression counts more accurate and general.

Tooling update:

  • Upgraded the safelint pre-commit hook to version 1.3.0 to ensure the new features are available in CI and local checks.

Summary by CodeRabbit

  • New Features

    • Added per_file_ignores configuration option to suppress specific rules for files matching glob patterns.
  • Documentation

    • Updated configuration documentation with examples for per-file ignores in TOML and YAML formats.
    • Updated README with guidance on pattern-based rule suppression.
  • Chores

    • Updated SafeLint dependency to v1.3.0.

@coderabbitai

coderabbitai Bot commented Apr 24, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@shelkesays has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 43 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 41 minutes and 43 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0b0276f3-4d69-4c89-a80d-e73cc4e06aa8

📥 Commits

Reviewing files that changed from the base of the PR and between 667aa35 and d6cce21.

📒 Files selected for processing (3)
  • CONFIGURATION.md
  • pyproject.toml
  • src/safelint/core/engine.py
📝 Walkthrough

Walkthrough

The changes introduce support for a new per_file_ignores configuration option that allows suppressing specific rules for files matching glob patterns. The feature is implemented across configuration defaults, engine logic, documentation, and CLI output reporting, with supporting updates to pre-commit configuration and README examples.

Changes

Cohort / File(s) Summary
Pre-commit & Documentation
.pre-commit-config.yaml, CONFIGURATION.md, README.md
SafeLint hook version bumped from v1.2.2 to v1.3.0; new per_file_ignores configuration option documented in both TOML and YAML formats with conceptual guidance distinguishing it from enabled: false, exclude_paths, and inline # nosafe suppression; README examples updated to demonstrate per-file glob-pattern rule suppression.
Configuration & Engine
src/safelint/core/config.py, src/safelint/core/engine.py
Configuration defaults expanded to include per_file_ignores: {}; engine now computes accumulated ignored rules for each file by matching against configured glob patterns and filters violations accordingly during check_file, with suppressed counts and fail-fast behavior updated to reflect post-filtering state.
CLI Output
src/safelint/cli.py
Summary text updated to report suppressed violation counts without attributing all suppression to # nosafe alone, now accounting for per-file ignores.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI
    participant ConfigLoader
    participant Engine
    participant PatternMatcher

    User->>CLI: Run safelint
    CLI->>ConfigLoader: Load configuration
    ConfigLoader->>ConfigLoader: Merge per_file_ignores defaults
    ConfigLoader-->>CLI: Config with per_file_ignores patterns
    
    loop For each file
        CLI->>Engine: check_file(file_path, config)
        Engine->>PatternMatcher: Match file against glob patterns
        PatternMatcher-->>Engine: Matched ignore patterns
        Engine->>Engine: Accumulate ignored rule names/codes
        Engine->>Engine: Filter violations by per_file ignores
        Engine->>Engine: Filter violations by inline # nosafe
        Engine-->>CLI: Violations, suppressed_count
    end
    
    CLI->>CLI: Generate summary with suppressed counts
    CLI-->>User: Report results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • #12 — Modifies the same src/safelint/core/config.py DEFAULTS and src/safelint/core/engine.py to implement rule suppression through configuration.
  • #6 — Updates SafetyEngine.check_file filtering logic, violation suppression tracking, and CLI summary output in the same areas.
  • #7 — Updates the SafeLint pre-commit hook revision in .pre-commit-config.yaml to a different version bump.

Poem

🐰 Hopping through patterns, glob by glob,
Per-file ignores make rules stop!
Each file its own suppression tale,
Fine-grained control without the flail,
Sweet suppression—no more fail! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title partially captures the main changes but is vague; 'update tags' does not clearly convey what was actually updated, and the emphasis obscures the primary feature addition. Consider a more specific title like 'Add per-file rule ignores configuration' or 'Implement per-file ignores feature' to better reflect the primary change.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CONFIGURATION.md (1)

87-101: ⚠️ Potential issue | 🟡 Minor

Example output is stale — CLI no longer emits "via # nosafe".

_make_summary in src/safelint/cli.py was changed in this PR to print a generic (N suppressed) suffix regardless of suppression source. The two code blocks below still reference the old wording, which will confuse users comparing docs against actual output.

📝 Proposed doc fix
 Found 2 errors, 1 warning. [--fail-on=error].
-No fixes available (safelint does not auto-fix violations). (3 suppressed via # nosafe)
+No fixes available (safelint does not auto-fix violations). (3 suppressed)
-All checks passed. (3 suppressed via # nosafe)
-No fixes available (safelint does not auto-fix violations). (3 suppressed via # nosafe)
+All checks passed. (3 suppressed)
+No fixes available (safelint does not auto-fix violations). (3 suppressed)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CONFIGURATION.md` around lines 87 - 101, The example output in
CONFIGURATION.md is stale: update the two example code blocks under "End-of-run
summary" to match the current _make_summary behavior (it now emits a generic "(N
suppressed)" suffix rather than "via # nosafe"); specifically, replace
occurrences of " (3 suppressed via # nosafe)" with " (3 suppressed)" and adjust
both examples to show the generic "(N suppressed)" text so the docs match the
output generated by the _make_summary function.
🧹 Nitpick comments (3)
src/safelint/core/engine.py (2)

189-205: Docstring is now stale — check_file also applies per_file_ignores, and suppressed now counts both sources.

The docstring still describes only inline # nosafe suppression and says suppressed reflects only those filtered out "by an inline # nosafe directive". That's no longer accurate: lines 235–236 add a second filtering pass, and both contribute to LintResult.suppressed. The fail_fast description is also now about after_pfi rather than only the # nosafe remainder.

📝 Suggested docstring update
-        Violations on lines that carry a ``# nosafe`` comment (optionally
-        followed by a comma-separated list of codes / rule names) are filtered
-        out and counted in :attr:`LintResult.suppressed` instead of appearing
-        in :attr:`LintResult.violations`.
-
-        When ``fail_fast`` is enabled the loop stops after the first rule that
-        produces at least one unsuppressed (active) violation, i.e. a violation
-        that is not filtered out by an inline ``# nosafe`` directive.
+        Violations are filtered in two passes and counted in
+        :attr:`LintResult.suppressed` instead of appearing in
+        :attr:`LintResult.violations`:
+
+        1. Inline ``# nosafe`` comments on the violation's line (optionally
+           followed by a comma-separated list of codes / rule names).
+        2. Rule codes / names listed under any ``per_file_ignores`` glob
+           pattern that matches *filepath*.
+
+        When ``fail_fast`` is enabled the loop stops after the first rule
+        that produces at least one violation surviving both filters.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/safelint/core/engine.py` around lines 189 - 205, Update the check_file
docstring to reflect that suppression comes from both inline "# nosafe"
directives and per_file_ignores: state that violations filtered by either
mechanism are counted in LintResult.suppressed, and clarify that when fail_fast
is enabled the loop stops after the first rule that produces at least one
unsuppressed violation after applying per_file_ignores (i.e., after the second
filtering pass). Mention the names per_file_ignores and LintResult.suppressed in
the text so readers can locate the relevant behavior.

170-171: Consider validating and warning on unknown entries in per_file_ignores, and splitting the comprehension for readability.

Two small points on this initializer:

  1. Consistency with ignore. Lines 141–146 above emit a _log.warning(...) when the global ignore list contains entries that match neither a known rule name nor a known code (typo / stale rule). The same class of bug silently no-ops here — a misspelled "SAFE1O1" in per_file_ignores never fires. Reusing the existing known_names / known_codes_upper sets for per-file entries would catch these cleanly.
  2. Robustness. If a user writes per_file_ignores = ["SAFE101"] (list instead of mapping) or "tests/**" = "SAFE101" (bare string instead of list), the current code will raise AttributeError / TypeError from deep inside the engine constructor rather than a friendly config error. A type-check on raw_pfi / entries would fail fast with a clear message.
  3. Nit. The single comprehension packs three responsibilities into one line; wrapping it makes the (pattern, names, codes_upper) shape obvious.
♻️ Proposed refactor
-        raw_pfi: dict[str, list[str]] = config.get("per_file_ignores", {})
-        self.per_file_ignores: list[tuple[str, frozenset[str], frozenset[str]]] = [(pattern, frozenset(entries), frozenset(e.upper() for e in entries)) for pattern, entries in raw_pfi.items()]
+        raw_pfi: dict[str, list[str]] = config.get("per_file_ignores", {}) or {}
+        if not isinstance(raw_pfi, dict):
+            _log.warning("per_file_ignores must be a mapping of glob → list; got %r — ignoring", type(raw_pfi).__name__)
+            raw_pfi = {}
+        self.per_file_ignores: list[tuple[str, frozenset[str], frozenset[str]]] = []
+        for pattern, entries in raw_pfi.items():
+            entries = list(entries or [])
+            unknown_pfi = frozenset(
+                e for e in entries if e not in known_names and e.upper() not in known_codes_upper
+            )
+            if unknown_pfi:
+                _log.warning(
+                    "Unknown entries in per_file_ignores[%r]: %s",
+                    pattern,
+                    ", ".join(sorted(unknown_pfi)),
+                )
+            self.per_file_ignores.append(
+                (pattern, frozenset(entries), frozenset(e.upper() for e in entries)),
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/safelint/core/engine.py` around lines 170 - 171, Validate and normalize
per-file ignores by first checking that raw_pfi is a mapping and each entries
value is an iterable of strings, then iterate (not in a single comprehension)
over raw_pfi and for each pattern build (pattern, frozenset(names),
frozenset(codes_upper)) but also check each entry against the existing
known_names and known_codes_upper sets and call _log.warning when an entry
matches neither (similar to the global ignore logic); on type mismatches raise a
clear ValueError/TypeError mentioning invalid per_file_ignores so callers get a
friendly config error instead of an AttributeError deep in the constructor, and
assign the validated list to self.per_file_ignores.
CONFIGURATION.md (1)

184-221: New section reads clearly — one follow-up on glob semantics.

The comparison table at lines 212–218 is a particularly helpful addition. Two small suggestions:

  • Consider explicitly stating which glob dialect per_file_ignores (and exclude_paths) use. Today the engine delegates to pathlib.PurePath.match, whose ** semantics differ from glob/shell and only gained recursive support in Python 3.13 — see the separate comment on src/safelint/core/engine.py. Once that is addressed (or if you pin to full_match()), a one-line note like "Patterns follow shell-glob semantics; ** matches any number of path segments" will save users trial-and-error.
  • Minor nit: the YAML example (lines 196–206) omits migrations/**'s "src/legacy/**" counterpart shown in the TOML block. Not important, but symmetry helps copy-paste parity.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CONFIGURATION.md` around lines 184 - 221, Update the CONFIGURATION.md
per_file_ignores section to (1) add a one-line note explaining the glob dialect
used by per_file_ignores and exclude_paths — state that patterns currently
follow Python's pathlib.PurePath.match (shell-glob-like) semantics where "**"
matches any number of path segments (or indicate intent to switch to a
full_match behavior once src/safelint/core/engine.py is changed), and (2) make
the YAML example mirror the TOML example by adding the missing "src/legacy/**"
entry under per_file_ignores so the two examples are symmetric; reference the
per_file_ignores and exclude_paths terminology and pathlib.PurePath.match when
editing the text.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/safelint/cli.py`:
- Line 134: Update the CONFIGURATION.md example blocks to match the CLI's new
summary wording produced by suppressed_note (the string now uses "{n}
suppressed" instead of "{n} suppressed via # nosafe"); specifically, edit the
two fenced code examples that currently show "(3 suppressed via # nosafe)" and
change them to "(3 suppressed)" so docs reflect the output generated by
suppressed_note; optionally note that if you later choose to split counters
(e.g., suppressed_inline vs suppressed_per_file) you should update these
examples again to show both counts.

In `@src/safelint/core/engine.py`:
- Around line 178-187: Path.match is incorrectly used for glob patterns with **
(used in _file_ignored_set and _is_excluded) and fails to match nested files on
Python ≤3.12; change matching to normalize the Path to a POSIX string and use
fnmatch.fnmatchcase(path.as_posix(), pattern) (or detect Python ≥3.13 and use
PurePath.full_match for that runtime) so that patterns like "tests/**" and
"migrations/**" behave as documented; update both _file_ignored_set and
_is_excluded to perform matching via the POSIX-string + fnmatch approach (or the
conditional full_match fallback) instead of Path.match.

---

Outside diff comments:
In `@CONFIGURATION.md`:
- Around line 87-101: The example output in CONFIGURATION.md is stale: update
the two example code blocks under "End-of-run summary" to match the current
_make_summary behavior (it now emits a generic "(N suppressed)" suffix rather
than "via # nosafe"); specifically, replace occurrences of " (3 suppressed via #
nosafe)" with " (3 suppressed)" and adjust both examples to show the generic "(N
suppressed)" text so the docs match the output generated by the _make_summary
function.

---

Nitpick comments:
In `@CONFIGURATION.md`:
- Around line 184-221: Update the CONFIGURATION.md per_file_ignores section to
(1) add a one-line note explaining the glob dialect used by per_file_ignores and
exclude_paths — state that patterns currently follow Python's
pathlib.PurePath.match (shell-glob-like) semantics where "**" matches any number
of path segments (or indicate intent to switch to a full_match behavior once
src/safelint/core/engine.py is changed), and (2) make the YAML example mirror
the TOML example by adding the missing "src/legacy/**" entry under
per_file_ignores so the two examples are symmetric; reference the
per_file_ignores and exclude_paths terminology and pathlib.PurePath.match when
editing the text.

In `@src/safelint/core/engine.py`:
- Around line 189-205: Update the check_file docstring to reflect that
suppression comes from both inline "# nosafe" directives and per_file_ignores:
state that violations filtered by either mechanism are counted in
LintResult.suppressed, and clarify that when fail_fast is enabled the loop stops
after the first rule that produces at least one unsuppressed violation after
applying per_file_ignores (i.e., after the second filtering pass). Mention the
names per_file_ignores and LintResult.suppressed in the text so readers can
locate the relevant behavior.
- Around line 170-171: Validate and normalize per-file ignores by first checking
that raw_pfi is a mapping and each entries value is an iterable of strings, then
iterate (not in a single comprehension) over raw_pfi and for each pattern build
(pattern, frozenset(names), frozenset(codes_upper)) but also check each entry
against the existing known_names and known_codes_upper sets and call
_log.warning when an entry matches neither (similar to the global ignore logic);
on type mismatches raise a clear ValueError/TypeError mentioning invalid
per_file_ignores so callers get a friendly config error instead of an
AttributeError deep in the constructor, and assign the validated list to
self.per_file_ignores.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 65756005-2541-4649-a146-d456c24c015a

📥 Commits

Reviewing files that changed from the base of the PR and between 0952e0b and 667aa35.

📒 Files selected for processing (6)
  • .pre-commit-config.yaml
  • CONFIGURATION.md
  • README.md
  • src/safelint/cli.py
  • src/safelint/core/config.py
  • src/safelint/core/engine.py

Comment thread src/safelint/cli.py
Comment thread src/safelint/core/engine.py
@shelkesays
shelkesays merged commit 09fac3d into main Apr 24, 2026
1 check passed
shelkesays added a commit that referenced this pull request Sep 23, 2026
…lls, Python config validation, dead code

Code-review findings 2, 4, 13, 14, 16 on the 2.13.0 taint work.

#2 (security, all seven trackers): a property cleared by a property-typed
sanitiser survived an intervening UNKNOWN call, so
`htmlSink(unescape(escape(u)))` read clean - an unknown call may transform or
DECODE its input and cannot be trusted to carry a safety property through. The
worklist now carries a per-node property: `_taint_step` / `_classify_call`
return `(bool, list[(node, property)])`, propagating and template children keep
the incoming property, and the unknown-call branch descends with `None` so any
taint reaching those children re-taints the result for every property. Raw
taint propagation is unchanged. Regression tests at tracker level (Python) and
end-to-end (JavaScript).

#4: `_resolve_core_lists` gained a `defaults` parameter so the Python path gets
the same `_validated_string_list` typo/scalar guard as the other languages
without losing its ClassVar fallback lists.

#13: `PropertyContract.all_properties` unioned the sanitiser properties and the
sink properties, so a property no sink ever requires still cost a full extra
tree traversal per assignment. Intersect instead - only properties that are
both established and required can change a verdict.

#14: restore the `combined == frozenset()` short-circuit in the Go tracker's
`_propagate` (a value already safe for nothing cannot be changed by further
ORs).

#16: drop the dead `ANNOTATED_ASSIGNMENT` dispatch branch and
`_visit_ann_assignment` - tree-sitter-python has no such node kind (annotated
assignments parse as `assignment`), so the branch was unreachable. The unused
constant goes with it.

Full gate green (pytest 2158 / 97.18%).
shelkesays added a commit that referenced this pull request Sep 24, 2026
…lls, Python config validation, dead code

Code-review findings 2, 4, 13, 14, 16 on the 2.13.0 taint work.

#2 (security, all seven trackers): a property cleared by a property-typed
sanitiser survived an intervening UNKNOWN call, so
`htmlSink(unescape(escape(u)))` read clean - an unknown call may transform or
DECODE its input and cannot be trusted to carry a safety property through. The
worklist now carries a per-node property: `_taint_step` / `_classify_call`
return `(bool, list[(node, property)])`, propagating and template children keep
the incoming property, and the unknown-call branch descends with `None` so any
taint reaching those children re-taints the result for every property. Raw
taint propagation is unchanged. Regression tests at tracker level (Python) and
end-to-end (JavaScript).

#4: `_resolve_core_lists` gained a `defaults` parameter so the Python path gets
the same `_validated_string_list` typo/scalar guard as the other languages
without losing its ClassVar fallback lists.

#13: `PropertyContract.all_properties` unioned the sanitiser properties and the
sink properties, so a property no sink ever requires still cost a full extra
tree traversal per assignment. Intersect instead - only properties that are
both established and required can change a verdict.

#14: restore the `combined == frozenset()` short-circuit in the Go tracker's
`_propagate` (a value already safe for nothing cannot be changed by further
ORs).

#16: drop the dead `ANNOTATED_ASSIGNMENT` dispatch branch and
`_visit_ann_assignment` - tree-sitter-python has no such node kind (annotated
assignments parse as `assignment`), so the branch was unreachable. The unused
constant goes with it.

Full gate green (pytest 2158 / 97.18%).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant