Skip to content

[CI]: Add automated linter tool for repo-level rules#22

Closed
spencrr wants to merge 2 commits into
mainfrom
spencrr/linter-tool
Closed

[CI]: Add automated linter tool for repo-level rules#22
spencrr wants to merge 2 commits into
mainfrom
spencrr/linter-tool

Conversation

@spencrr

@spencrr spencrr commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Description

Add rampart_lint — a custom AST-based linter that enforces two project coding conventions that ruff cannot check natively:

  • RAMPART001 — async functions must end with _async suffix (dunders exempt)
  • RAMPART002 — functions with >1 non-self/cls parameter must use keyword-only * (dunders exempt)

Architecture

tools/rampart_lint/
  __main__.py           # CLI: argparse subcommands (check, list, explain)
  driver.py             # engine: AST walking, dispatch, noqa suppression
  rule.py               # Rule protocol, LocatedNode type alias, shared utilities
  rules/
    rampart001_async_suffix.py
    rampart002_keyword_only.py
  tests/                # co-located, 66 tests

Rules implement a Rule protocol and register concrete AST node types.

Suppression follows ruff semantics — same-line # noqa: RAMPART001 or bare # noqa. Codes are declared as external in ruff config so RUF100 won't flag them.

Integration

  • pre-commit: rampart-lint hook runs on rampart files
  • CI: added RAMPART lint step to lint job; test job now uses testpaths from pyproject.toml (includes tool tests)
  • pyproject.toml: external, testpaths, pyright include, per-file-ignores for tools

Existing code

Added # noqa: RAMPART002 to 3 pytest hook functions in plugin.py whose signatures are defined by the pytest framework.

Breaking changes

None

Checklist

  • pre-commit run --all-files passes
  • Tests added or updated for changes
  • Documentation updated

@spencrr spencrr marked this pull request as ready for review April 21, 2026 20:28
@spencrr spencrr force-pushed the spencrr/touchup-coding-standards branch from 72106fd to 2128925 Compare April 21, 2026 21:29
@spencrr spencrr requested a review from a team as a code owner April 21, 2026 21:29
@spencrr spencrr force-pushed the spencrr/linter-tool branch from cb6ab83 to a2bbdb6 Compare April 21, 2026 21:29
@nina-msft

Copy link
Copy Markdown
Contributor

I don't see any changes to RAMPART002 in this PR for existing files, just additions of missing _async suffix from RAMPART001. Just checking that you ran this and nothing was flagged by this rule?

@nina-msft

Copy link
Copy Markdown
Contributor

Instead of writing a custom linter - have you considered creating custom flake8 plugins for these rules?

I asked GHCLI if existing linters can replace this custom tool & then asked for more details about how we'd implement, see output below :-)


Plugin file (~40 lines)


 # flake8_rampart.py
 import ast
 from typing import Generator
 
 class RampartChecker:
     name = "flake8-rampart"
     version = "1.0.0"
 
     def __init__(self, tree: ast.AST) -> None:
         self.tree = tree
 
     def run(self) -> Generator[tuple[int, int, str, type], None, None]:
         for node in ast.walk(self.tree):
             if isinstance(node, ast.AsyncFunctionDef):
                 if not _is_dunder(node.name) and not node.name.endswith("_async"):
                     yield (node.lineno, node.col_offset,
                            f"RAMPART001 async function `{node.name}` missing `_async` suffix",
                            type(self))
 
             if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                 if not _is_dunder(node.name):
                     positional = node.args.posonlyargs + node.args.args
                     non_self = [a for a in positional if a.arg not in ("self", "cls")]
                     if len(non_self) > 1:
                         yield (node.lineno, node.col_offset,
                                f"RAMPART002 function `{node.name}` has {len(non_self)} "
                                f"positional params — use `*` to enforce keyword-only arguments",
                                type(self))
 
 def _is_dunder(name: str) -> bool:
     return name.startswith("__") and name.endswith("__")

Registration via pyproject.toml

[project.entry-points."flake8.extension"]
RAMPART = "flake8_rampart:RampartChecker"

The entry point key (RAMPART) must match the prefix of your error codes. Flake8 uses this to route # noqa: RAMPART001 suppression automatically.

What you get for free

  • noqa suppression — # noqa: RAMPART001, bare # noqa, all handled by flake8
  • CLI — flake8 --select RAMPART001,RAMPART002, --extend-ignore, etc.
  • File filtering — per-file-ignores in config
  • Pre-commit — use the standard flake8 hook

Trade-off vs. the custom linter

The custom linter in this PR gives you a standalone tool with no flake8 dependency, a purpose-built CLI (check, list, explain subcommands), and full test isolation. The flake8 approach is ~40 lines vs.
~800, but adds flake8 as a dependency alongside ruff — so you'd be running two linters anyway, just different ones.

Custom linter (this PR) Flake8 plugin
Lines of code 811 ~50-80
Files changed 26 ~3-4
Test infrastructure Custom (66 tests) Pytest with standard flake8 test helpers
noqa support Reimplemented from scratch Built-in
CLI Custom argparse Built-in
CI integration Separate step Same flake8 step
Adding future rules New file + tests + register 3-5 lines in existing plugin

@spencrr

spencrr commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

The custom linter in this PR gives you a standalone tool with no flake8 dependency, a purpose-built CLI (check, list, explain subcommands), and full test isolation. The flake8 approach is ~40 lines vs.

LOL that's way easier - what the heck Copilot - I said "use existing tools if possible" 🤣

@spencrr

spencrr commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

I don't see any changes to RAMPART002 in this PR for existing files, just additions of missing _async suffix from RAMPART001. Just checking that you ran this and nothing was flagged by this rule?

http://rampart/pytest_plugin/plugin.py#L244 and a few others have noqa's

@spencrr spencrr force-pushed the spencrr/touchup-coding-standards branch from 2128925 to eb26d6e Compare April 22, 2026 01:36
@spencrr spencrr deleted the branch main April 22, 2026 04:48
@spencrr spencrr closed this Apr 22, 2026
@spencrr spencrr reopened this Apr 22, 2026
@spencrr spencrr changed the base branch from spencrr/touchup-coding-standards to main April 22, 2026 04:52
@spencrr spencrr force-pushed the spencrr/linter-tool branch from a2bbdb6 to c895536 Compare April 22, 2026 04:53
@spencrr

spencrr commented May 12, 2026

Copy link
Copy Markdown
Contributor Author

Closing for prioritization, may revisit

@spencrr spencrr closed this May 12, 2026
@spencrr spencrr deleted the spencrr/linter-tool branch May 13, 2026 20:20
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.

2 participants