Skip to content

Enforce AST-based Pattern and Metric interface contracts#814

Merged
rultor merged 11 commits into
cqfn:masterfrom
AntonProkopyev:improve-typings
Jun 13, 2025
Merged

Enforce AST-based Pattern and Metric interface contracts#814
rultor merged 11 commits into
cqfn:masterfrom
AntonProkopyev:improve-typings

Conversation

@AntonProkopyev

@AntonProkopyev AntonProkopyev commented Jun 7, 2025

Copy link
Copy Markdown
Contributor

#813

The current codebase exhibits inconsistencies in metric and pattern interfaces, where some implementations accept an ast while others take a filepath. Additionally, the unstructured use of dictionaries in config and main complicates maintenance and refactoring.

Historical commit patterns suggest that the intended contract operates on an ast (Abstract Syntax Tree), but this is not formally enforced, leading to potential bugs and technical debt.

This PR formalizes the expected interface contract by:

  1. Introducing Mypy type hints to explicitly declare parameter and return types.
  2. Adding unit tests with inspect to validate function signatures at runtime.

Benefits

  • Clearer Contracts: Explicit typing reduces ambiguity in interfaces.
  • Better Maintainability: Static checks catch type-related errors early.
  • Future-Proofing: Simplifies refactoring and enforces consistency for new contributions.

Impact

  • No breaking changes to existing functionality.
  • Lays groundwork for future cleanup of config/main dictionary usage.

Summary by CodeRabbit

  • Refactor

    • Enhanced static typing and type hinting for improved code clarity and type safety.
    • Added protocol and TypedDict classes to better define expected interfaces and configuration structures.
    • Added assertions to ensure metric objects conform to expected types.
  • Tests

    • Replaced empty test class with new tests verifying type annotations and required parameters in configuration factories.
    • Added targeted tests for specific metrics and patterns, with clear instructions for future updates.
  • Style

    • Applied type checking decorators to disable type checks where necessary for compatibility.

@coderabbitai

coderabbitai Bot commented Jun 7, 2025

Copy link
Copy Markdown

Walkthrough

The changes introduce static typing enhancements across the configuration module, add interface protocols for patterns and metrics, and update related type hints. Tests are refactored to use pytest and introspect callable signatures for correct AST parameter typing. Minor runtime checks and type-checking suppressions are added without altering application logic.

Changes

File(s) Change Summary
aibolit/config.py Added Pattern and Metric protocol classes, TypedDicts for config structure, and static typing to config methods. Updated type hints and suppressed type checker warnings for dynamic constructs.
aibolit/main.py Imported Metric and added a runtime assertion to verify metric object type in decomposition calculation.
aibolit/model/model.py Added @no_type_check decorator to the test method in PatternRankingModel to disable type checking.
test/config/test_config.py Replaced empty unittest class with multiple pytest tests that introspect and assert correct type annotations for AST parameters in metric and pattern factories. Added xfail markers for incomplete compliance.

Sequence Diagram(s)

sequenceDiagram
    participant TestRunner as pytest
    participant Config as Config
    participant MetricFactory as Metric Factory
    participant PatternFactory as Pattern Factory

    TestRunner->>Config: get_patterns_config()
    Config-->>TestRunner: patterns & metrics configs

    loop For each metric in config
        TestRunner->>MetricFactory: Inspect value() signature
        MetricFactory-->>TestRunner: Signature info
        TestRunner->>TestRunner: Assert 'ast' parameter and type
    end

    loop For each pattern in config
        TestRunner->>PatternFactory: Inspect value() signature
        PatternFactory-->>TestRunner: Signature info
        TestRunner->>TestRunner: Assert 'ast' parameter and type
    end
Loading

Poem

In the warren of code, new types now appear,
Protocols and TypedDicts, the structure is clear.
Pytest hops in, inspecting each call,
Ensuring AST’s present for metrics and all.
With static hints growing, our code’s future is bright—
A rabbit’s delight in the soft, typed light!
🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 98058c3 and ba921e2.

