feat: implement core analysis engine (INH001, INH002) with ADRs - #2
feat: implement core analysis engine (INH001, INH002) with ADRs#2steven-cutting wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8077110316
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for kw in node.keywords: | ||
| if kw.arg == "metaclass" and isinstance(kw.value, ast.Name): | ||
| name = kw.value.id | ||
| if import_map.get(name) == "abc" and name in _abc_metaclass_aliases(import_map): |
There was a problem hiding this comment.
Detect metaclass=abc.ABCMeta in ABC checks
The ABC detection only recognizes metaclass= when the value is an ast.Name. That misses the common pattern class X(metaclass=abc.ABCMeta) (or metaclass=abc.ABCMeta with import abc), because kw.value is an ast.Attribute and the class won’t be treated as abstract. In that case, concrete methods in such ABCs won’t trigger INH002, leading to false negatives. Consider handling ast.Attribute for metaclass values whose root resolves to abc and whose attribute is ABCMeta.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Implements the core analysis layer for flake8-inheritance, adding AST-based detection for rules INH001 (internal inheritance) and INH002 (concrete methods in ABCs), along with ADR documentation and unit tests to validate behavior independently of the flake8 adapter.
Changes:
- Add framework-agnostic
ErrorCodedefinitions for INH001/INH002. - Implement import tracking, base-class classification, and INH001/INH002 checks via
InheritanceVisitor. - Add ADRs and tests covering import mapping, classification, rule detection, and edge cases.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/flake8_inheritance/visitors.py |
Core AST analysis (import map, classification, INH001/INH002 detection). |
src/flake8_inheritance/codes.py |
Frozen dataclass error-code definitions and formatting. |
tests/test_visitors.py |
Unit tests for import mapping, classification, INH001/INH002, and edge cases. |
tests/test_codes.py |
Unit tests for error-code immutability and message formatting. |
doc/adr/README.md |
Index update to include new ADRs. |
doc/adr/0007-define-error-codes-as-framework-agnostic-frozen-dataclasses.md |
ADR for error-code design. |
doc/adr/0008-classify-base-classes-using-import-map-lookup-with-sys-stdlib-module-names.md |
ADR for classification strategy. |
doc/adr/0009-decouple-visitor-analysis-logic-from-flake8-plugin-interface.md |
ADR for architecture split (analysis vs adapter). |
doc/adr/0010-silently-skip-unanalyzable-patterns-rather-than-guessing-or-crashing.md |
ADR for “skip vs guess” behavior on unanalyzable patterns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def test_unknown_not_flagged(self) -> None: | ||
| # Absolute import with no project_packages configured → external | ||
| errors = _visit("""\ | ||
| from somelib import Thing | ||
| class Foo(Thing): | ||
| pass | ||
| """) | ||
| assert errors == [] |
There was a problem hiding this comment.
The test name test_unknown_not_flagged (and the comment) don't match the behavior under test: Thing is imported from somelib, so it’s classified as external, not unknown. Renaming this test (or adjusting the scenario to truly be unknown) would make the intent clearer.
| class MyABC(ABC): | ||
| @abstractmethod | ||
| async def do_thing(self): | ||
| pass |
There was a problem hiding this comment.
This test duplicates test_async_abstractmethod_not_flagged above with the same scenario/assertions. Consider removing one of them (or varying the setup) to avoid redundant coverage.
| class MyABC(ABC): | |
| @abstractmethod | |
| async def do_thing(self): | |
| pass | |
| class Base(ABC): | |
| @abstractmethod | |
| async def do_thing(self) -> None: | |
| pass | |
| class Impl(Base): | |
| async def do_thing(self) -> None: | |
| pass |
| import_map: dict[str, str] = {} | ||
| for node in ast.walk(tree): | ||
| if isinstance(node, ast.Import): | ||
| for alias in node.names: | ||
| # ``import foo.bar`` -> local name "foo", package "foo" | ||
| top = alias.name.split(".")[0] | ||
| local_name = alias.asname if alias.asname else top | ||
| import_map[local_name] = top |
There was a problem hiding this comment.
build_import_map uses ast.walk(tree), which collects imports from nested scopes (functions/classes) and also imports that appear after a class statement. That can make a base name look "imported" even when it isn’t actually in scope at the point the class bases are evaluated, leading to incorrect INH001/INH002 diagnostics. Consider restricting the map to module-level imports (and/or only statements that occur before a given class), or making the import map scope-aware.
| return "internal_relative" | ||
| if pkg in _project_set(project_packages): | ||
| return "internal_project" |
There was a problem hiding this comment.
classify_name() calls _project_set(project_packages) on every invocation, recreating a set repeatedly during AST walking. Since project_packages is effectively constant for a visitor run, compute a set once (e.g., in InheritanceVisitor.__init__ or at the start of visit()) and reuse it to avoid unnecessary allocations.
| return { | ||
| name | ||
| for name, pkg in import_map.items() | ||
| if pkg == "abc" and name != "abstractmethod" | ||
| and name not in _abc_class_aliases(import_map) | ||
| } | { | ||
| name | ||
| for name, pkg in import_map.items() | ||
| if pkg == "abc" and name == "ABCMeta" |
There was a problem hiding this comment.
_abc_metaclass_aliases() recomputes _abc_class_aliases(import_map) inside a comprehension and then unions with a second set that is redundant for ABCMeta (it already satisfies the first set’s predicate). Consider computing the alias sets once and simplifying the logic to a single pass for readability and efficiency.
| return { | |
| name | |
| for name, pkg in import_map.items() | |
| if pkg == "abc" and name != "abstractmethod" | |
| and name not in _abc_class_aliases(import_map) | |
| } | { | |
| name | |
| for name, pkg in import_map.items() | |
| if pkg == "abc" and name == "ABCMeta" | |
| class_aliases = _abc_class_aliases(import_map) | |
| return { | |
| name | |
| for name, pkg in import_map.items() | |
| if pkg == "abc" | |
| and name != "abstractmethod" | |
| and name not in class_aliases |
| import pytest | ||
|
|
There was a problem hiding this comment.
pytest is imported but never used in this test module. With Ruff configured to check F401, this will fail linting; remove the import or use it.
| import pytest |
|
|
||
| import pytest | ||
|
|
||
| from flake8_inheritance.codes import INH001, INH002, ErrorCode |
There was a problem hiding this comment.
ErrorCode is imported but never used. This will trigger an unused-import lint error (e.g., Ruff F401); either remove it from the import list or add an assertion that references ErrorCode.
| from flake8_inheritance.codes import INH001, INH002, ErrorCode | |
| from flake8_inheritance.codes import INH001, INH002 |
https://claude.ai/code/session_01XCWZSwXFCDx4b71TT1sh5C