thailint 0.15.4__tar.gz → 0.15.6__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.
- {thailint-0.15.4 → thailint-0.15.6}/PKG-INFO +1 -1
- {thailint-0.15.4 → thailint-0.15.6}/pyproject.toml +1 -1
- {thailint-0.15.4 → thailint-0.15.6}/src/core/cli_utils.py +24 -4
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/config.py +38 -7
- thailint-0.15.6/src/linters/magic_numbers/definition_detector.py +226 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/linter.py +7 -0
- thailint-0.15.6/src/linters/nesting/python_analyzer.py +156 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stateless_class/config.py +4 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stateless_class/linter.py +116 -4
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stateless_class/python_analyzer.py +86 -4
- thailint-0.15.4/src/linters/nesting/python_analyzer.py +0 -93
- {thailint-0.15.4 → thailint-0.15.6}/CHANGELOG.md +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/LICENSE +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/README.md +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/analyzers/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/analyzers/ast_utils.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/analyzers/rust_base.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/analyzers/rust_context.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/analyzers/typescript_base.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/api.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/__main__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/config_merge.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/code_patterns.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/code_smells.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/documentation.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/performance.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/shared.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/structure.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/structure_quality.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/main.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli/utils.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/cli_main.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/core/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/core/base.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/core/config_parser.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/core/constants.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/core/linter_utils.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/core/python_lint_rule.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/core/registry.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/core/rule_discovery.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/core/types.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/core/violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/core/violation_utils.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/formatters/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/formatters/sarif.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linter_config/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linter_config/directive_markers.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linter_config/ignore.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linter_config/loader.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linter_config/pattern_utils.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linter_config/rule_matcher.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/any_all_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/ast_utils.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/continue_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/filter_map_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/suggestion_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/function_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/input_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/output_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/python_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/types.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/typescript_cqs_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/typescript_function_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/typescript_input_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/typescript_output_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/base_token_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/block_filter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/block_grouper.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/cache.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/cache_query.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/config_loader.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/constant.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/constant_matcher.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/constant_violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/deduplicator.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/duplicate_storage.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/file_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/inline_ignore.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/python_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/python_constant_extractor.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/single_statement_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/storage_initializer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/token_hasher.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/typescript_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/typescript_constant_extractor.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/typescript_statement_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/typescript_value_extractor.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/violation_filter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/violation_generator.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/atemporal_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/base_parser.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/bash_parser.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/css_parser.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/field_validator.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/markdown_parser.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/python_parser.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/typescript_parser.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/config_loader.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/directory_matcher.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/path_resolver.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/pattern_matcher.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/pattern_validator.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/rule_checker.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/violation_factory.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/directive_utils.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/header_parser.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/matcher.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/python_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/rule_id_utils.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/skip_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/types.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/typescript_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/base.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/dict_key_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/division_check_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/file_exists_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/hasattr_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/isinstance_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/len_check_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/none_check_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/string_validator_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/python_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/context_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/python_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/typescript_ignore_checker.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/method_property/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/method_property/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/method_property/linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/method_property/python_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/method_property/violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/nesting/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/nesting/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/nesting/linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/nesting/typescript_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/nesting/typescript_function_extractor.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/nesting/violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/constants.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/python_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/regex_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/regex_linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/typescript_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/print_statements/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/print_statements/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/print_statements/linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/print_statements/python_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/print_statements/typescript_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/print_statements/violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/class_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/heuristics.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/metrics_evaluator.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/python_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/typescript_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/typescript_metrics_calculator.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stateless_class/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/config.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/context_filter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/function_call_violation_builder.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/ignore_checker.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/ignore_utils.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/linter.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/call_tracker.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/comparison_tracker.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/condition_extractor.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/conditional_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/constants.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/match_analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/validation_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/variable_extractor.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/storage.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/storage_initializer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/typescript/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/typescript/analyzer.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/typescript/call_tracker.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/typescript/comparison_tracker.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/violation_generator.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/orchestrator/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/orchestrator/core.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/orchestrator/language_detector.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/templates/thailint_config_template.yaml +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/utils/__init__.py +0 -0
- {thailint-0.15.4 → thailint-0.15.6}/src/utils/project_root.py +0 -0
|
@@ -17,7 +17,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
17
17
|
|
|
18
18
|
[tool.poetry]
|
|
19
19
|
name = "thailint"
|
|
20
|
-
version = "0.15.
|
|
20
|
+
version = "0.15.6"
|
|
21
21
|
description = "The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages"
|
|
22
22
|
authors = ["Steve Jackson"]
|
|
23
23
|
license = "MIT"
|
|
@@ -143,6 +143,24 @@ def _load_json_config(config_file: Path) -> dict[str, Any]:
|
|
|
143
143
|
return dict(result) if isinstance(result, dict) else {}
|
|
144
144
|
|
|
145
145
|
|
|
146
|
+
def _sanitize_string(text: str) -> str:
|
|
147
|
+
"""Remove or replace surrogate characters that can't be encoded to UTF-8.
|
|
148
|
+
|
|
149
|
+
Surrogate characters (U+D800-U+DFFF) appear when Python reads filesystem paths
|
|
150
|
+
or file content with invalid UTF-8 bytes using surrogateescape error handling.
|
|
151
|
+
These characters cannot be encoded to UTF-8 and cause UnicodeEncodeError.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
text: String that may contain surrogate characters
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
String with surrogates replaced by the Unicode replacement character
|
|
158
|
+
"""
|
|
159
|
+
# Encode with surrogateescape to handle surrogates, then decode back
|
|
160
|
+
# This effectively replaces surrogates with a replacement representation
|
|
161
|
+
return text.encode("utf-8", errors="surrogateescape").decode("utf-8", errors="replace")
|
|
162
|
+
|
|
163
|
+
|
|
146
164
|
def format_violations(violations: list, output_format: str) -> None:
|
|
147
165
|
"""Format and print violations to console.
|
|
148
166
|
|
|
@@ -168,10 +186,10 @@ def _output_json(violations: list) -> None:
|
|
|
168
186
|
"violations": [
|
|
169
187
|
{
|
|
170
188
|
"rule_id": v.rule_id,
|
|
171
|
-
"file_path": str(v.file_path),
|
|
189
|
+
"file_path": _sanitize_string(str(v.file_path)),
|
|
172
190
|
"line": v.line,
|
|
173
191
|
"column": v.column,
|
|
174
|
-
"message": v.message,
|
|
192
|
+
"message": _sanitize_string(v.message),
|
|
175
193
|
"severity": v.severity.name,
|
|
176
194
|
}
|
|
177
195
|
for v in violations
|
|
@@ -215,9 +233,11 @@ def _print_violation(v: Any) -> None:
|
|
|
215
233
|
Args:
|
|
216
234
|
v: Violation object with file_path, line, column, severity, rule_id, message
|
|
217
235
|
"""
|
|
218
|
-
|
|
236
|
+
file_path = _sanitize_string(str(v.file_path))
|
|
237
|
+
message = _sanitize_string(v.message)
|
|
238
|
+
location = f"{file_path}:{v.line}" if v.line else file_path
|
|
219
239
|
if v.column:
|
|
220
240
|
location += f":{v.column}"
|
|
221
241
|
click.echo(f" {location}")
|
|
222
|
-
click.echo(f" [{v.severity.name}] {v.rule_id}: {
|
|
242
|
+
click.echo(f" [{v.severity.name}] {v.rule_id}: {message}")
|
|
223
243
|
click.echo()
|
|
@@ -4,8 +4,9 @@ Purpose: Configuration schema for magic numbers linter
|
|
|
4
4
|
Scope: MagicNumberConfig dataclass with allowed_numbers and max_small_integer settings
|
|
5
5
|
|
|
6
6
|
Overview: Defines configuration schema for magic numbers linter. Provides MagicNumberConfig dataclass
|
|
7
|
-
with allowed_numbers set (default includes common acceptable numbers like -1, 0, 1, 2, 3, 4, 5, 10, 100, 1000
|
|
8
|
-
and
|
|
7
|
+
with allowed_numbers set (default includes common acceptable numbers like -1, 0, 1, 2, 3, 4, 5, 10, 100, 1000
|
|
8
|
+
and standard ports like 80, 443, 22, 21, 8080, 8443, 3000, 5000) and max_small_integer threshold (default 10)
|
|
9
|
+
for range() contexts. Supports per-file and per-directory
|
|
9
10
|
config overrides through from_dict class method. Validates that configuration values are appropriate
|
|
10
11
|
types. Integrates with orchestrator's configuration system to allow users to customize allowed numbers
|
|
11
12
|
via .thailint.yaml configuration files.
|
|
@@ -18,11 +19,41 @@ Interfaces: MagicNumberConfig(allowed_numbers: set, max_small_integer: int, enab
|
|
|
18
19
|
from_dict class method for loading configuration from dictionary
|
|
19
20
|
|
|
20
21
|
Implementation: Dataclass with validation and defaults, matches reference implementation patterns
|
|
22
|
+
|
|
23
|
+
Suppressions:
|
|
24
|
+
- unnecessary-lambda: The lambda is required here because dataclass field default_factory
|
|
25
|
+
needs a callable, and DEFAULT_ALLOWED_NUMBERS.copy() is a method call, not a callable.
|
|
26
|
+
Using `default_factory=DEFAULT_ALLOWED_NUMBERS.copy` would call the method at class
|
|
27
|
+
definition time, not at instance creation time.
|
|
21
28
|
"""
|
|
22
29
|
|
|
23
30
|
from dataclasses import dataclass, field
|
|
24
31
|
from typing import Any
|
|
25
32
|
|
|
33
|
+
# Default allowed numbers including common small integers and standard ports
|
|
34
|
+
DEFAULT_ALLOWED_NUMBERS: set[int | float] = {
|
|
35
|
+
# Common small integers
|
|
36
|
+
-1,
|
|
37
|
+
0,
|
|
38
|
+
1,
|
|
39
|
+
2,
|
|
40
|
+
3,
|
|
41
|
+
4,
|
|
42
|
+
5,
|
|
43
|
+
10,
|
|
44
|
+
100,
|
|
45
|
+
1000,
|
|
46
|
+
# Standard ports
|
|
47
|
+
21, # FTP
|
|
48
|
+
22, # SSH
|
|
49
|
+
80, # HTTP
|
|
50
|
+
443, # HTTPS
|
|
51
|
+
3000, # Common dev server (Node.js, Rails)
|
|
52
|
+
5000, # Flask default
|
|
53
|
+
8080, # Alternate HTTP
|
|
54
|
+
8443, # Alternate HTTPS
|
|
55
|
+
}
|
|
56
|
+
|
|
26
57
|
|
|
27
58
|
@dataclass
|
|
28
59
|
class MagicNumberConfig:
|
|
@@ -30,10 +61,11 @@ class MagicNumberConfig:
|
|
|
30
61
|
|
|
31
62
|
enabled: bool = True
|
|
32
63
|
allowed_numbers: set[int | float] = field(
|
|
33
|
-
default_factory=lambda:
|
|
64
|
+
default_factory=lambda: DEFAULT_ALLOWED_NUMBERS.copy() # pylint: disable=unnecessary-lambda
|
|
34
65
|
)
|
|
35
66
|
max_small_integer: int = 10
|
|
36
67
|
ignore: list[str] = field(default_factory=list)
|
|
68
|
+
exempt_definition_files: bool = True
|
|
37
69
|
|
|
38
70
|
def __post_init__(self) -> None:
|
|
39
71
|
"""Validate configuration values."""
|
|
@@ -58,16 +90,14 @@ class MagicNumberConfig:
|
|
|
58
90
|
allowed_numbers = set(
|
|
59
91
|
lang_config.get(
|
|
60
92
|
"allowed_numbers",
|
|
61
|
-
config.get("allowed_numbers",
|
|
93
|
+
config.get("allowed_numbers", DEFAULT_ALLOWED_NUMBERS),
|
|
62
94
|
)
|
|
63
95
|
)
|
|
64
96
|
max_small_integer = lang_config.get(
|
|
65
97
|
"max_small_integer", config.get("max_small_integer", 10)
|
|
66
98
|
)
|
|
67
99
|
else:
|
|
68
|
-
allowed_numbers = set(
|
|
69
|
-
config.get("allowed_numbers", {-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000})
|
|
70
|
-
)
|
|
100
|
+
allowed_numbers = set(config.get("allowed_numbers", DEFAULT_ALLOWED_NUMBERS))
|
|
71
101
|
max_small_integer = config.get("max_small_integer", 10)
|
|
72
102
|
|
|
73
103
|
ignore_patterns = config.get("ignore", [])
|
|
@@ -79,4 +109,5 @@ class MagicNumberConfig:
|
|
|
79
109
|
allowed_numbers=allowed_numbers,
|
|
80
110
|
max_small_integer=max_small_integer,
|
|
81
111
|
ignore=ignore_patterns,
|
|
112
|
+
exempt_definition_files=config.get("exempt_definition_files", True),
|
|
82
113
|
)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Detect constant definition files that should be exempt from magic number checking
|
|
3
|
+
|
|
4
|
+
Scope: File-level detection of definition patterns (status codes, constants files)
|
|
5
|
+
|
|
6
|
+
Overview: Provides functions to detect if a file is a constant definition file that should
|
|
7
|
+
be exempt from magic number violations. Definition files exist specifically to define
|
|
8
|
+
named constants and shouldn't be flagged. Detection is based on:
|
|
9
|
+
1. Filename patterns (*_codes.py, *_constants.py, constants.py)
|
|
10
|
+
2. Content patterns (dicts with 5+ int keys, 10+ UPPERCASE constant assignments)
|
|
11
|
+
Files matching these patterns contain legitimate constant definitions.
|
|
12
|
+
|
|
13
|
+
Dependencies: ast module for parsing, pathlib for Path handling, re for pattern matching
|
|
14
|
+
|
|
15
|
+
Exports: is_definition_file function
|
|
16
|
+
|
|
17
|
+
Interfaces: is_definition_file(file_path, content) -> bool
|
|
18
|
+
|
|
19
|
+
Implementation: Filename pattern matching and AST-based content analysis
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import ast
|
|
23
|
+
import re
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
# Threshold for number of UPPERCASE constants to consider a file as definition file
|
|
27
|
+
MIN_UPPERCASE_CONSTANTS = 10
|
|
28
|
+
|
|
29
|
+
# Threshold for number of int keys in a dict to consider it a definition pattern
|
|
30
|
+
MIN_DICT_INT_KEYS = 5
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_definition_file(file_path: Path | str | None, content: str | None) -> bool:
|
|
34
|
+
"""Check if file is a constant definition file that should be exempt.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
file_path: Path to the file
|
|
38
|
+
content: File content
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if file is a definition file that should be exempt
|
|
42
|
+
"""
|
|
43
|
+
if _matches_definition_filename(file_path):
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
if content and _has_definition_content_patterns(content):
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _matches_definition_filename(file_path: Path | str | None) -> bool:
|
|
53
|
+
"""Check if filename matches definition file patterns.
|
|
54
|
+
|
|
55
|
+
Patterns:
|
|
56
|
+
- *_codes.py (status_codes.py, error_codes.py, etc.)
|
|
57
|
+
- *_constants.py (app_constants.py, etc.)
|
|
58
|
+
- constants.py
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
file_path: Path to the file
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True if filename matches definition patterns
|
|
65
|
+
"""
|
|
66
|
+
if not file_path:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
file_name = Path(file_path).name.lower()
|
|
70
|
+
|
|
71
|
+
# Check for *_codes.py pattern
|
|
72
|
+
if file_name.endswith("_codes.py"):
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
# Check for constants.py or *_constants.py
|
|
76
|
+
if file_name == "constants.py" or file_name.endswith("_constants.py"):
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _has_definition_content_patterns(content: str) -> bool:
|
|
83
|
+
"""Check if content has definition file patterns.
|
|
84
|
+
|
|
85
|
+
Patterns:
|
|
86
|
+
- 10+ UPPERCASE constant assignments
|
|
87
|
+
- Dict with 5+ integer keys
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
content: File content
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if content matches definition patterns
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
tree = ast.parse(content)
|
|
97
|
+
except SyntaxError:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# Check for many UPPERCASE constants
|
|
101
|
+
if _count_uppercase_constants(tree) >= MIN_UPPERCASE_CONSTANTS:
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
# Check for dicts with many int keys
|
|
105
|
+
if _has_dict_with_int_keys(tree):
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _count_uppercase_constants(tree: ast.Module) -> int:
|
|
112
|
+
"""Count UPPERCASE constant assignments at module level.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
tree: Parsed AST module
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Number of UPPERCASE constant assignments
|
|
119
|
+
"""
|
|
120
|
+
count = 0
|
|
121
|
+
for node in tree.body:
|
|
122
|
+
if isinstance(node, ast.Assign):
|
|
123
|
+
count += _count_numeric_constant_targets(node)
|
|
124
|
+
return count
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _count_numeric_constant_targets(assign_node: ast.Assign) -> int:
|
|
128
|
+
"""Count UPPERCASE constant targets with numeric values in an assignment.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
assign_node: AST Assign node
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Number of uppercase constant targets with numeric values
|
|
135
|
+
"""
|
|
136
|
+
if not _is_numeric_constant(assign_node.value):
|
|
137
|
+
return 0
|
|
138
|
+
return sum(1 for t in assign_node.targets if _is_uppercase_name_target(t))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _is_numeric_constant(value: ast.expr) -> bool:
|
|
142
|
+
"""Check if value is a numeric constant.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
value: AST expression node
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
True if value is a numeric constant
|
|
149
|
+
"""
|
|
150
|
+
return isinstance(value, ast.Constant) and isinstance(value.value, (int, float))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _is_uppercase_name_target(target: ast.expr) -> bool:
|
|
154
|
+
"""Check if target is an uppercase name.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
target: AST expression node
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
True if target is an uppercase Name node
|
|
161
|
+
"""
|
|
162
|
+
return isinstance(target, ast.Name) and _is_constant_name(target.id)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _is_constant_name(name: str) -> bool:
|
|
166
|
+
"""Check if name follows UPPERCASE constant convention.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
name: Variable name
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True if name is UPPERCASE (with underscores allowed)
|
|
173
|
+
"""
|
|
174
|
+
# Must be uppercase and contain at least 2 characters
|
|
175
|
+
if len(name) < 2:
|
|
176
|
+
return False
|
|
177
|
+
# Allow underscores but must have uppercase letters
|
|
178
|
+
return re.match(r"^[A-Z][A-Z0-9_]*$", name) is not None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _has_dict_with_int_keys(tree: ast.Module) -> bool:
|
|
182
|
+
"""Check if module has a dict with many integer keys.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
tree: Parsed AST module
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
True if there's a dict with MIN_DICT_INT_KEYS+ int keys
|
|
189
|
+
"""
|
|
190
|
+
return any(_has_enough_int_keys(node) for node in ast.walk(tree) if isinstance(node, ast.Dict))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _has_enough_int_keys(dict_node: ast.Dict) -> bool:
|
|
194
|
+
"""Check if dict has enough integer keys to be a definition pattern.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
dict_node: AST Dict node
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
True if dict has MIN_DICT_INT_KEYS or more integer keys
|
|
201
|
+
"""
|
|
202
|
+
return _count_int_keys(dict_node) >= MIN_DICT_INT_KEYS
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _count_int_keys(dict_node: ast.Dict) -> int:
|
|
206
|
+
"""Count integer keys in a dict.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
dict_node: AST Dict node
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Number of integer constant keys
|
|
213
|
+
"""
|
|
214
|
+
return sum(1 for key in dict_node.keys if _is_int_key(key))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _is_int_key(key: ast.expr | None) -> bool:
|
|
218
|
+
"""Check if key is an integer constant.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
key: AST expression node (or None for **dict unpacking)
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
True if key is an integer constant
|
|
225
|
+
"""
|
|
226
|
+
return isinstance(key, ast.Constant) and isinstance(key.value, int)
|
|
@@ -40,6 +40,7 @@ from src.linter_config.ignore import get_ignore_parser
|
|
|
40
40
|
|
|
41
41
|
from .config import MagicNumberConfig
|
|
42
42
|
from .context_analyzer import is_acceptable_context
|
|
43
|
+
from .definition_detector import is_definition_file
|
|
43
44
|
from .python_analyzer import PythonMagicNumberAnalyzer
|
|
44
45
|
from .typescript_analyzer import TypeScriptMagicNumberAnalyzer
|
|
45
46
|
from .typescript_ignore_checker import TypeScriptIgnoreChecker
|
|
@@ -174,6 +175,12 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
174
175
|
if self._is_file_ignored(context, config):
|
|
175
176
|
return []
|
|
176
177
|
|
|
178
|
+
# Check if file is a definition file (status_codes.py, constants.py, etc.)
|
|
179
|
+
if config.exempt_definition_files and is_definition_file(
|
|
180
|
+
context.file_path, context.file_content
|
|
181
|
+
):
|
|
182
|
+
return []
|
|
183
|
+
|
|
177
184
|
tree = self._parse_python_code(context.file_content)
|
|
178
185
|
if tree is None:
|
|
179
186
|
return []
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Python AST-based nesting depth calculator
|
|
3
|
+
|
|
4
|
+
Scope: Python code nesting depth analysis using ast module
|
|
5
|
+
|
|
6
|
+
Overview: Analyzes Python code to calculate maximum nesting depth using AST traversal. Implements
|
|
7
|
+
visitor pattern to walk AST, tracking current depth and maximum depth found. Increments depth
|
|
8
|
+
for If, For, While, With, AsyncWith, Try, ExceptHandler, Match, and match_case nodes. Correctly
|
|
9
|
+
handles elif chains by detecting when an If node is in elif position (sole child in parent's
|
|
10
|
+
orelse list) and not incrementing depth. Starts depth counting at 1 for function body, matching
|
|
11
|
+
reference implementation behavior. Returns maximum depth found and location information for
|
|
12
|
+
violation reporting. Provides helper method to find all function definitions in an AST tree
|
|
13
|
+
for batch processing.
|
|
14
|
+
|
|
15
|
+
Dependencies: ast module for Python parsing
|
|
16
|
+
|
|
17
|
+
Exports: PythonNestingAnalyzer class with calculate_max_depth method
|
|
18
|
+
|
|
19
|
+
Interfaces: calculate_max_depth(func_node: ast.FunctionDef) -> tuple[int, int], find_all_functions
|
|
20
|
+
|
|
21
|
+
Implementation: AST visitor pattern with depth tracking, elif detection via parent orelse inspection
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import ast
|
|
25
|
+
|
|
26
|
+
# Control structure types that increase nesting depth
|
|
27
|
+
_CONTROL_STRUCTURES = (
|
|
28
|
+
ast.For,
|
|
29
|
+
ast.While,
|
|
30
|
+
ast.With,
|
|
31
|
+
ast.AsyncWith,
|
|
32
|
+
ast.Try,
|
|
33
|
+
ast.Match,
|
|
34
|
+
ast.match_case,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _DepthTracker:
|
|
39
|
+
"""Tracks maximum nesting depth during AST traversal."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, default_line: int) -> None:
|
|
42
|
+
"""Initialize tracker with default line number."""
|
|
43
|
+
self.max_depth = 0
|
|
44
|
+
self.max_depth_line = default_line
|
|
45
|
+
|
|
46
|
+
def record(self, node: ast.AST, depth: int, default_line: int) -> None:
|
|
47
|
+
"""Record depth if it's the new maximum."""
|
|
48
|
+
if depth > self.max_depth:
|
|
49
|
+
self.max_depth = depth
|
|
50
|
+
self.max_depth_line = getattr(node, "lineno", default_line)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PythonNestingAnalyzer:
|
|
54
|
+
"""Calculates maximum nesting depth in Python functions."""
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
"""Initialize the Python nesting analyzer."""
|
|
58
|
+
pass # Stateless analyzer for nesting depth calculation
|
|
59
|
+
|
|
60
|
+
def calculate_max_depth(
|
|
61
|
+
self, func_node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
62
|
+
) -> tuple[int, int]:
|
|
63
|
+
"""Calculate maximum nesting depth in a function.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
func_node: AST node for function definition
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Tuple of (max_depth, line_number_of_max_depth)
|
|
70
|
+
"""
|
|
71
|
+
tracker = _DepthTracker(func_node.lineno)
|
|
72
|
+
|
|
73
|
+
for stmt in func_node.body:
|
|
74
|
+
_visit_node(stmt, 0, tracker, func_node.lineno)
|
|
75
|
+
|
|
76
|
+
return tracker.max_depth, tracker.max_depth_line
|
|
77
|
+
|
|
78
|
+
def find_all_functions(self, tree: ast.AST) -> list[ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
79
|
+
"""Find all function definitions in AST.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
tree: Python AST to search
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
List of all FunctionDef and AsyncFunctionDef nodes found
|
|
86
|
+
"""
|
|
87
|
+
functions = []
|
|
88
|
+
for node in ast.walk(tree):
|
|
89
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
90
|
+
functions.append(node)
|
|
91
|
+
return functions
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _visit_node(
|
|
95
|
+
node: ast.AST,
|
|
96
|
+
current_depth: int,
|
|
97
|
+
tracker: _DepthTracker,
|
|
98
|
+
default_line: int,
|
|
99
|
+
is_elif: bool = False,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Visit AST node, tracking nesting depth for control structures only."""
|
|
102
|
+
if isinstance(node, ast.If):
|
|
103
|
+
_visit_if_node(node, current_depth, tracker, default_line, is_elif)
|
|
104
|
+
elif isinstance(node, _CONTROL_STRUCTURES):
|
|
105
|
+
_visit_control_structure(node, current_depth, tracker, default_line)
|
|
106
|
+
else:
|
|
107
|
+
_visit_children(node, current_depth, tracker, default_line)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _visit_if_node(
|
|
111
|
+
node: ast.If, current_depth: int, tracker: _DepthTracker, default_line: int, is_elif: bool
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Visit If node with special elif handling."""
|
|
114
|
+
if not is_elif:
|
|
115
|
+
current_depth += 1
|
|
116
|
+
tracker.record(node, current_depth, default_line)
|
|
117
|
+
|
|
118
|
+
# Visit body
|
|
119
|
+
for child in node.body:
|
|
120
|
+
_visit_node(child, current_depth, tracker, default_line)
|
|
121
|
+
|
|
122
|
+
# Handle orelse - check for elif chain
|
|
123
|
+
if _is_elif_chain(node.orelse):
|
|
124
|
+
_visit_node(node.orelse[0], current_depth, tracker, default_line, is_elif=True)
|
|
125
|
+
else:
|
|
126
|
+
for child in node.orelse:
|
|
127
|
+
_visit_node(child, current_depth, tracker, default_line)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _visit_control_structure(
|
|
131
|
+
node: ast.AST, current_depth: int, tracker: _DepthTracker, default_line: int
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Visit a control structure node that increases depth."""
|
|
134
|
+
current_depth += 1
|
|
135
|
+
tracker.record(node, current_depth, default_line)
|
|
136
|
+
_visit_children(node, current_depth, tracker, default_line)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _visit_children(
|
|
140
|
+
node: ast.AST, current_depth: int, tracker: _DepthTracker, default_line: int
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Visit all children of a node without incrementing depth."""
|
|
143
|
+
for child in ast.iter_child_nodes(node):
|
|
144
|
+
_visit_node(child, current_depth, tracker, default_line)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _is_elif_chain(orelse: list[ast.stmt]) -> bool:
|
|
148
|
+
"""Check if orelse list represents an elif (single If node).
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
orelse: The orelse list from an If node
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
True if this is an elif (single If in orelse), False otherwise
|
|
155
|
+
"""
|
|
156
|
+
return len(orelse) == 1 and isinstance(orelse[0], ast.If)
|
|
@@ -30,6 +30,8 @@ class StatelessClassConfig:
|
|
|
30
30
|
enabled: bool = True
|
|
31
31
|
min_methods: int = 2
|
|
32
32
|
ignore: list[str] = field(default_factory=list)
|
|
33
|
+
exempt_test_classes: bool = True
|
|
34
|
+
exempt_mixins: bool = True
|
|
33
35
|
|
|
34
36
|
@classmethod
|
|
35
37
|
def from_dict(
|
|
@@ -55,4 +57,6 @@ class StatelessClassConfig:
|
|
|
55
57
|
enabled=config.get("enabled", True),
|
|
56
58
|
min_methods=config.get("min_methods", 2),
|
|
57
59
|
ignore=ignore_patterns,
|
|
60
|
+
exempt_test_classes=config.get("exempt_test_classes", True),
|
|
61
|
+
exempt_mixins=config.get("exempt_mixins", True),
|
|
58
62
|
)
|