python-code-validator 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_validator/__init__.py +25 -0
- code_validator/__main__.py +11 -0
- code_validator/cli.py +88 -0
- code_validator/components/__init__.py +0 -0
- code_validator/components/ast_utils.py +40 -0
- code_validator/components/definitions.py +88 -0
- code_validator/components/factories.py +243 -0
- code_validator/components/scope_handler.py +59 -0
- code_validator/config.py +99 -0
- code_validator/core.py +100 -0
- code_validator/exceptions.py +48 -0
- code_validator/output.py +90 -0
- code_validator/rules_library/__init__.py +0 -0
- code_validator/rules_library/basic_rules.py +167 -0
- code_validator/rules_library/constraint_logic.py +257 -0
- code_validator/rules_library/selector_nodes.py +319 -0
- python_code_validator-0.1.1.dist-info/METADATA +308 -0
- python_code_validator-0.1.1.dist-info/RECORD +22 -0
- python_code_validator-0.1.1.dist-info/WHEEL +5 -0
- python_code_validator-0.1.1.dist-info/entry_points.txt +2 -0
- python_code_validator-0.1.1.dist-info/licenses/LICENSE +21 -0
- python_code_validator-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
"""A flexible framework for static validation of Python code.
|
2
|
+
|
3
|
+
This package provides a comprehensive toolkit for statically analyzing Python source
|
4
|
+
code based on a declarative set of rules defined in a JSON format. It allows
|
5
|
+
for checking syntax, style, structure, and constraints without executing the code.
|
6
|
+
|
7
|
+
Key components exposed by this package include:
|
8
|
+
- StaticValidator: The main orchestrator for running the validation process.
|
9
|
+
- AppConfig: A dataclass for configuring the validator's behavior.
|
10
|
+
- ExitCode: An Enum defining exit codes for CLI operations.
|
11
|
+
- Custom Exceptions: For fine-grained error handling during validation.
|
12
|
+
"""
|
13
|
+
|
14
|
+
from .config import AppConfig, ExitCode
|
15
|
+
from .core import StaticValidator
|
16
|
+
from .exceptions import RuleParsingError, ValidationFailedError
|
17
|
+
|
18
|
+
__all__ = [
|
19
|
+
"StaticValidator",
|
20
|
+
"AppConfig",
|
21
|
+
"ExitCode",
|
22
|
+
"ValidationFailedError",
|
23
|
+
"RuleParsingError",
|
24
|
+
]
|
25
|
+
__version__ = "0.1.0"
|
@@ -0,0 +1,11 @@
|
|
1
|
+
"""Enables running the validator as a module.
|
2
|
+
|
3
|
+
This file allows the package to be executed directly from the command line
|
4
|
+
using `python -m code_validator`. It serves as the main entry point
|
5
|
+
that invokes the command-line interface logic.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .cli import run_from_cli
|
9
|
+
|
10
|
+
if __name__ == "__main__":
|
11
|
+
run_from_cli()
|
code_validator/cli.py
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
"""Defines the command-line interface for the code validator.
|
2
|
+
|
3
|
+
This module is responsible for parsing command-line arguments, setting up the
|
4
|
+
application configuration, and orchestrating the main validation workflow. It acts
|
5
|
+
as the primary entry point for user interaction.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import argparse
|
9
|
+
import sys
|
10
|
+
from pathlib import Path
|
11
|
+
|
12
|
+
from . import __version__
|
13
|
+
from .config import AppConfig, ExitCode, LogLevel
|
14
|
+
from .core import StaticValidator
|
15
|
+
from .exceptions import CodeValidatorError
|
16
|
+
from .output import Console, setup_logging
|
17
|
+
|
18
|
+
|
19
|
+
def setup_arg_parser() -> argparse.ArgumentParser:
|
20
|
+
"""Creates and configures the argument parser for the CLI.
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
An instance of argparse.ArgumentParser with all arguments defined.
|
24
|
+
"""
|
25
|
+
parser = argparse.ArgumentParser(
|
26
|
+
prog="validate-code",
|
27
|
+
description="Validates a Python source file against a set of JSON rules.",
|
28
|
+
)
|
29
|
+
parser.add_argument("solution_path", type=Path, help="Path to the Python solution file to validate.")
|
30
|
+
parser.add_argument("rules_path", type=Path, help="Path to the JSON file with validation rules.")
|
31
|
+
parser.add_argument(
|
32
|
+
"-l",
|
33
|
+
"--log-level",
|
34
|
+
type=LogLevel,
|
35
|
+
choices=LogLevel,
|
36
|
+
default=LogLevel.WARNING,
|
37
|
+
help="Set the logging level (default: WARNING).",
|
38
|
+
)
|
39
|
+
parser.add_argument("--silent", action="store_true", help="Suppress stdout output, show only logs.")
|
40
|
+
parser.add_argument("--stop-on-first-fail", action="store_true", help="Stop after the first failed rule.")
|
41
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
42
|
+
return parser
|
43
|
+
|
44
|
+
|
45
|
+
def run_from_cli() -> None:
|
46
|
+
"""Runs the full application lifecycle from the command line.
|
47
|
+
|
48
|
+
This function parses arguments, initializes logging and configuration,
|
49
|
+
runs the validator, and handles all top-level exceptions, exiting with an
|
50
|
+
appropriate exit code.
|
51
|
+
"""
|
52
|
+
parser = setup_arg_parser()
|
53
|
+
args = parser.parse_args()
|
54
|
+
|
55
|
+
# 1. Setup environment
|
56
|
+
logger = setup_logging(args.log_level)
|
57
|
+
console = Console(logger, is_silent=args.silent)
|
58
|
+
config = AppConfig(
|
59
|
+
solution_path=args.solution_path,
|
60
|
+
rules_path=args.rules_path,
|
61
|
+
log_level=args.log_level,
|
62
|
+
is_silent=args.silent,
|
63
|
+
stop_on_first_fail=args.stop_on_first_fail,
|
64
|
+
)
|
65
|
+
|
66
|
+
# 2. Run main logic with robust error handling
|
67
|
+
try:
|
68
|
+
console.print(f"Starting validation for: {config.solution_path}", level=LogLevel.INFO)
|
69
|
+
validator = StaticValidator(config, console)
|
70
|
+
is_valid = validator.run()
|
71
|
+
|
72
|
+
if is_valid:
|
73
|
+
console.print("Validation successful.", level=LogLevel.INFO)
|
74
|
+
sys.exit(ExitCode.SUCCESS)
|
75
|
+
else:
|
76
|
+
console.print("Validation failed.", level=LogLevel.ERROR)
|
77
|
+
sys.exit(ExitCode.VALIDATION_FAILED)
|
78
|
+
|
79
|
+
except CodeValidatorError as e:
|
80
|
+
console.print(str(e), level=LogLevel.CRITICAL)
|
81
|
+
sys.exit(ExitCode.VALIDATION_FAILED)
|
82
|
+
except FileNotFoundError as e:
|
83
|
+
console.print(f"Error: File not found - {e.strerror}: {e.filename}", level=LogLevel.CRITICAL)
|
84
|
+
sys.exit(ExitCode.FILE_NOT_FOUND)
|
85
|
+
except Exception as e:
|
86
|
+
console.print(f"An unexpected error occurred: {e}", level=LogLevel.CRITICAL)
|
87
|
+
logger.exception("Traceback for unexpected error:")
|
88
|
+
sys.exit(ExitCode.UNEXPECTED_ERROR)
|
File without changes
|
@@ -0,0 +1,40 @@
|
|
1
|
+
"""Provides utility functions for working with Python's Abstract Syntax Trees (AST)."""
|
2
|
+
|
3
|
+
import ast
|
4
|
+
|
5
|
+
|
6
|
+
def enrich_ast_with_parents(tree: ast.Module) -> None:
|
7
|
+
"""Walks the AST and adds a 'parent' attribute to each node.
|
8
|
+
|
9
|
+
This function mutates the AST in-place, making it easier to traverse upwards
|
10
|
+
or determine the context of a specific node. This is a crucial preprocessing
|
11
|
+
step for many complex validation rules.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
tree: The root node of the AST (typically an ast.Module object) to enrich.
|
15
|
+
"""
|
16
|
+
for node in ast.walk(tree):
|
17
|
+
for child in ast.iter_child_nodes(node):
|
18
|
+
# Dynamically add a reference to the parent node.
|
19
|
+
child.parent = node
|
20
|
+
|
21
|
+
|
22
|
+
def get_full_name(node: ast.AST) -> str | None:
|
23
|
+
"""A helper function to recursively build a full attribute name from an AST node.
|
24
|
+
|
25
|
+
For example, for an `ast.Attribute` node representing `foo.bar.baz`, this
|
26
|
+
function will return the string "foo.bar.baz".
|
27
|
+
|
28
|
+
Args:
|
29
|
+
node: The AST node to extract the name from.
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
The full, dot-separated name as a string, or None if a name cannot be
|
33
|
+
constructed.
|
34
|
+
"""
|
35
|
+
if isinstance(node, ast.Name):
|
36
|
+
return node.id
|
37
|
+
if isinstance(node, ast.Attribute):
|
38
|
+
base = get_full_name(node.value)
|
39
|
+
return f"{base}.{node.attr}" if base else node.attr
|
40
|
+
return None
|
@@ -0,0 +1,88 @@
|
|
1
|
+
"""Defines the core component interfaces using Protocols.
|
2
|
+
|
3
|
+
This module establishes the fundamental "contracts" for the main architectural
|
4
|
+
components of the validator: Rules, Selectors, and Constraints. By using
|
5
|
+
Protocols, we ensure that any class conforming to these interfaces can be used
|
6
|
+
interchangeably by the system's factories and core engine, enabling a flexible
|
7
|
+
and decoupled plugin-style architecture.
|
8
|
+
"""
|
9
|
+
|
10
|
+
import ast
|
11
|
+
from typing import Protocol, runtime_checkable
|
12
|
+
|
13
|
+
from ..config import FullRuleConfig, ShortRuleConfig
|
14
|
+
|
15
|
+
|
16
|
+
@runtime_checkable
|
17
|
+
class Selector(Protocol):
|
18
|
+
"""An interface for objects that find and select specific nodes from an AST.
|
19
|
+
|
20
|
+
A Selector's main responsibility is to traverse the Abstract Syntax Tree (AST)
|
21
|
+
of a Python source file and return a list of nodes that match a specific
|
22
|
+
criterion (e.g., all function definitions, all import statements).
|
23
|
+
"""
|
24
|
+
|
25
|
+
def select(self, tree: ast.Module) -> list[ast.AST]:
|
26
|
+
"""Selects and returns a list of relevant AST nodes from the tree.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
tree: The full, parsed AST of the source code to be searched.
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
A list of ast.AST nodes that match the selector's criteria.
|
33
|
+
An empty list should be returned if no matching nodes are found.
|
34
|
+
"""
|
35
|
+
...
|
36
|
+
|
37
|
+
|
38
|
+
@runtime_checkable
|
39
|
+
class Constraint(Protocol):
|
40
|
+
"""An interface for objects that apply a condition to a set of AST nodes.
|
41
|
+
|
42
|
+
A Constraint takes the list of nodes found by a Selector and checks if they
|
43
|
+
satisfy a specific condition (e.g., the list must not be empty, the node
|
44
|
+
must inherit from a specific class).
|
45
|
+
"""
|
46
|
+
|
47
|
+
def check(self, nodes: list[ast.AST]) -> bool:
|
48
|
+
"""Checks if the given list of nodes satisfies the constraint.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
nodes: A list of AST nodes provided by a Selector.
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
True if the constraint is satisfied, False otherwise.
|
55
|
+
"""
|
56
|
+
...
|
57
|
+
|
58
|
+
|
59
|
+
@runtime_checkable
|
60
|
+
class Rule(Protocol):
|
61
|
+
"""An interface for any complete, executable validation rule.
|
62
|
+
|
63
|
+
A Rule represents a single, self-contained validation check. It can be a
|
64
|
+
"short" rule (like a linter check) or a "full" rule that internally uses
|
65
|
+
a Selector and a Constraint. The core validator engine interacts with
|
66
|
+
objects conforming to this protocol.
|
67
|
+
|
68
|
+
Attributes:
|
69
|
+
config: The dataclass object holding the configuration for this rule,
|
70
|
+
parsed from the JSON file.
|
71
|
+
"""
|
72
|
+
|
73
|
+
config: FullRuleConfig | ShortRuleConfig
|
74
|
+
|
75
|
+
def execute(self, tree: ast.Module | None, source_code: str | None = None) -> bool:
|
76
|
+
"""Executes the validation rule.
|
77
|
+
|
78
|
+
Depending on the rule type, this method might operate on the AST, the
|
79
|
+
raw source code, or both.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
tree: The full AST of the source code (for structural checks).
|
83
|
+
source_code: The raw source code string (e.g., for linter checks).
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
True if the validation check passes, False otherwise.
|
87
|
+
"""
|
88
|
+
...
|
@@ -0,0 +1,243 @@
|
|
1
|
+
"""Contains factories for creating rule, selector, and constraint objects.
|
2
|
+
|
3
|
+
This module implements the Factory Method design pattern to decouple the core
|
4
|
+
validator engine from the concrete implementation of rules. Factories are
|
5
|
+
responsible for parsing raw dictionary configurations from JSON and instantiating
|
6
|
+
the appropriate handler classes.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import dataclasses
|
10
|
+
from typing import Any, Type, TypeVar
|
11
|
+
|
12
|
+
from ..config import ConstraintConfig, FullRuleCheck, FullRuleConfig, SelectorConfig, ShortRuleConfig
|
13
|
+
from ..exceptions import RuleParsingError
|
14
|
+
from ..output import Console
|
15
|
+
from ..rules_library.basic_rules import CheckLinterRule, CheckSyntaxRule, FullRuleHandler
|
16
|
+
from ..rules_library.constraint_logic import (
|
17
|
+
IsForbiddenConstraint,
|
18
|
+
IsRequiredConstraint,
|
19
|
+
MustBeTypeConstraint,
|
20
|
+
MustHaveArgsConstraint,
|
21
|
+
MustInheritFromConstraint,
|
22
|
+
NameMustBeInConstraint,
|
23
|
+
ValueMustBeInConstraint,
|
24
|
+
)
|
25
|
+
from ..rules_library.selector_nodes import (
|
26
|
+
AssignmentSelector,
|
27
|
+
AstNodeSelector,
|
28
|
+
ClassDefSelector,
|
29
|
+
FunctionCallSelector,
|
30
|
+
FunctionDefSelector,
|
31
|
+
ImportStatementSelector,
|
32
|
+
LiteralSelector,
|
33
|
+
UsageSelector,
|
34
|
+
)
|
35
|
+
from .definitions import Constraint, Rule, Selector
|
36
|
+
|
37
|
+
T = TypeVar("T")
|
38
|
+
|
39
|
+
|
40
|
+
def _create_dataclass_from_dict(cls: Type[T], data: dict[str, Any]) -> T:
|
41
|
+
"""Safely creates a dataclass instance from a dictionary.
|
42
|
+
|
43
|
+
This helper function filters the input dictionary to include only the keys
|
44
|
+
that correspond to fields in the target dataclass, preventing `TypeError`
|
45
|
+
for unexpected arguments.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
cls: The dataclass type to instantiate.
|
49
|
+
data: The dictionary with raw data.
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
An instance of the specified dataclass.
|
53
|
+
"""
|
54
|
+
expected_fields = {f.name for f in dataclasses.fields(cls)}
|
55
|
+
filtered_data = {k: v for k, v in data.items() if k in expected_fields}
|
56
|
+
return cls(**filtered_data)
|
57
|
+
|
58
|
+
|
59
|
+
class RuleFactory:
|
60
|
+
"""Creates rule handler objects from raw dictionary configuration.
|
61
|
+
|
62
|
+
This is the main factory that acts as an entry point for parsing the
|
63
|
+
'validation_rules' list from a JSON file. It determines whether a rule is
|
64
|
+
a "short" pre-defined type or a "full" custom rule and delegates the
|
65
|
+
creation of its components to other specialized factories.
|
66
|
+
|
67
|
+
Attributes:
|
68
|
+
_console (Console): An instance of the console for logging.
|
69
|
+
_selector_factory (SelectorFactory): A factory for creating selector objects.
|
70
|
+
_constraint_factory (ConstraintFactory): A factory for creating constraint objects.
|
71
|
+
"""
|
72
|
+
|
73
|
+
def __init__(self, console: Console):
|
74
|
+
"""Initializes the RuleFactory.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
console: An instance of the Console for logging and output,
|
78
|
+
to be passed to rule handlers.
|
79
|
+
"""
|
80
|
+
self._console = console
|
81
|
+
self._selector_factory = SelectorFactory()
|
82
|
+
self._constraint_factory = ConstraintFactory()
|
83
|
+
|
84
|
+
def create(self, rule_config: dict[str, Any]) -> Rule:
|
85
|
+
"""Creates a specific rule instance based on its configuration.
|
86
|
+
|
87
|
+
This method acts as a dispatcher. It determines whether the configuration
|
88
|
+
describes a "short" pre-defined rule or a "full" custom rule with a
|
89
|
+
selector/constraint pair, and then delegates to the appropriate
|
90
|
+
creation logic.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
rule_config: A dictionary parsed from the JSON rules file.
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
An instance of an object that conforms to the Rule protocol.
|
97
|
+
|
98
|
+
Raises:
|
99
|
+
RuleParsingError: If the rule configuration is invalid, missing
|
100
|
+
required keys, or specifies an unknown type.
|
101
|
+
"""
|
102
|
+
rule_id = rule_config.get("rule_id")
|
103
|
+
try:
|
104
|
+
if "type" in rule_config:
|
105
|
+
config = _create_dataclass_from_dict(ShortRuleConfig, rule_config)
|
106
|
+
return self._create_short_rule(config)
|
107
|
+
|
108
|
+
elif "check" in rule_config:
|
109
|
+
raw_selector_cfg = rule_config["check"]["selector"]
|
110
|
+
raw_constraint_cfg = rule_config["check"]["constraint"]
|
111
|
+
|
112
|
+
selector = self._selector_factory.create(raw_selector_cfg)
|
113
|
+
constraint = self._constraint_factory.create(raw_constraint_cfg)
|
114
|
+
|
115
|
+
selector_cfg = _create_dataclass_from_dict(SelectorConfig, raw_selector_cfg)
|
116
|
+
constraint_cfg = _create_dataclass_from_dict(ConstraintConfig, raw_constraint_cfg)
|
117
|
+
check_cfg = FullRuleCheck(selector=selector_cfg, constraint=constraint_cfg)
|
118
|
+
config = FullRuleConfig(
|
119
|
+
rule_id=rule_config["rule_id"],
|
120
|
+
message=rule_config["message"],
|
121
|
+
check=check_cfg,
|
122
|
+
is_critical=rule_config.get("is_critical", False),
|
123
|
+
)
|
124
|
+
return FullRuleHandler(config, selector, constraint, self._console)
|
125
|
+
else:
|
126
|
+
raise RuleParsingError("Rule must contain 'type' or 'check' key.", rule_id)
|
127
|
+
except (TypeError, KeyError, RuleParsingError) as e:
|
128
|
+
raise RuleParsingError(f"Invalid config for rule '{rule_id}': {e}", rule_id) from e
|
129
|
+
|
130
|
+
def _create_short_rule(self, config: ShortRuleConfig) -> Rule:
|
131
|
+
"""Dispatches the creation of handlers for "short" rules.
|
132
|
+
|
133
|
+
This private helper method acts as a registry for pre-defined, common
|
134
|
+
validation checks like syntax or PEP8 linting. It maps a rule's 'type'
|
135
|
+
string to a concrete Rule handler class.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
config: The dataclass object containing the configuration for the
|
139
|
+
short rule.
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
An initialized instance of a concrete class that implements the
|
143
|
+
Rule protocol.
|
144
|
+
|
145
|
+
Raises:
|
146
|
+
RuleParsingError: If the 'type' specified in the config does not
|
147
|
+
correspond to any known short rule.
|
148
|
+
"""
|
149
|
+
if config.type == "check_syntax":
|
150
|
+
return CheckSyntaxRule(config, self._console)
|
151
|
+
elif config.type == "check_linter_pep8":
|
152
|
+
return CheckLinterRule(config, self._console)
|
153
|
+
else:
|
154
|
+
raise RuleParsingError(f"Unknown short rule type: '{config.type}'", config.rule_id)
|
155
|
+
|
156
|
+
|
157
|
+
class SelectorFactory:
|
158
|
+
"""Creates selector objects from raw dictionary configuration.
|
159
|
+
|
160
|
+
This factory is responsible for instantiating the correct Selector object
|
161
|
+
based on the 'type' field in a rule's selector configuration block. Each
|
162
|
+
concrete selector specializes in finding a specific type of AST node.
|
163
|
+
This class uses a static `create` method as it does not need to maintain
|
164
|
+
any state.
|
165
|
+
"""
|
166
|
+
|
167
|
+
@staticmethod
|
168
|
+
def create(selector_config: dict[str, Any]) -> Selector:
|
169
|
+
"""Creates a specific selector instance based on its type.
|
170
|
+
|
171
|
+
This method uses the 'type' field from the selector configuration
|
172
|
+
to determine which concrete Selector class to instantiate.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
selector_config: The 'selector' block from a JSON rule.
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
An instance of a class that conforms to the Selector protocol.
|
179
|
+
"""
|
180
|
+
config = _create_dataclass_from_dict(SelectorConfig, selector_config)
|
181
|
+
|
182
|
+
match config.type:
|
183
|
+
case "function_def":
|
184
|
+
return FunctionDefSelector(name=config.name, in_scope_config=config.in_scope)
|
185
|
+
case "class_def":
|
186
|
+
return ClassDefSelector(name=config.name, in_scope_config=config.in_scope)
|
187
|
+
case "import_statement":
|
188
|
+
return ImportStatementSelector(name=config.name, in_scope_config=config.in_scope)
|
189
|
+
case "function_call":
|
190
|
+
return FunctionCallSelector(name=config.name, in_scope_config=config.in_scope)
|
191
|
+
case "assignment":
|
192
|
+
return AssignmentSelector(name=config.name, in_scope_config=config.in_scope)
|
193
|
+
case "usage":
|
194
|
+
return UsageSelector(name=config.name, in_scope_config=config.in_scope)
|
195
|
+
case "literal":
|
196
|
+
return LiteralSelector(name=config.name, in_scope_config=config.in_scope)
|
197
|
+
case "ast_node":
|
198
|
+
return AstNodeSelector(node_type=config.node_type, in_scope_config=config.in_scope)
|
199
|
+
case _:
|
200
|
+
raise RuleParsingError(f"Unknown selector type: '{config.type}'")
|
201
|
+
|
202
|
+
|
203
|
+
class ConstraintFactory:
|
204
|
+
"""Creates constraint objects from raw dictionary configuration.
|
205
|
+
|
206
|
+
This factory is responsible for instantiating the correct Constraint object
|
207
|
+
based on the 'type' field in a rule's constraint configuration block.
|
208
|
+
Each concrete constraint specializes in applying a specific logical check
|
209
|
+
to a list of AST nodes. This class uses a static `create` method.
|
210
|
+
"""
|
211
|
+
|
212
|
+
@staticmethod
|
213
|
+
def create(constraint_config: dict[str, Any]) -> Constraint:
|
214
|
+
"""Creates a specific constraint instance based on its type.
|
215
|
+
|
216
|
+
This method uses the 'type' field from the constraint configuration
|
217
|
+
to determine which concrete Constraint class to instantiate.
|
218
|
+
|
219
|
+
Args:
|
220
|
+
constraint_config: The 'constraint' block from a JSON rule.
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
An instance of a class that conforms to the Constraint protocol.
|
224
|
+
"""
|
225
|
+
config = _create_dataclass_from_dict(ConstraintConfig, constraint_config)
|
226
|
+
|
227
|
+
match config.type:
|
228
|
+
case "is_required":
|
229
|
+
return IsRequiredConstraint(count=config.count)
|
230
|
+
case "is_forbidden":
|
231
|
+
return IsForbiddenConstraint()
|
232
|
+
case "must_inherit_from":
|
233
|
+
return MustInheritFromConstraint(parent_name=config.parent_name)
|
234
|
+
case "must_be_type":
|
235
|
+
return MustBeTypeConstraint(expected_type=config.expected_type)
|
236
|
+
case "must_have_args":
|
237
|
+
return MustHaveArgsConstraint(count=config.count, names=config.names, exact_match=config.exact_match)
|
238
|
+
case "name_must_be_in":
|
239
|
+
return NameMustBeInConstraint(allowed_names=config.allowed_names)
|
240
|
+
case "value_must_be_in":
|
241
|
+
return ValueMustBeInConstraint(allowed_values=config.allowed_values)
|
242
|
+
case _:
|
243
|
+
raise RuleParsingError(f"Unknown constraint type: '{config.type}'")
|
@@ -0,0 +1,59 @@
|
|
1
|
+
"""Provides functionality to find and isolate specific scopes within an AST.
|
2
|
+
|
3
|
+
This module contains helper functions that are used by ScopedSelectors to narrow
|
4
|
+
down their search area from the entire module to a specific function, class,
|
5
|
+
or method, based on the `in_scope` configuration from a JSON rule.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import ast
|
9
|
+
from typing import Any
|
10
|
+
|
11
|
+
|
12
|
+
def find_scope_node(tree: ast.Module, scope_config: dict[str, Any]) -> ast.AST | None:
|
13
|
+
"""Finds a specific scope node (class or function) within the AST.
|
14
|
+
|
15
|
+
This function traverses the AST to locate a node that matches the provided
|
16
|
+
scope configuration. It supports finding global functions, classes, and
|
17
|
+
methods within classes.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
tree: The root of the AST (the module object).
|
21
|
+
scope_config: A dictionary defining the desired scope.
|
22
|
+
Expected keys:
|
23
|
+
- "function": name of a global function.
|
24
|
+
- "class": name of a class.
|
25
|
+
- "method": name of a method (must be used with "class").
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
The found ast.AST node (either ast.ClassDef or ast.FunctionDef) that
|
29
|
+
represents the desired scope, or None if the scope is not found.
|
30
|
+
|
31
|
+
Example:
|
32
|
+
>>> # To find the scope of 'my_func' in 'MyClass':
|
33
|
+
>>> scope_config = {"class": "MyClass", "method": "my_func"}
|
34
|
+
>>> find_scope_node(my_ast_tree, scope_config)
|
35
|
+
<ast.FunctionDef object at ...>
|
36
|
+
"""
|
37
|
+
class_name = scope_config.get("class")
|
38
|
+
if class_name:
|
39
|
+
for node in ast.walk(tree):
|
40
|
+
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
41
|
+
# If only a class scope is needed, return it.
|
42
|
+
if "method" not in scope_config:
|
43
|
+
return node
|
44
|
+
|
45
|
+
# If a method is needed, search within the class body.
|
46
|
+
method_name = scope_config.get("method")
|
47
|
+
for item in node.body:
|
48
|
+
if isinstance(item, ast.FunctionDef) and item.name == method_name:
|
49
|
+
return item
|
50
|
+
return None # Class was found, but the method was not.
|
51
|
+
|
52
|
+
function_name = scope_config.get("function")
|
53
|
+
if function_name:
|
54
|
+
# For global functions, search only the top-level body of the module.
|
55
|
+
for node in tree.body:
|
56
|
+
if isinstance(node, ast.FunctionDef) and node.name == function_name:
|
57
|
+
return node
|
58
|
+
|
59
|
+
return None
|
code_validator/config.py
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
"""Defines all data structures and configuration models for the validator.
|
2
|
+
|
3
|
+
This module contains Enum classes for standardized codes and several frozen
|
4
|
+
dataclasses that represent the structured configuration loaded from JSON files
|
5
|
+
and command-line arguments. These models ensure type safety and provide a
|
6
|
+
clear "shape" for the application's data.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from dataclasses import dataclass, field
|
10
|
+
from enum import IntEnum, StrEnum
|
11
|
+
from pathlib import Path
|
12
|
+
from typing import Any
|
13
|
+
|
14
|
+
|
15
|
+
class ExitCode(IntEnum):
|
16
|
+
"""Defines standardized exit codes for the command-line application."""
|
17
|
+
|
18
|
+
SUCCESS = 0
|
19
|
+
VALIDATION_FAILED = 1
|
20
|
+
FILE_NOT_FOUND = 2
|
21
|
+
JSON_ERROR = 3
|
22
|
+
UNEXPECTED_ERROR = 10
|
23
|
+
|
24
|
+
|
25
|
+
class LogLevel(StrEnum):
|
26
|
+
"""Defines the supported logging levels for the application."""
|
27
|
+
|
28
|
+
DEBUG = "DEBUG"
|
29
|
+
INFO = "INFO"
|
30
|
+
WARNING = "WARNING"
|
31
|
+
ERROR = "ERROR"
|
32
|
+
CRITICAL = "CRITICAL"
|
33
|
+
|
34
|
+
|
35
|
+
@dataclass(frozen=True)
|
36
|
+
class AppConfig:
|
37
|
+
"""Stores the main application configuration from CLI arguments."""
|
38
|
+
|
39
|
+
solution_path: Path
|
40
|
+
rules_path: Path
|
41
|
+
log_level: LogLevel
|
42
|
+
is_silent: bool
|
43
|
+
stop_on_first_fail: bool
|
44
|
+
|
45
|
+
|
46
|
+
@dataclass(frozen=True)
|
47
|
+
class SelectorConfig:
|
48
|
+
"""Represents the configuration for a Selector component from a JSON rule."""
|
49
|
+
|
50
|
+
type: str
|
51
|
+
name: str | None = None
|
52
|
+
node_type: str | list[str] | None = None
|
53
|
+
in_scope: str | dict[str, Any] | None = None
|
54
|
+
|
55
|
+
|
56
|
+
@dataclass(frozen=True)
|
57
|
+
class ConstraintConfig:
|
58
|
+
"""Represents the configuration for a Constraint component from a JSON rule."""
|
59
|
+
|
60
|
+
type: str
|
61
|
+
count: int | None = None
|
62
|
+
parent_name: str | None = None
|
63
|
+
expected_type: str | None = None
|
64
|
+
allowed_names: list[str] | None = None
|
65
|
+
allowed_values: list[Any] | None = None
|
66
|
+
names: list[str] | None = None
|
67
|
+
exact_match: bool | None = None
|
68
|
+
|
69
|
+
|
70
|
+
@dataclass(frozen=True)
|
71
|
+
class FullRuleCheck:
|
72
|
+
"""Represents the 'check' block within a full validation rule."""
|
73
|
+
|
74
|
+
selector: SelectorConfig
|
75
|
+
constraint: ConstraintConfig
|
76
|
+
|
77
|
+
|
78
|
+
@dataclass(frozen=True)
|
79
|
+
class ShortRuleConfig:
|
80
|
+
"""Represents a 'short' (pre-defined) validation rule from JSON."""
|
81
|
+
|
82
|
+
rule_id: int
|
83
|
+
type: str
|
84
|
+
message: str
|
85
|
+
params: dict[str, Any] = field(default_factory=dict)
|
86
|
+
|
87
|
+
|
88
|
+
@dataclass(frozen=True)
|
89
|
+
class FullRuleConfig:
|
90
|
+
"""Represents a 'full' (custom) validation rule with selector and constraint."""
|
91
|
+
|
92
|
+
rule_id: int
|
93
|
+
message: str
|
94
|
+
check: FullRuleCheck
|
95
|
+
is_critical: bool = False
|
96
|
+
|
97
|
+
|
98
|
+
# A type alias representing any possible rule configuration object.
|
99
|
+
ValidationRuleConfig = ShortRuleConfig | FullRuleConfig
|