📒 Files selected for processing (1)
  • aibolit/config.py (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • aibolit/config.py
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: make
  • GitHub Check: latexmk
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Comment thread aibolit/config.py Outdated
@AntonProkopyev AntonProkopyev changed the title Enforce AST-based Interface Contracts with Mypy and Inspect Tests #813: Enforce AST-based Interface Contracts with Mypy and Inspect Tests Jun 7, 2025
@AntonProkopyev AntonProkopyev changed the title #813: Enforce AST-based Interface Contracts with Mypy and Inspect Tests Enforce AST-based Interface Contracts with Mypy and Inspect Tests Jun 7, 2025
@AntonProkopyev

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@AntonProkopyev AntonProkopyev changed the title Enforce AST-based Interface Contracts with Mypy and Inspect Tests Enforce AST-based patterns and metrics interface contracts with Mypy and Inspect Tests Jun 7, 2025
@AntonProkopyev AntonProkopyev changed the title Enforce AST-based patterns and metrics interface contracts with Mypy and Inspect Tests Enforce AST-based Pattern and Metric interface contracts with Mypy and Inspect Tests Jun 7, 2025

@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: 4

♻️ Duplicate comments (1)
aibolit/config.py (1)

147-147: Address the type ignore comment when the typing issue is resolved.

Based on the past review comment mentioning "Ignore until astral-sh/ty#154", this appears to be a temporary workaround for a known typing limitation.

🧹 Nitpick comments (1)
aibolit/model/model.py (1)

180-180: Consider addressing the underlying typing issues instead of suppressing type checking.

While the @no_type_check decorator provides a quick workaround, it would be better to resolve the underlying type checking issues to maintain type safety throughout the codebase.

Can you clarify what specific typing issues are causing problems in this method? This would help determine if there's a better solution than suppressing type checking entirely.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c1ab5b0 and 7524c7e.

📒 Files selected for processing (4)
  • aibolit/__main__.py (2 hunks)
  • aibolit/config.py (6 hunks)
  • aibolit/model/model.py (2 hunks)
  • test/config/test_config.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
aibolit/__main__.py (1)
aibolit/config.py (2)
  • Config (107-240)
  • Metric (88-90)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: latexmk
  • GitHub Check: make
🔇 Additional comments (6)
aibolit/config.py (2)

75-79: Well-defined protocol for Pattern interface.

The Protocol definition clearly establishes the expected interface for patterns, requiring a value method that accepts an AST and returns a list of integers (line numbers).


87-91: Well-defined protocol for Metric interface.

The Protocol definition clearly establishes the expected interface for metrics, requiring a value method that accepts an AST and returns an integer.

aibolit/__main__.py (2)

36-36: Good addition of Metric protocol import for runtime validation.

This enables the runtime assertion to verify that metric instances conform to the expected interface.


299-299: Excellent runtime validation of protocol compliance.

The assertion ensures that factory-created metric objects actually implement the expected Metric protocol, providing both static and runtime type safety.

aibolit/model/model.py (1)

4-4: Import added for type checking suppression.

The no_type_check import enables the decorator used on the test method below.

test/config/test_config.py (1)

10-23: Excellent test structure and documentation.

The test design using @pytest.mark.xfail with detailed TODO comments is a great approach for tracking progress toward full interface compliance. The comprehensive documentation explains both the requirements and next steps clearly.

Comment thread test/config/test_config.py
Comment thread test/config/test_config.py
Comment thread test/config/test_config.py
Comment thread test/config/test_config.py Outdated
@AntonProkopyev AntonProkopyev changed the title Enforce AST-based Pattern and Metric interface contracts with Mypy and Inspect Tests Enforce AST-based Pattern and Metric interface contracts Jun 7, 2025
@AntonProkopyev AntonProkopyev marked this pull request as ready for review June 7, 2025 18:47
@0crat

0crat commented Jun 7, 2025

Copy link
Copy Markdown
Collaborator

@AntonProkopyev Hello team member, I noticed that your branch name improve-typings doesn't follow our naming convention. As per our policy, branch names should match the ticket number you're working on. In this case, 813 would have been the correct name. Unfortunately, this results in a -12 point deduction. Your current balance is now +45. For future reference, please ensure your branch names align with our guidelines. Let's work together to maintain consistency in our project management. 👍

@AntonProkopyev

Copy link
Copy Markdown
Contributor Author

@yegor256 please take a look here

@yegor256

yegor256 commented Jun 8, 2025

Copy link
Copy Markdown
Member

@ivanovmg what do you think?

Comment thread aibolit/config.py Outdated

@typing.runtime_checkable
class Pattern(typing.Protocol):
def value(self, ast: AST) -> list[int]:

@ivanovmg ivanovmg Jun 9, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@yegor256 @AntonProkopyev in my opinion, having to pass ast (or filepath) into value is not the best idea.
The reason for this is that it makes a class essentially a function.

class SomePattern:
  def __init__(self) -> None:
    pass  # why would we even need a class without an instance variable?

  def value(self, ast: AST) -> list[LineNumber]:
    # here is a pure function, which does not have any state (ast is not even an attribute)

Either we have a class with ast passed as a parameter:

class SomePattern:
  def __init__(self, ast: AST) -> None:
    self.ast = ast

  @classmethod
  def from_filepath(cls, filepath: str | os.PathLike) -> SomePattern:
    # an alternative constructor, which is easy to use for the end user
    ast = ...  # create ast from the filepath
    return cls(ast)

  def value(self) -> list[LineNumber]:
    ...

Or we have a simple function:

def some_pattern(ast: AST) -> list[LineNumber]:
  ...

I personally gravitate towards a class with ast as an input parameter, since it allows for an alternative constructor via a classmethod.
But still, a functional approach may work: there is a library variants, which enables function overloading https://github.com/python-variants/variants. However, the project hasn't been updated for quite a while.

Example:

matching_lines = some_pattern(ast)
matching_lines = some_pattern.from_filepath(filepath)

@AntonProkopyev AntonProkopyev Jun 9, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@ivanovmg (cc: @yegor256) thank you for review! I agree that the "function"-like interface is not a best idea. I think before any refactorings we need to provide clear and obvious contracts between modules and subsystems. After that making the code consistent. And then making it beautiful.

Let me show the steps of refactorings with pseudocode:

  1. Initial state - unclear contracts, code will brakes
class P1: # works
  def value(ast: AST) -> list[LineNumber]: ...
class P2: # wrong
  def value(filepath: str) -> list[LineNumber]: ...
  1. Involve the contract which show me as a developer how all patterns should look, all new patterns matches the contract because of tests:
class Pattern(Protocol):
  def value(ast: AST) -> list[LineNumber]: ...

class P1: # works
  def value(ast: AST) -> list[LineNumber]: ...

class P2: # wrong # type: ignore
  def value(filepath: str) -> list[LineNumber]: ...
  1. Fix the rest of patterns
class P1(Pattern): # works
  def value(ast: AST) -> list[LineNumber]: ...

class P2(Pattern): # now works
  def value(ast: AST) -> list[LineNumber]: ...
  1. After that maybe we realize that we need to provide not only ast, also a code, filename, etc and need get not only a line numbres, but also columns. We involving a new contract:
class Source:
  def ast() -> AST: ...
  def code() -> str: ...
  def name() -> str: ...

class Findings:
  def locations() -> Iterable[Location]: ...

class PatternV2(Protocol):
  def findings_in(soruce: Source) -> Findings: ...

class P1(Pattern, PatternV2): # supports a new contract
  def value(ast: AST) -> list[LineNumber]: ...
  def findings_in(soruce: Source) -> Findings: ...

class P2(Pattern):
  def value(ast: AST) -> list[LineNumber]: ...
  1. Support the new contract everywhere in code:
class P1(Pattern, PatternV2): ...
class P2(Pattern, PatternV2): ...
  1. Remove the obsolete contract:
class P1(PatternV2): ...
class P2(PatternV2): ...
  1. Finally rename PatternV2 -> Pattern
class P1(Pattern): ...
class P2(Pattern): ...

In conclusion with this approach we can incrementally fix the entire codebase with small PRs minimizing the risk of braking the code. The current PR is a second step of proposed approach. What do you think?

@ivanovmg ivanovmg Jun 9, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@AntonProkopyev I appreciate the way you think of a potential future transition towards Source and Findings! This indeed can be a probable outcome of the project evolution, in my opinion. It is a good point, and I haven't thought of this.

My question still stands. What if we avoid passing ast and/or filepath into value method?

class Pattern(Protocol):
  def value(self) -> list[LineNumber]: ...

class ConcretePattern:
  def __init__(self, ast: AST) -> None:
    self.ast = ast

  def value(self) -> list[LineNumber]:
    return ...

I mean, Pattern is an object for a particular AST, right? Otherwise, it will become a class with static methods, and can be replaced with a function.
Or do you think of some parameters to be injected into Pattern instance apart from ast?
I can understand, that metric can have a tuning parameters, like (max_limit, or something, to throw an error when exceeded), but still even in this case making it be composed out of AST and some tuning parameters still seems a better option to me from the object thinking standpoint.

Then the sequence of the changes proposed from your side will be quite similar:

class Source:
  def ast() -> AST: ...
  def code() -> str: ...
  def name() -> str: ...

class Findings:
  def locations() -> Iterable[Location]: ...

class PatternV2(Protocol):
  def findings(self) -> Findings: ...

class P1(Pattern, PatternV2): # supports a new contract
  def value(self) -> list[LineNumber]: ...
  def findings(self) -> Findings: ...

class P2(Pattern):
  def value(self) -> list[LineNumber]: ...

@yegor256 what do you think from the object perspective regarding Pattern and Metric? Should they be composed out of AST or should they take AST as a parameter into instance methods (which will look more like static methods, IMHO)?

@AntonProkopyev AntonProkopyev Jun 9, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@ivanovmg I agree, we can use a single Pattern instance per AST instance, and move an ast paramete to __init__ method but this change requires patching of all patterns and control code - bloats the current PR.

Probably we should make another PR with chain of changes proposed - moving ast to constructor.

On the other hand there are patterns with hyperparameters, like P20, but we can bind a parameters with a lambda.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@AntonProkopyev sure, let's proceed with your suggestion. I wonder how much we will have to change in future to adhere to the new interface though :)

Comment thread aibolit/config.py Outdated

@ivanovmg ivanovmg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@yegor256 @AntonProkopyev I think it is good idea that the interface get formalized.
But I suppose we need to put more thought in this before we start making drastic changes to the whole code base.
Here are my thoughts on the Pattern interface in particular.

@ivanovmg ivanovmg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's proceed with this, keeping in mind incremental changes towards better design, like having ast as a part of Pattern as an instance attribute.

@AntonProkopyev

Copy link
Copy Markdown
Contributor Author

@yegor256 сan you please merge the PR?

@yegor256

Copy link
Copy Markdown
Member

@rultor merge

@rultor

rultor commented Jun 13, 2025

Copy link
Copy Markdown
Contributor

@rultor merge

@yegor256 OK, I'll try to merge now. You can check the progress of the merge here.

@rultor rultor merged commit 0a45624 into cqfn:master Jun 13, 2025
17 checks passed
@rultor

rultor commented Jun 13, 2025

Copy link
Copy Markdown
Contributor

@rultor merge

@yegor256 Done! FYI, the full log is here (took me 18min).

@0crat

0crat commented Jun 16, 2025

Copy link
Copy Markdown
Collaborator

@ivanovmg Great job on the code review! 🎉 You've earned +12 points, which is the base reward for reviewing someone else's contribution. Your running balance is now +256. Keep up the good work and remember that additional points can be earned based on the number of hits-of-code and comments in the review, as per our work policy.

@0crat

0crat commented Jun 16, 2025

Copy link
Copy Markdown
Collaborator

@AntonProkopyev Thanks for your contribution! 🚀 You've earned a solid +19 points: +16 base, +7 for your 141 hits-of-code, and a small -4 deduction due to 20 review comments (our policy caps at 8). Keep up the great work and focus on quality to maximize your rewards. Your current balance stands at +33. Remember, contributions under 20 hits-of-code incur an 8-point deduction, so aim for impactful changes!

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.

5 participants