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.
Files changed (31) hide show
  1. {python_code_validator-0.1.3/src/python_code_validator.egg-info → python_code_validator-0.3.0}/PKG-INFO +2 -2
  2. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/README.md +2 -2
  3. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/pyproject.toml +7 -6
  4. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/__init__.py +1 -1
  5. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/cli.py +45 -19
  6. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/components/factories.py +31 -13
  7. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/config.py +7 -4
  8. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/core.py +115 -36
  9. python_code_validator-0.3.0/src/code_validator/output.py +238 -0
  10. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/rules_library/basic_rules.py +12 -7
  11. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/rules_library/constraint_logic.py +301 -292
  12. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/rules_library/selector_nodes.py +9 -0
  13. {python_code_validator-0.1.3 → python_code_validator-0.3.0/src/python_code_validator.egg-info}/PKG-INFO +2 -2
  14. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/tests/test_validator.py +20 -18
  15. python_code_validator-0.1.3/src/code_validator/output.py +0 -90
  16. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/LICENSE +0 -0
  17. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/setup.cfg +0 -0
  18. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/__main__.py +0 -0
  19. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/components/__init__.py +0 -0
  20. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/components/ast_utils.py +0 -0
  21. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/components/definitions.py +0 -0
  22. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/components/scope_handler.py +0 -0
  23. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/exceptions.py +0 -0
  24. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/code_validator/rules_library/__init__.py +0 -0
  25. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/python_code_validator.egg-info/SOURCES.txt +0 -0
  26. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/python_code_validator.egg-info/dependency_links.txt +0 -0
  27. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/python_code_validator.egg-info/entry_points.txt +0 -0
  28. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/python_code_validator.egg-info/requires.txt +0 -0
  29. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/src/python_code_validator.egg-info/top_level.txt +0 -0
  30. {python_code_validator-0.1.3 → python_code_validator-0.3.0}/tests/test_components.py +0 -0
  31. {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.1.3
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**: *(Coming Soon)* An interactive documentation experience.
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**: *(Coming Soon)* An interactive documentation experience.
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.1.3"
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"
@@ -54,4 +54,4 @@ __all__ = [
54
54
  "RuleParsingError",
55
55
  ]
56
56
 
57
- __version__ = "0.1.3"
57
+ __version__ = "0.3.0"
@@ -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
- "-l",
42
- "--log-level",
43
+ "--log",
43
44
  type=LogLevel,
44
- choices=list(LogLevel),
45
- default=LogLevel.WARNING,
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("--silent", action="store_true", help="Suppress stdout output, show only logs.")
49
- parser.add_argument("--stop-on-first-fail", action="store_true", help="Stop after the first failed rule.")
50
- parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
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.log_level)
72
- console = Console(logger, is_silent=args.silent)
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.log_level,
77
- is_silent=args.silent,
78
- stop_on_first_fail=args.stop_on_first_fail,
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.ERROR)
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(str(e), level=LogLevel.CRITICAL)
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(f"Error: File not found - {e.strerror}: {e.filename}", level=LogLevel.CRITICAL)
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(f"An unexpected error occurred: {e}", level=LogLevel.CRITICAL)
101
- logger.exception("Traceback for unexpected error:")
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(selector_config: dict[str, Any]) -> Selector:
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
- selector_config: The 'selector' block from a JSON rule.
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(constraint_config: dict[str, Any]) -> Constraint:
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
- constraint_config: The 'constraint' block from a JSON rule.
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
- is_silent: If True, suppresses all non-log output to stdout.
44
- stop_on_first_fail: If True, halts validation after the first failed rule.
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
- is_silent: bool
51
- stop_on_first_fail: bool
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[int]): A list of rule IDs that failed during the run.
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[int] = []
86
+ self._failed_rules: list[Rule] = []
85
87
 
86
88
  @property
87
- def failed_rules_id(self) -> list[int]:
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._failed_rules.append(rule.config.rule_id)
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 _load_and_parse_rules(self) -> None:
132
- """Loads and parses the JSON file into executable Rule objects.
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
- This method reads the JSON rules file, validates its basic structure,
135
- and then uses the `RuleFactory` to instantiate a list of concrete
136
- Rule objects.
194
+ if num_errors == 0:
195
+ return None
137
196
 
138
- Raises:
139
- FileNotFoundError: If the rules file does not exist.
140
- RuleParsingError: If the JSON is malformed or a rule configuration
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
- self._rules = [self._rule_factory.create(rule) for rule in raw_rules]
151
- self._console.print(f"Successfully parsed {len(self._rules)} rules.")
152
- except json.JSONDecodeError as e:
153
- raise RuleParsingError(f"Invalid JSON in rules file: {e}") from e
154
- except FileNotFoundError:
155
- raise
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
- except (FileNotFoundError, RuleParsingError):
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(f"Executing rule: {rule.config.rule_id}", level=LogLevel.DEBUG)
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._console.print(rule.config.message, level=LogLevel.ERROR)
188
- self._failed_rules.append(rule.config.rule_id)
189
- if getattr(rule.config, "is_critical", False) or self._config.stop_on_first_fail:
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