python-code-validator 0.1.3__tar.gz → 0.3.0__tar.gz
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.
- {python_code_validator-0.1.3/src/python_code_validator.egg-info → python_code_validator-0.3.0}/PKG-INFO +2 -2
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/README.md +2 -2
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/pyproject.toml +7 -6
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/__init__.py +1 -1
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/cli.py +45 -19
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/components/factories.py +31 -13
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/config.py +7 -4
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/core.py +115 -36
- python_code_validator-0.3.0/src/code_validator/output.py +238 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/rules_library/basic_rules.py +12 -7
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/rules_library/constraint_logic.py +301 -292
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/rules_library/selector_nodes.py +9 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0/src/python_code_validator.egg-info}/PKG-INFO +2 -2
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/tests/test_validator.py +20 -18
- python_code_validator-0.1.3/src/code_validator/output.py +0 -90
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/LICENSE +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/setup.cfg +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/__main__.py +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/components/__init__.py +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/components/ast_utils.py +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/components/definitions.py +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/components/scope_handler.py +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/exceptions.py +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/rules_library/__init__.py +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/python_code_validator.egg-info/SOURCES.txt +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/python_code_validator.egg-info/dependency_links.txt +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/python_code_validator.egg-info/entry_points.txt +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/python_code_validator.egg-info/requires.txt +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/python_code_validator.egg-info/top_level.txt +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/tests/test_components.py +0 -0
- {python_code_validator-0.1.3 → python_code_validator-0.3.0}/tests/test_scope_handler.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: python-code-validator
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: A flexible, AST-based framework for static validation of Python code using declarative JSON rules.
|
5
5
|
Author-email: Qu1nel <covach.qn@gmail.com>
|
6
6
|
License: MIT License
|
@@ -287,7 +287,7 @@ Validation failed.
|
|
287
287
|
**[Read the Docs](https://[your-project].readthedocs.io)**.
|
288
288
|
- **Developer's Guide**: For a deep dive into the architecture, see the
|
289
289
|
**[How It Works guide](./docs/how_it_works/index.md)**.
|
290
|
-
- **Interactive AI-Powered Docs**:
|
290
|
+
- **Interactive AI-Powered Docs**: **[DeepWiki](https://deepwiki.com/Qu1nel/PythonCodeValidator)**.
|
291
291
|
|
292
292
|
## 🤝 Contributing
|
293
293
|
|
@@ -229,7 +229,7 @@ Validation failed.
|
|
229
229
|
**[Read the Docs](https://[your-project].readthedocs.io)**.
|
230
230
|
- **Developer's Guide**: For a deep dive into the architecture, see the
|
231
231
|
**[How It Works guide](./docs/how_it_works/index.md)**.
|
232
|
-
- **Interactive AI-Powered Docs**:
|
232
|
+
- **Interactive AI-Powered Docs**: **[DeepWiki](https://deepwiki.com/Qu1nel/PythonCodeValidator)**.
|
233
233
|
|
234
234
|
## 🤝 Contributing
|
235
235
|
|
@@ -253,4 +253,4 @@ Email: **[covach.qn@gmail.com](mailto:covach.qn@gmail.com)** Telegram: **[@qnlln
|
|
253
253
|
|
254
254
|
<br/>
|
255
255
|
|
256
|
-
<p align="right"><a href="./LICENSE">MIT</a> © <a href="https://github.com/Qu1nel/">Ivan Kovach</a></p>
|
256
|
+
<p align="right"><a href="./LICENSE">MIT</a> © <a href="https://github.com/Qu1nel/">Ivan Kovach</a></p>
|
@@ -10,13 +10,13 @@ build-backend = "setuptools.build_meta"
|
|
10
10
|
# ==============================================================================
|
11
11
|
[project]
|
12
12
|
name = "python-code-validator"
|
13
|
-
version = "0.
|
14
|
-
authors = [{ name = "Qu1nel", email = "covach.qn@gmail.com" }]
|
13
|
+
version = "0.3.0"
|
15
14
|
description = "A flexible, AST-based framework for static validation of Python code using declarative JSON rules."
|
15
|
+
keywords = ["validation", "linter", "static analysis", "testing", "education", "ast"]
|
16
|
+
authors = [{ name = "Qu1nel", email = "covach.qn@gmail.com" }]
|
16
17
|
readme = "README.md"
|
17
18
|
requires-python = ">=3.11"
|
18
19
|
license = { file = "LICENSE" }
|
19
|
-
keywords = ["validation", "linter", "static analysis", "testing", "education", "ast"]
|
20
20
|
classifiers = [
|
21
21
|
"Development Status :: 4 - Beta",
|
22
22
|
"Intended Audience :: Developers",
|
@@ -40,9 +40,11 @@ dependencies = [
|
|
40
40
|
"Documentation" = "https://pythoncodevalidator.readthedocs.io/en/latest/"
|
41
41
|
"Bug Tracker" = "https://github.com/Qu1nel/PythonCodeValidator/issues"
|
42
42
|
|
43
|
+
|
43
44
|
[project.scripts]
|
44
45
|
validate-code = "code_validator.cli:run_from_cli"
|
45
46
|
|
47
|
+
|
46
48
|
[project.optional-dependencies]
|
47
49
|
dev = [
|
48
50
|
"ruff>=0.4.0",
|
@@ -81,8 +83,7 @@ select = [
|
|
81
83
|
"C4", # flake8-comprehensions
|
82
84
|
"D", # pydocstyle
|
83
85
|
]
|
84
|
-
ignore = [
|
85
|
-
]
|
86
|
+
ignore = []
|
86
87
|
|
87
88
|
[tool.ruff.lint.per-file-ignores]
|
88
89
|
"tests/*" = [
|
@@ -95,7 +96,7 @@ ignore = [
|
|
95
96
|
]
|
96
97
|
"src/code_validator/components/__init__.py" = ["D104"]
|
97
98
|
"src/code_validator/rules_library/__init__.py" = ["D104"]
|
98
|
-
|
99
|
+
"src/code_validator/components/factories.py" = ["D107"]
|
99
100
|
|
100
101
|
[tool.ruff.lint.pydocstyle]
|
101
102
|
convention = "google"
|
@@ -35,19 +35,31 @@ def setup_arg_parser() -> argparse.ArgumentParser:
|
|
35
35
|
prog="validate-code",
|
36
36
|
description="Validates a Python source file against a set of JSON rules.",
|
37
37
|
)
|
38
|
+
|
38
39
|
parser.add_argument("solution_path", type=Path, help="Path to the Python solution file to validate.")
|
39
40
|
parser.add_argument("rules_path", type=Path, help="Path to the JSON file with validation rules.")
|
41
|
+
|
40
42
|
parser.add_argument(
|
41
|
-
"
|
42
|
-
"--log-level",
|
43
|
+
"--log",
|
43
44
|
type=LogLevel,
|
44
|
-
|
45
|
-
|
46
|
-
help="Set the logging level (default: WARNING).",
|
45
|
+
default=LogLevel.ERROR,
|
46
|
+
help="Set the logging level for stderr (TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL). Default: ERROR.",
|
47
47
|
)
|
48
|
-
parser.add_argument(
|
49
|
-
|
50
|
-
|
48
|
+
parser.add_argument(
|
49
|
+
"--quiet", action="store_true", help="Suppress all stdout output (validation errors and final verdict)."
|
50
|
+
)
|
51
|
+
parser.add_argument("--no-verdict", action="store_true", help="Suppress stdout output verdict, show failed rules.")
|
52
|
+
parser.add_argument(
|
53
|
+
"--max-messages",
|
54
|
+
type=int,
|
55
|
+
default=0,
|
56
|
+
metavar="N",
|
57
|
+
help="Maximum number of error messages to display. 0 for no limit. Default: 0.",
|
58
|
+
)
|
59
|
+
parser.add_argument(
|
60
|
+
"-x", "--exit-on-first-error", action="store_true", help="Exit instantly on the first error found."
|
61
|
+
)
|
62
|
+
parser.add_argument("--version", "-v", action="version", version=f"%(prog)s {__version__}")
|
51
63
|
return parser
|
52
64
|
|
53
65
|
|
@@ -68,35 +80,49 @@ def run_from_cli() -> None:
|
|
68
80
|
parser = setup_arg_parser()
|
69
81
|
args = parser.parse_args()
|
70
82
|
|
71
|
-
logger = setup_logging(args.
|
72
|
-
console = Console(logger,
|
83
|
+
logger = setup_logging(args.log)
|
84
|
+
console = Console(logger, is_quiet=args.quiet, show_verdict=not args.no_verdict)
|
85
|
+
console.print(f"Level of logging: {args.log}", level=LogLevel.DEBUG)
|
73
86
|
config = AppConfig(
|
74
87
|
solution_path=args.solution_path,
|
75
88
|
rules_path=args.rules_path,
|
76
|
-
log_level=args.
|
77
|
-
|
78
|
-
|
89
|
+
log_level=args.log,
|
90
|
+
is_quiet=args.quiet,
|
91
|
+
exit_on_first_error=args.exit_on_first_error,
|
92
|
+
max_messages=args.max_messages,
|
79
93
|
)
|
94
|
+
console.print(f"Config is: {config}", level=LogLevel.TRACE)
|
80
95
|
|
81
96
|
try:
|
82
97
|
console.print(f"Starting validation for: {config.solution_path}", level=LogLevel.INFO)
|
83
98
|
validator = StaticValidator(config, console)
|
99
|
+
|
100
|
+
console.print("Start of validation..", level=LogLevel.TRACE)
|
84
101
|
is_valid = validator.run()
|
102
|
+
console.print(f"End of validation with result: {is_valid = }", level=LogLevel.TRACE)
|
85
103
|
|
86
104
|
if is_valid:
|
87
|
-
console.print("Validation successful.", level=LogLevel.INFO)
|
105
|
+
console.print("Validation successful.", level=LogLevel.INFO, is_verdict=True)
|
88
106
|
sys.exit(ExitCode.SUCCESS)
|
89
107
|
else:
|
90
|
-
console.print("Validation failed.", level=LogLevel.
|
108
|
+
console.print("Validation failed.", level=LogLevel.WARNING, is_verdict=True)
|
91
109
|
sys.exit(ExitCode.VALIDATION_FAILED)
|
92
110
|
|
93
111
|
except CodeValidatorError as e:
|
94
|
-
console.print(
|
112
|
+
console.print(
|
113
|
+
f"Error: An internal validator error occurred: {e}", level=LogLevel.CRITICAL, show_user=True, exc_info=True
|
114
|
+
)
|
95
115
|
sys.exit(ExitCode.VALIDATION_FAILED)
|
96
116
|
except FileNotFoundError as e:
|
97
|
-
console.print(
|
117
|
+
console.print(
|
118
|
+
f"Error: Input file not found: {e.filename}", level=LogLevel.CRITICAL, show_user=True, exc_info=True
|
119
|
+
)
|
98
120
|
sys.exit(ExitCode.FILE_NOT_FOUND)
|
99
121
|
except Exception as e:
|
100
|
-
console.print(
|
101
|
-
|
122
|
+
console.print(
|
123
|
+
f"Error: An unexpected error occurred: {e.__class__.__name__}. See logs for detailed traceback.",
|
124
|
+
level=LogLevel.CRITICAL,
|
125
|
+
show_user=True,
|
126
|
+
exc_info=True,
|
127
|
+
)
|
102
128
|
sys.exit(ExitCode.UNEXPECTED_ERROR)
|
@@ -10,9 +10,9 @@ rules file and instantiating the appropriate handler classes from the
|
|
10
10
|
import dataclasses
|
11
11
|
from typing import Any, Type, TypeVar
|
12
12
|
|
13
|
-
from ..config import ConstraintConfig, FullRuleCheck, FullRuleConfig, SelectorConfig, ShortRuleConfig
|
13
|
+
from ..config import ConstraintConfig, FullRuleCheck, FullRuleConfig, LogLevel, SelectorConfig, ShortRuleConfig
|
14
14
|
from ..exceptions import RuleParsingError
|
15
|
-
from ..output import Console
|
15
|
+
from ..output import Console, log_initialization
|
16
16
|
from ..rules_library.basic_rules import CheckLinterRule, CheckSyntaxRule, FullRuleHandler
|
17
17
|
from ..rules_library.constraint_logic import (
|
18
18
|
IsForbiddenConstraint,
|
@@ -71,6 +71,7 @@ class RuleFactory:
|
|
71
71
|
_constraint_factory (ConstraintFactory): A factory for creating constraint objects.
|
72
72
|
"""
|
73
73
|
|
74
|
+
@log_initialization(level=LogLevel.TRACE)
|
74
75
|
def __init__(self, console: Console):
|
75
76
|
"""Initializes the RuleFactory.
|
76
77
|
|
@@ -101,8 +102,10 @@ class RuleFactory:
|
|
101
102
|
required keys, or specifies an unknown type.
|
102
103
|
"""
|
103
104
|
rule_id = rule_config.get("rule_id")
|
105
|
+
self._console.print(f"Start parsing rule ({rule_id}):\n{rule_config}", level=LogLevel.TRACE)
|
104
106
|
try:
|
105
107
|
if "type" in rule_config:
|
108
|
+
self._console.print(f"Rule {rule_id} is shorted rule - {rule_config['type']}", level=LogLevel.DEBUG)
|
106
109
|
config = _create_dataclass_from_dict(ShortRuleConfig, rule_config)
|
107
110
|
return self._create_short_rule(config)
|
108
111
|
|
@@ -110,20 +113,31 @@ class RuleFactory:
|
|
110
113
|
raw_selector_cfg = rule_config["check"]["selector"]
|
111
114
|
raw_constraint_cfg = rule_config["check"]["constraint"]
|
112
115
|
|
113
|
-
selector = self._selector_factory.create(raw_selector_cfg)
|
114
|
-
constraint = self._constraint_factory.create(raw_constraint_cfg)
|
115
|
-
|
116
116
|
selector_cfg = _create_dataclass_from_dict(SelectorConfig, raw_selector_cfg)
|
117
117
|
constraint_cfg = _create_dataclass_from_dict(ConstraintConfig, raw_constraint_cfg)
|
118
|
+
|
119
|
+
selector = self._selector_factory.create(selector_cfg)
|
120
|
+
constraint = self._constraint_factory.create(constraint_cfg)
|
121
|
+
|
122
|
+
self._console.print(
|
123
|
+
f"Rule {rule_id} is general rule with: selector - "
|
124
|
+
f"{selector_cfg.type}, constraint - {raw_constraint_cfg['type']}",
|
125
|
+
level=LogLevel.DEBUG,
|
126
|
+
)
|
127
|
+
|
118
128
|
check_cfg = FullRuleCheck(selector=selector_cfg, constraint=constraint_cfg)
|
129
|
+
self._console.print(f"Create FullRuleCheck: {check_cfg}", level=LogLevel.TRACE)
|
130
|
+
|
119
131
|
config = FullRuleConfig(
|
120
132
|
rule_id=rule_config["rule_id"],
|
121
133
|
message=rule_config["message"],
|
122
134
|
check=check_cfg,
|
123
135
|
is_critical=rule_config.get("is_critical", False),
|
124
136
|
)
|
137
|
+
self._console.print(f"Create FullRuleConfig: {config}", level=LogLevel.TRACE)
|
125
138
|
return FullRuleHandler(config, selector, constraint, self._console)
|
126
139
|
else:
|
140
|
+
self._console.print(f"Invalid syntax of rule: {rule_id}", level=LogLevel.WARNING)
|
127
141
|
raise RuleParsingError("Rule must contain 'type' or 'check' key.", rule_id)
|
128
142
|
except (TypeError, KeyError, RuleParsingError) as e:
|
129
143
|
raise RuleParsingError(f"Invalid config for rule '{rule_id}': {e}", rule_id) from e
|
@@ -165,21 +179,23 @@ class SelectorFactory:
|
|
165
179
|
any state.
|
166
180
|
"""
|
167
181
|
|
182
|
+
@log_initialization(level=LogLevel.TRACE)
|
183
|
+
def __init__(self) -> None:
|
184
|
+
pass
|
185
|
+
|
168
186
|
@staticmethod
|
169
|
-
def create(
|
187
|
+
def create(config: SelectorConfig) -> Selector:
|
170
188
|
"""Creates a specific selector instance based on its type.
|
171
189
|
|
172
190
|
This method uses the 'type' field from the selector configuration
|
173
191
|
to determine which concrete Selector class to instantiate.
|
174
192
|
|
175
193
|
Args:
|
176
|
-
|
194
|
+
config: The 'selector' block from a JSON rule.
|
177
195
|
|
178
196
|
Returns:
|
179
197
|
An instance of a class that conforms to the Selector protocol.
|
180
198
|
"""
|
181
|
-
config = _create_dataclass_from_dict(SelectorConfig, selector_config)
|
182
|
-
|
183
199
|
match config.type:
|
184
200
|
case "function_def":
|
185
201
|
return FunctionDefSelector(name=config.name, in_scope_config=config.in_scope)
|
@@ -210,21 +226,23 @@ class ConstraintFactory:
|
|
210
226
|
to a list of AST nodes. This class uses a static `create` method.
|
211
227
|
"""
|
212
228
|
|
229
|
+
@log_initialization(level=LogLevel.TRACE)
|
230
|
+
def __init__(self) -> None:
|
231
|
+
pass
|
232
|
+
|
213
233
|
@staticmethod
|
214
|
-
def create(
|
234
|
+
def create(config: ConstraintConfig) -> Constraint:
|
215
235
|
"""Creates a specific constraint instance based on its type.
|
216
236
|
|
217
237
|
This method uses the 'type' field from the constraint configuration
|
218
238
|
to determine which concrete Constraint class to instantiate.
|
219
239
|
|
220
240
|
Args:
|
221
|
-
|
241
|
+
config: The 'constraint' block from a JSON rule.
|
222
242
|
|
223
243
|
Returns:
|
224
244
|
An instance of a class that conforms to the Constraint protocol.
|
225
245
|
"""
|
226
|
-
config = _create_dataclass_from_dict(ConstraintConfig, constraint_config)
|
227
|
-
|
228
246
|
match config.type:
|
229
247
|
case "is_required":
|
230
248
|
return IsRequiredConstraint(count=config.count)
|
@@ -25,6 +25,7 @@ class ExitCode(IntEnum):
|
|
25
25
|
class LogLevel(StrEnum):
|
26
26
|
"""Defines the supported logging levels for the application."""
|
27
27
|
|
28
|
+
TRACE = "TRACE"
|
28
29
|
DEBUG = "DEBUG"
|
29
30
|
INFO = "INFO"
|
30
31
|
WARNING = "WARNING"
|
@@ -40,15 +41,17 @@ class AppConfig:
|
|
40
41
|
solution_path: The file path to the Python solution to be validated.
|
41
42
|
rules_path: The file path to the JSON rules file.
|
42
43
|
log_level: The minimum logging level for console output.
|
43
|
-
|
44
|
-
|
44
|
+
is_quiet: If True, suppresses all non-log output to stdout.
|
45
|
+
exit_on_first_error: If True, halts validation after the first failed rule.
|
46
|
+
max_messages: Maximum number of error messages to display. 0 for no limit. Default: 0.
|
45
47
|
"""
|
46
48
|
|
47
49
|
solution_path: Path
|
48
50
|
rules_path: Path
|
49
51
|
log_level: LogLevel
|
50
|
-
|
51
|
-
|
52
|
+
is_quiet: bool
|
53
|
+
exit_on_first_error: bool
|
54
|
+
max_messages: int = 0
|
52
55
|
|
53
56
|
|
54
57
|
@dataclass(frozen=True)
|
@@ -45,9 +45,9 @@ import json
|
|
45
45
|
from .components.ast_utils import enrich_ast_with_parents
|
46
46
|
from .components.definitions import Rule
|
47
47
|
from .components.factories import RuleFactory
|
48
|
-
from .config import AppConfig, LogLevel
|
48
|
+
from .config import AppConfig, LogLevel, ShortRuleConfig
|
49
49
|
from .exceptions import RuleParsingError
|
50
|
-
from .output import Console
|
50
|
+
from .output import Console, log_initialization
|
51
51
|
|
52
52
|
|
53
53
|
class StaticValidator:
|
@@ -64,9 +64,10 @@ class StaticValidator:
|
|
64
64
|
_source_code (str): The raw text content of the Python file being validated.
|
65
65
|
_ast_tree (ast.Module | None): The Abstract Syntax Tree of the source code.
|
66
66
|
_rules (list[Rule]): A list of initialized, executable rule objects.
|
67
|
-
_failed_rules (list[
|
67
|
+
_failed_rules (list[Rule]): A list of rules that contained IDs of failed checks during the run.
|
68
68
|
"""
|
69
69
|
|
70
|
+
@log_initialization(level=LogLevel.DEBUG)
|
70
71
|
def __init__(self, config: AppConfig, console: Console):
|
71
72
|
"""Initializes the StaticValidator.
|
72
73
|
|
@@ -77,14 +78,15 @@ class StaticValidator:
|
|
77
78
|
"""
|
78
79
|
self._config = config
|
79
80
|
self._console = console
|
81
|
+
|
80
82
|
self._rule_factory = RuleFactory(self._console)
|
81
83
|
self._source_code: str = ""
|
82
84
|
self._ast_tree: ast.Module | None = None
|
83
85
|
self._rules: list[Rule] = []
|
84
|
-
self._failed_rules: list[
|
86
|
+
self._failed_rules: list[Rule] = []
|
85
87
|
|
86
88
|
@property
|
87
|
-
def failed_rules_id(self) -> list[
|
89
|
+
def failed_rules_id(self) -> list[Rule]:
|
88
90
|
"""list[int]: A list of rule IDs that failed during the last run."""
|
89
91
|
return self._failed_rules
|
90
92
|
|
@@ -95,14 +97,47 @@ class StaticValidator:
|
|
95
97
|
FileNotFoundError: If the source file specified in the config does not exist.
|
96
98
|
RuleParsingError: If the source file cannot be read for any other reason.
|
97
99
|
"""
|
98
|
-
self._console.print(f"Reading source file: {self._config.solution_path}")
|
100
|
+
self._console.print(f"Reading source file: {self._config.solution_path}", level=LogLevel.DEBUG)
|
99
101
|
try:
|
100
102
|
self._source_code = self._config.solution_path.read_text(encoding="utf-8")
|
103
|
+
self._console.print(f"Source code:\n{self._source_code}\n", level=LogLevel.TRACE)
|
101
104
|
except FileNotFoundError:
|
105
|
+
self._console.print("During reading source file raised FileNotFound", level=LogLevel.TRACE)
|
102
106
|
raise
|
103
107
|
except Exception as e:
|
108
|
+
self._console.print("During reading source file raised some exception..", level=LogLevel.TRACE)
|
104
109
|
raise RuleParsingError(f"Cannot read source file: {e}") from e
|
105
110
|
|
111
|
+
def _load_and_parse_rules(self) -> None:
|
112
|
+
"""Loads and parses the JSON file into executable Rule objects.
|
113
|
+
|
114
|
+
This method reads the JSON rules file, validates its basic structure,
|
115
|
+
and then uses the `RuleFactory` to instantiate a list of concrete
|
116
|
+
Rule objects.
|
117
|
+
|
118
|
+
Raises:
|
119
|
+
FileNotFoundError: If the rules file does not exist.
|
120
|
+
RuleParsingError: If the JSON is malformed or a rule configuration
|
121
|
+
is invalid.
|
122
|
+
"""
|
123
|
+
self._console.print(f"Loading rules from: {self._config.rules_path}", level=LogLevel.DEBUG)
|
124
|
+
try:
|
125
|
+
rules_data = json.loads(self._config.rules_path.read_text(encoding="utf-8"))
|
126
|
+
self._console.print(f"Load rules:\n{rules_data}", level=LogLevel.TRACE)
|
127
|
+
raw_rules = rules_data.get("validation_rules")
|
128
|
+
if not isinstance(raw_rules, list):
|
129
|
+
raise RuleParsingError("`validation_rules` key not found or is not a list.")
|
130
|
+
|
131
|
+
self._console.print(f"Found {len(raw_rules)}.", level=LogLevel.DEBUG)
|
132
|
+
self._rules = [self._rule_factory.create(rule) for rule in raw_rules]
|
133
|
+
self._console.print(f"Successfully parsed {len(self._rules)} rules.", level=LogLevel.DEBUG)
|
134
|
+
except json.JSONDecodeError as e:
|
135
|
+
self._console.print("During reading file of rules raised JsonDecodeError..", level=LogLevel.TRACE)
|
136
|
+
raise RuleParsingError(f"Invalid JSON in rules file: {e}") from e
|
137
|
+
except FileNotFoundError:
|
138
|
+
self._console.print("During reading file of rules raised FileNotFound", level=LogLevel.TRACE)
|
139
|
+
raise
|
140
|
+
|
106
141
|
def _parse_ast_tree(self) -> bool:
|
107
142
|
"""Parses the loaded source code into an AST and enriches it.
|
108
143
|
|
@@ -114,45 +149,65 @@ class StaticValidator:
|
|
114
149
|
Returns:
|
115
150
|
bool: True if parsing was successful, False otherwise.
|
116
151
|
"""
|
117
|
-
self._console.print("Parsing Abstract Syntax Tree (AST)...")
|
152
|
+
self._console.print("Parsing Abstract Syntax Tree (AST)...", level=LogLevel.DEBUG)
|
118
153
|
try:
|
154
|
+
self._console.print("Start parse source code.", level=LogLevel.TRACE)
|
119
155
|
self._ast_tree = ast.parse(self._source_code)
|
120
156
|
enrich_ast_with_parents(self._ast_tree)
|
121
157
|
return True
|
122
158
|
except SyntaxError as e:
|
159
|
+
self._console.print("In source code SyntaxError..", level=LogLevel.TRACE)
|
123
160
|
for rule in self._rules:
|
124
161
|
if getattr(rule.config, "type", None) == "check_syntax":
|
125
|
-
self._console.print(rule.config.message, level=LogLevel.ERROR)
|
126
|
-
self.
|
162
|
+
self._console.print(rule.config.message, level=LogLevel.ERROR, show_user=True)
|
163
|
+
self._console.print(f"Failed rule id: {rule.config.rule_id}", level=LogLevel.DEBUG)
|
164
|
+
self._failed_rules.append(rule)
|
127
165
|
return False
|
128
166
|
self._console.print(f"Syntax Error found: {e}", level=LogLevel.ERROR)
|
129
167
|
return False
|
130
168
|
|
131
|
-
def
|
132
|
-
"""
|
169
|
+
def _report_errors(self) -> None:
|
170
|
+
"""Formats and prints collected validation errors to the console.
|
171
|
+
|
172
|
+
This method is responsible for presenting the final list of failed
|
173
|
+
rules to the user. It respects the `--max-messages` configuration
|
174
|
+
to avoid cluttering the terminal. If the number of found errors
|
175
|
+
exceeds the specified limit, it truncates the output and displays
|
176
|
+
a summary message indicating how many more errors were found.
|
177
|
+
|
178
|
+
The method retrieves the list of failed rules from `self._failed_rules`
|
179
|
+
and the display limit from `self._config`. All user-facing output is
|
180
|
+
channeled through the `self._console` object.
|
181
|
+
|
182
|
+
It performs the following steps:
|
183
|
+
1. Checks if any errors were recorded. If not, it returns immediately.
|
184
|
+
2. Determines the subset of errors to display based on the configured
|
185
|
+
`max_messages` limit (a value of 0 means no limit).
|
186
|
+
3. Iterates through the selected error rules and prints their
|
187
|
+
failure messages.
|
188
|
+
4. If the error list was truncated, prints a summary line, e.g.,
|
189
|
+
"... (5 more errors found)".
|
190
|
+
"""
|
191
|
+
max_errors = self._config.max_messages
|
192
|
+
num_errors = len(self._failed_rules)
|
133
193
|
|
134
|
-
|
135
|
-
|
136
|
-
Rule objects.
|
194
|
+
if num_errors == 0:
|
195
|
+
return None
|
137
196
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
is invalid.
|
142
|
-
"""
|
143
|
-
self._console.print(f"Loading rules from: {self._config.rules_path}")
|
144
|
-
try:
|
145
|
-
rules_data = json.loads(self._config.rules_path.read_text(encoding="utf-8"))
|
146
|
-
raw_rules = rules_data.get("validation_rules")
|
147
|
-
if not isinstance(raw_rules, list):
|
148
|
-
raise RuleParsingError("`validation_rules` key not found or is not a list.")
|
197
|
+
errors_to_show = self._failed_rules
|
198
|
+
if 0 < max_errors < num_errors:
|
199
|
+
errors_to_show = self._failed_rules[:max_errors]
|
149
200
|
|
150
|
-
|
151
|
-
self._console.print(
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
201
|
+
for rule in errors_to_show:
|
202
|
+
self._console.print(rule.config.message, level=LogLevel.WARNING, show_user=True)
|
203
|
+
|
204
|
+
if 0 < max_errors < num_errors:
|
205
|
+
remaining_count = num_errors - max_errors
|
206
|
+
self._console.print(
|
207
|
+
f"... ({remaining_count} more error{'s' if remaining_count > 1 else ''} found)",
|
208
|
+
level=LogLevel.WARNING,
|
209
|
+
show_user=True,
|
210
|
+
)
|
156
211
|
|
157
212
|
def run(self) -> bool:
|
158
213
|
"""Runs the entire validation process from start to finish.
|
@@ -172,22 +227,46 @@ class StaticValidator:
|
|
172
227
|
self._load_and_parse_rules()
|
173
228
|
|
174
229
|
if not self._parse_ast_tree():
|
230
|
+
self._report_errors()
|
175
231
|
return False
|
176
232
|
|
177
|
-
|
233
|
+
self._console.print("Lead source code, load and parse rules and parsing code - PASS", level=LogLevel.DEBUG)
|
234
|
+
|
235
|
+
except (FileNotFoundError, RuleParsingError) as e:
|
236
|
+
self._console.print(
|
237
|
+
f"In method `run` of 'StaticValidator' raised exception {e.__class__.__name__}", level=LogLevel.WARNING
|
238
|
+
)
|
178
239
|
raise
|
179
240
|
|
241
|
+
self._console.print("Starting check rules..", level=LogLevel.DEBUG)
|
180
242
|
for rule in self._rules:
|
181
243
|
if getattr(rule.config, "type", None) == "check_syntax":
|
182
244
|
continue
|
183
245
|
|
184
|
-
self._console.print(
|
246
|
+
self._console.print(
|
247
|
+
f"Executing rule: {rule.config.rule_id}"
|
248
|
+
+ (
|
249
|
+
f" [{rule.config.check.selector.type}, {rule.config.check.constraint.type}, "
|
250
|
+
f"is_critical={rule.config.is_critical}]"
|
251
|
+
if not isinstance(rule.config, ShortRuleConfig)
|
252
|
+
else ""
|
253
|
+
),
|
254
|
+
level=LogLevel.INFO,
|
255
|
+
)
|
185
256
|
is_passed = rule.execute(self._ast_tree, self._source_code)
|
186
257
|
if not is_passed:
|
187
|
-
self.
|
188
|
-
self.
|
189
|
-
|
258
|
+
self._failed_rules.append(rule)
|
259
|
+
# self._console.print(rule.config.message, level=LogLevel.WARNING, show_user=True)
|
260
|
+
self._console.print(f"Rule {rule.config.rule_id} - FAIL", level=LogLevel.INFO)
|
261
|
+
if getattr(rule.config, "is_critical", False):
|
190
262
|
self._console.print("Critical rule failed. Halting validation.", level=LogLevel.WARNING)
|
191
263
|
break
|
264
|
+
elif self._config.exit_on_first_error:
|
265
|
+
self._console.print("Exiting on first error.", level=LogLevel.INFO)
|
266
|
+
break
|
267
|
+
else:
|
268
|
+
self._console.print(f"Rule {rule.config.rule_id} - PASS", level=LogLevel.INFO)
|
269
|
+
|
270
|
+
self._report_errors()
|
192
271
|
|
193
272
|
return not self._failed_rules
|