thailint 0.15.1__tar.gz → 0.15.3__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 (226) hide show
  1. {thailint-0.15.1 → thailint-0.15.3}/CHANGELOG.md +6 -0
  2. {thailint-0.15.1 → thailint-0.15.3}/PKG-INFO +3 -3
  3. {thailint-0.15.1 → thailint-0.15.3}/README.md +2 -2
  4. {thailint-0.15.1 → thailint-0.15.3}/pyproject.toml +1 -1
  5. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/linter.py +79 -8
  6. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/violation_generator.py +70 -10
  7. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lazy_ignores/config.py +6 -0
  8. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lazy_ignores/directive_utils.py +47 -4
  9. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lazy_ignores/matcher.py +42 -9
  10. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lazy_ignores/python_analyzer.py +1 -1
  11. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lazy_ignores/types.py +1 -0
  12. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lazy_ignores/violation_builder.py +5 -1
  13. {thailint-0.15.1 → thailint-0.15.3}/src/linters/srp/heuristics.py +47 -14
  14. {thailint-0.15.1 → thailint-0.15.3}/src/linters/srp/typescript_metrics_calculator.py +34 -10
  15. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/storage.py +4 -14
  16. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/violation_generator.py +27 -13
  17. {thailint-0.15.1 → thailint-0.15.3}/LICENSE +0 -0
  18. {thailint-0.15.1 → thailint-0.15.3}/src/__init__.py +0 -0
  19. {thailint-0.15.1 → thailint-0.15.3}/src/analyzers/__init__.py +0 -0
  20. {thailint-0.15.1 → thailint-0.15.3}/src/analyzers/ast_utils.py +0 -0
  21. {thailint-0.15.1 → thailint-0.15.3}/src/analyzers/rust_base.py +0 -0
  22. {thailint-0.15.1 → thailint-0.15.3}/src/analyzers/rust_context.py +0 -0
  23. {thailint-0.15.1 → thailint-0.15.3}/src/analyzers/typescript_base.py +0 -0
  24. {thailint-0.15.1 → thailint-0.15.3}/src/api.py +0 -0
  25. {thailint-0.15.1 → thailint-0.15.3}/src/cli/__init__.py +0 -0
  26. {thailint-0.15.1 → thailint-0.15.3}/src/cli/__main__.py +0 -0
  27. {thailint-0.15.1 → thailint-0.15.3}/src/cli/config.py +0 -0
  28. {thailint-0.15.1 → thailint-0.15.3}/src/cli/config_merge.py +0 -0
  29. {thailint-0.15.1 → thailint-0.15.3}/src/cli/linters/__init__.py +0 -0
  30. {thailint-0.15.1 → thailint-0.15.3}/src/cli/linters/code_patterns.py +0 -0
  31. {thailint-0.15.1 → thailint-0.15.3}/src/cli/linters/code_smells.py +0 -0
  32. {thailint-0.15.1 → thailint-0.15.3}/src/cli/linters/documentation.py +0 -0
  33. {thailint-0.15.1 → thailint-0.15.3}/src/cli/linters/performance.py +0 -0
  34. {thailint-0.15.1 → thailint-0.15.3}/src/cli/linters/shared.py +0 -0
  35. {thailint-0.15.1 → thailint-0.15.3}/src/cli/linters/structure.py +0 -0
  36. {thailint-0.15.1 → thailint-0.15.3}/src/cli/linters/structure_quality.py +0 -0
  37. {thailint-0.15.1 → thailint-0.15.3}/src/cli/main.py +0 -0
  38. {thailint-0.15.1 → thailint-0.15.3}/src/cli/utils.py +0 -0
  39. {thailint-0.15.1 → thailint-0.15.3}/src/cli_main.py +0 -0
  40. {thailint-0.15.1 → thailint-0.15.3}/src/config.py +0 -0
  41. {thailint-0.15.1 → thailint-0.15.3}/src/core/__init__.py +0 -0
  42. {thailint-0.15.1 → thailint-0.15.3}/src/core/base.py +0 -0
  43. {thailint-0.15.1 → thailint-0.15.3}/src/core/cli_utils.py +0 -0
  44. {thailint-0.15.1 → thailint-0.15.3}/src/core/config_parser.py +0 -0
  45. {thailint-0.15.1 → thailint-0.15.3}/src/core/constants.py +0 -0
  46. {thailint-0.15.1 → thailint-0.15.3}/src/core/linter_utils.py +0 -0
  47. {thailint-0.15.1 → thailint-0.15.3}/src/core/python_lint_rule.py +0 -0
  48. {thailint-0.15.1 → thailint-0.15.3}/src/core/registry.py +0 -0
  49. {thailint-0.15.1 → thailint-0.15.3}/src/core/rule_discovery.py +0 -0
  50. {thailint-0.15.1 → thailint-0.15.3}/src/core/types.py +0 -0
  51. {thailint-0.15.1 → thailint-0.15.3}/src/core/violation_builder.py +0 -0
  52. {thailint-0.15.1 → thailint-0.15.3}/src/core/violation_utils.py +0 -0
  53. {thailint-0.15.1 → thailint-0.15.3}/src/formatters/__init__.py +0 -0
  54. {thailint-0.15.1 → thailint-0.15.3}/src/formatters/sarif.py +0 -0
  55. {thailint-0.15.1 → thailint-0.15.3}/src/linter_config/__init__.py +0 -0
  56. {thailint-0.15.1 → thailint-0.15.3}/src/linter_config/directive_markers.py +0 -0
  57. {thailint-0.15.1 → thailint-0.15.3}/src/linter_config/ignore.py +0 -0
  58. {thailint-0.15.1 → thailint-0.15.3}/src/linter_config/loader.py +0 -0
  59. {thailint-0.15.1 → thailint-0.15.3}/src/linter_config/pattern_utils.py +0 -0
  60. {thailint-0.15.1 → thailint-0.15.3}/src/linter_config/rule_matcher.py +0 -0
  61. {thailint-0.15.1 → thailint-0.15.3}/src/linters/__init__.py +0 -0
  62. {thailint-0.15.1 → thailint-0.15.3}/src/linters/collection_pipeline/__init__.py +0 -0
  63. {thailint-0.15.1 → thailint-0.15.3}/src/linters/collection_pipeline/any_all_analyzer.py +0 -0
  64. {thailint-0.15.1 → thailint-0.15.3}/src/linters/collection_pipeline/ast_utils.py +0 -0
  65. {thailint-0.15.1 → thailint-0.15.3}/src/linters/collection_pipeline/config.py +0 -0
  66. {thailint-0.15.1 → thailint-0.15.3}/src/linters/collection_pipeline/continue_analyzer.py +0 -0
  67. {thailint-0.15.1 → thailint-0.15.3}/src/linters/collection_pipeline/detector.py +0 -0
  68. {thailint-0.15.1 → thailint-0.15.3}/src/linters/collection_pipeline/filter_map_analyzer.py +0 -0
  69. {thailint-0.15.1 → thailint-0.15.3}/src/linters/collection_pipeline/linter.py +0 -0
  70. {thailint-0.15.1 → thailint-0.15.3}/src/linters/collection_pipeline/suggestion_builder.py +0 -0
  71. {thailint-0.15.1 → thailint-0.15.3}/src/linters/cqs/__init__.py +0 -0
  72. {thailint-0.15.1 → thailint-0.15.3}/src/linters/cqs/config.py +0 -0
  73. {thailint-0.15.1 → thailint-0.15.3}/src/linters/cqs/function_analyzer.py +0 -0
  74. {thailint-0.15.1 → thailint-0.15.3}/src/linters/cqs/input_detector.py +0 -0
  75. {thailint-0.15.1 → thailint-0.15.3}/src/linters/cqs/linter.py +0 -0
  76. {thailint-0.15.1 → thailint-0.15.3}/src/linters/cqs/output_detector.py +0 -0
  77. {thailint-0.15.1 → thailint-0.15.3}/src/linters/cqs/python_analyzer.py +0 -0
  78. {thailint-0.15.1 → thailint-0.15.3}/src/linters/cqs/types.py +0 -0
  79. {thailint-0.15.1 → thailint-0.15.3}/src/linters/cqs/typescript_cqs_analyzer.py +0 -0
  80. {thailint-0.15.1 → thailint-0.15.3}/src/linters/cqs/typescript_function_analyzer.py +0 -0
  81. {thailint-0.15.1 → thailint-0.15.3}/src/linters/cqs/typescript_input_detector.py +0 -0
  82. {thailint-0.15.1 → thailint-0.15.3}/src/linters/cqs/typescript_output_detector.py +0 -0
  83. {thailint-0.15.1 → thailint-0.15.3}/src/linters/cqs/violation_builder.py +0 -0
  84. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/__init__.py +0 -0
  85. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/base_token_analyzer.py +0 -0
  86. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/block_filter.py +0 -0
  87. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/block_grouper.py +0 -0
  88. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/cache.py +0 -0
  89. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/cache_query.py +0 -0
  90. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/config.py +0 -0
  91. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/config_loader.py +0 -0
  92. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/constant.py +0 -0
  93. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/constant_matcher.py +0 -0
  94. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/constant_violation_builder.py +0 -0
  95. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/deduplicator.py +0 -0
  96. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/duplicate_storage.py +0 -0
  97. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/file_analyzer.py +0 -0
  98. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/inline_ignore.py +0 -0
  99. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/python_analyzer.py +0 -0
  100. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/python_constant_extractor.py +0 -0
  101. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/single_statement_detector.py +0 -0
  102. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/storage_initializer.py +0 -0
  103. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/token_hasher.py +0 -0
  104. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/typescript_analyzer.py +0 -0
  105. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/typescript_constant_extractor.py +0 -0
  106. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/typescript_statement_detector.py +0 -0
  107. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/typescript_value_extractor.py +0 -0
  108. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/violation_builder.py +0 -0
  109. {thailint-0.15.1 → thailint-0.15.3}/src/linters/dry/violation_filter.py +0 -0
  110. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_header/__init__.py +0 -0
  111. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_header/atemporal_detector.py +0 -0
  112. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_header/base_parser.py +0 -0
  113. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_header/bash_parser.py +0 -0
  114. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_header/config.py +0 -0
  115. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_header/css_parser.py +0 -0
  116. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_header/field_validator.py +0 -0
  117. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_header/linter.py +0 -0
  118. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_header/markdown_parser.py +0 -0
  119. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_header/python_parser.py +0 -0
  120. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_header/typescript_parser.py +0 -0
  121. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_header/violation_builder.py +0 -0
  122. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_placement/__init__.py +0 -0
  123. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_placement/config_loader.py +0 -0
  124. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_placement/directory_matcher.py +0 -0
  125. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_placement/linter.py +0 -0
  126. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_placement/path_resolver.py +0 -0
  127. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_placement/pattern_matcher.py +0 -0
  128. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_placement/pattern_validator.py +0 -0
  129. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_placement/rule_checker.py +0 -0
  130. {thailint-0.15.1 → thailint-0.15.3}/src/linters/file_placement/violation_factory.py +0 -0
  131. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lazy_ignores/__init__.py +0 -0
  132. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lazy_ignores/header_parser.py +0 -0
  133. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lazy_ignores/linter.py +0 -0
  134. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lazy_ignores/rule_id_utils.py +0 -0
  135. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lazy_ignores/skip_detector.py +0 -0
  136. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lazy_ignores/typescript_analyzer.py +0 -0
  137. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/__init__.py +0 -0
  138. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/config.py +0 -0
  139. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/linter.py +0 -0
  140. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/pattern_detectors/__init__.py +0 -0
  141. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/pattern_detectors/base.py +0 -0
  142. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/pattern_detectors/dict_key_detector.py +0 -0
  143. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/pattern_detectors/division_check_detector.py +0 -0
  144. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/pattern_detectors/file_exists_detector.py +0 -0
  145. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/pattern_detectors/hasattr_detector.py +0 -0
  146. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/pattern_detectors/isinstance_detector.py +0 -0
  147. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/pattern_detectors/len_check_detector.py +0 -0
  148. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/pattern_detectors/none_check_detector.py +0 -0
  149. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/pattern_detectors/string_validator_detector.py +0 -0
  150. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/python_analyzer.py +0 -0
  151. {thailint-0.15.1 → thailint-0.15.3}/src/linters/lbyl/violation_builder.py +0 -0
  152. {thailint-0.15.1 → thailint-0.15.3}/src/linters/magic_numbers/__init__.py +0 -0
  153. {thailint-0.15.1 → thailint-0.15.3}/src/linters/magic_numbers/config.py +0 -0
  154. {thailint-0.15.1 → thailint-0.15.3}/src/linters/magic_numbers/context_analyzer.py +0 -0
  155. {thailint-0.15.1 → thailint-0.15.3}/src/linters/magic_numbers/linter.py +0 -0
  156. {thailint-0.15.1 → thailint-0.15.3}/src/linters/magic_numbers/python_analyzer.py +0 -0
  157. {thailint-0.15.1 → thailint-0.15.3}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
  158. {thailint-0.15.1 → thailint-0.15.3}/src/linters/magic_numbers/typescript_ignore_checker.py +0 -0
  159. {thailint-0.15.1 → thailint-0.15.3}/src/linters/magic_numbers/violation_builder.py +0 -0
  160. {thailint-0.15.1 → thailint-0.15.3}/src/linters/method_property/__init__.py +0 -0
  161. {thailint-0.15.1 → thailint-0.15.3}/src/linters/method_property/config.py +0 -0
  162. {thailint-0.15.1 → thailint-0.15.3}/src/linters/method_property/linter.py +0 -0
  163. {thailint-0.15.1 → thailint-0.15.3}/src/linters/method_property/python_analyzer.py +0 -0
  164. {thailint-0.15.1 → thailint-0.15.3}/src/linters/method_property/violation_builder.py +0 -0
  165. {thailint-0.15.1 → thailint-0.15.3}/src/linters/nesting/__init__.py +0 -0
  166. {thailint-0.15.1 → thailint-0.15.3}/src/linters/nesting/config.py +0 -0
  167. {thailint-0.15.1 → thailint-0.15.3}/src/linters/nesting/linter.py +0 -0
  168. {thailint-0.15.1 → thailint-0.15.3}/src/linters/nesting/python_analyzer.py +0 -0
  169. {thailint-0.15.1 → thailint-0.15.3}/src/linters/nesting/typescript_analyzer.py +0 -0
  170. {thailint-0.15.1 → thailint-0.15.3}/src/linters/nesting/typescript_function_extractor.py +0 -0
  171. {thailint-0.15.1 → thailint-0.15.3}/src/linters/nesting/violation_builder.py +0 -0
  172. {thailint-0.15.1 → thailint-0.15.3}/src/linters/performance/__init__.py +0 -0
  173. {thailint-0.15.1 → thailint-0.15.3}/src/linters/performance/config.py +0 -0
  174. {thailint-0.15.1 → thailint-0.15.3}/src/linters/performance/constants.py +0 -0
  175. {thailint-0.15.1 → thailint-0.15.3}/src/linters/performance/linter.py +0 -0
  176. {thailint-0.15.1 → thailint-0.15.3}/src/linters/performance/python_analyzer.py +0 -0
  177. {thailint-0.15.1 → thailint-0.15.3}/src/linters/performance/regex_analyzer.py +0 -0
  178. {thailint-0.15.1 → thailint-0.15.3}/src/linters/performance/regex_linter.py +0 -0
  179. {thailint-0.15.1 → thailint-0.15.3}/src/linters/performance/typescript_analyzer.py +0 -0
  180. {thailint-0.15.1 → thailint-0.15.3}/src/linters/performance/violation_builder.py +0 -0
  181. {thailint-0.15.1 → thailint-0.15.3}/src/linters/print_statements/__init__.py +0 -0
  182. {thailint-0.15.1 → thailint-0.15.3}/src/linters/print_statements/config.py +0 -0
  183. {thailint-0.15.1 → thailint-0.15.3}/src/linters/print_statements/linter.py +0 -0
  184. {thailint-0.15.1 → thailint-0.15.3}/src/linters/print_statements/python_analyzer.py +0 -0
  185. {thailint-0.15.1 → thailint-0.15.3}/src/linters/print_statements/typescript_analyzer.py +0 -0
  186. {thailint-0.15.1 → thailint-0.15.3}/src/linters/print_statements/violation_builder.py +0 -0
  187. {thailint-0.15.1 → thailint-0.15.3}/src/linters/srp/__init__.py +0 -0
  188. {thailint-0.15.1 → thailint-0.15.3}/src/linters/srp/class_analyzer.py +0 -0
  189. {thailint-0.15.1 → thailint-0.15.3}/src/linters/srp/config.py +0 -0
  190. {thailint-0.15.1 → thailint-0.15.3}/src/linters/srp/linter.py +0 -0
  191. {thailint-0.15.1 → thailint-0.15.3}/src/linters/srp/metrics_evaluator.py +0 -0
  192. {thailint-0.15.1 → thailint-0.15.3}/src/linters/srp/python_analyzer.py +0 -0
  193. {thailint-0.15.1 → thailint-0.15.3}/src/linters/srp/typescript_analyzer.py +0 -0
  194. {thailint-0.15.1 → thailint-0.15.3}/src/linters/srp/violation_builder.py +0 -0
  195. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stateless_class/__init__.py +0 -0
  196. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stateless_class/config.py +0 -0
  197. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stateless_class/linter.py +0 -0
  198. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stateless_class/python_analyzer.py +0 -0
  199. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/__init__.py +0 -0
  200. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/config.py +0 -0
  201. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/context_filter.py +0 -0
  202. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/function_call_violation_builder.py +0 -0
  203. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/ignore_checker.py +0 -0
  204. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/ignore_utils.py +0 -0
  205. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/linter.py +0 -0
  206. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/python/__init__.py +0 -0
  207. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/python/analyzer.py +0 -0
  208. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/python/call_tracker.py +0 -0
  209. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/python/comparison_tracker.py +0 -0
  210. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/python/condition_extractor.py +0 -0
  211. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/python/conditional_detector.py +0 -0
  212. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/python/constants.py +0 -0
  213. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/python/match_analyzer.py +0 -0
  214. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/python/validation_detector.py +0 -0
  215. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/python/variable_extractor.py +0 -0
  216. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/storage_initializer.py +0 -0
  217. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/typescript/__init__.py +0 -0
  218. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/typescript/analyzer.py +0 -0
  219. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/typescript/call_tracker.py +0 -0
  220. {thailint-0.15.1 → thailint-0.15.3}/src/linters/stringly_typed/typescript/comparison_tracker.py +0 -0
  221. {thailint-0.15.1 → thailint-0.15.3}/src/orchestrator/__init__.py +0 -0
  222. {thailint-0.15.1 → thailint-0.15.3}/src/orchestrator/core.py +0 -0
  223. {thailint-0.15.1 → thailint-0.15.3}/src/orchestrator/language_detector.py +0 -0
  224. {thailint-0.15.1 → thailint-0.15.3}/src/templates/thailint_config_template.yaml +0 -0
  225. {thailint-0.15.1 → thailint-0.15.3}/src/utils/__init__.py +0 -0
  226. {thailint-0.15.1 → thailint-0.15.3}/src/utils/project_root.py +0 -0
@@ -27,6 +27,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
27
27
  ### Added
28
28
 
29
29
  - **Stateless Class Linter** - Detect Python classes without state that should be module-level functions
30
+
31
+ ## [0.15.3] - 2026-01-26
32
+
33
+ ### Fixed
34
+
35
+ - **DRY Linter**: Ignore directives (`# thailint: ignore-start dry` / `ignore-end`) now work correctly for both duplicate code and duplicate constant detection (#144)
30
36
  - AST-based detection of classes without `__init__`/`__new__` constructors
31
37
  - Detects classes without instance state (`self.attr` assignments)
32
38
  - Excludes ABC, Protocol, and decorated classes (legitimate patterns)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thailint
3
- Version: 0.15.1
3
+ Version: 0.15.3
4
4
  Summary: The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -38,7 +38,7 @@ Description-Content-Type: text/markdown
38
38
 
39
39
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
40
40
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
41
- [![PyPI](https://img.shields.io/pypi/v/thai-lint)](https://pypi.org/project/thai-lint/)
41
+ [![PyPI](https://img.shields.io/pypi/v/thailint)](https://pypi.org/project/thailint/)
42
42
  [![Documentation](https://readthedocs.org/projects/thai-lint/badge/?version=latest)](https://thai-lint.readthedocs.io/)
43
43
 
44
44
  **The AI Linter** - Catch the mistakes AI coding assistants keep making.
@@ -48,7 +48,7 @@ thailint detects anti-patterns that AI tools frequently introduce: duplicate cod
48
48
  ## Installation
49
49
 
50
50
  ```bash
51
- pip install thai-lint
51
+ pip install thailint
52
52
  ```
53
53
 
54
54
  Or with Docker:
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
5
- [![PyPI](https://img.shields.io/pypi/v/thai-lint)](https://pypi.org/project/thai-lint/)
5
+ [![PyPI](https://img.shields.io/pypi/v/thailint)](https://pypi.org/project/thailint/)
6
6
  [![Documentation](https://readthedocs.org/projects/thai-lint/badge/?version=latest)](https://thai-lint.readthedocs.io/)
7
7
 
8
8
  **The AI Linter** - Catch the mistakes AI coding assistants keep making.
@@ -12,7 +12,7 @@ thailint detects anti-patterns that AI tools frequently introduce: duplicate cod
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- pip install thai-lint
15
+ pip install thailint
16
16
  ```
17
17
 
18
18
  Or with Docker:
@@ -17,7 +17,7 @@ build-backend = "poetry.core.masonry.api"
17
17
 
18
18
  [tool.poetry]
19
19
  name = "thailint"
20
- version = "0.15.1"
20
+ version = "0.15.3"
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"
@@ -21,7 +21,9 @@ Interfaces: DRYRule.check(context) -> list[Violation], finalize() -> list[Violat
21
21
  Implementation: Delegates all logic to helper classes, maintains only orchestration and state
22
22
 
23
23
  Suppressions:
24
- - too-many-instance-attributes: DRYComponents groups related helper dependencies
24
+ - too-many-instance-attributes: DRYComponents groups helper dependencies; DRYRule has 8
25
+ attributes due to stateful caching requirements (storage, config, constants, file contents
26
+ for ignore directive processing)
25
27
  - B101: Type narrowing assertions after guards (storage initialized, file_path/content set)
26
28
  """
27
29
 
@@ -34,6 +36,7 @@ from pathlib import Path
34
36
  from src.core.base import BaseLintContext, BaseLintRule
35
37
  from src.core.linter_utils import should_process_file
36
38
  from src.core.types import Violation
39
+ from src.linter_config.ignore import IgnoreDirectiveParser
37
40
 
38
41
  from .config import DRYConfig
39
42
  from .config_loader import ConfigLoader
@@ -46,7 +49,7 @@ from .inline_ignore import InlineIgnoreParser
46
49
  from .python_constant_extractor import extract_python_constants
47
50
  from .storage_initializer import StorageInitializer
48
51
  from .typescript_constant_extractor import TypeScriptConstantExtractor
49
- from .violation_generator import ViolationGenerator
52
+ from .violation_generator import IgnoreContext, ViolationGenerator
50
53
 
51
54
 
52
55
  @dataclass
@@ -62,7 +65,7 @@ class DRYComponents: # pylint: disable=too-many-instance-attributes
62
65
  constant_violation_builder: ConstantViolationBuilder
63
66
 
64
67
 
65
- class DRYRule(BaseLintRule):
68
+ class DRYRule(BaseLintRule): # pylint: disable=too-many-instance-attributes
66
69
  """Detects duplicate code across project files."""
67
70
 
68
71
  def __init__(self) -> None:
@@ -71,10 +74,14 @@ class DRYRule(BaseLintRule):
71
74
  self._initialized = False
72
75
  self._config: DRYConfig | None = None
73
76
  self._file_analyzer: FileAnalyzer | None = None
77
+ self._project_root: Path | None = None
74
78
 
75
79
  # Collected constants for cross-file detection: list of (file_path, ConstantInfo)
76
80
  self._constants: list[tuple[Path, ConstantInfo]] = []
77
81
 
82
+ # Cache file contents for ignore directive checking during finalize
83
+ self._file_contents: dict[str, str] = {}
84
+
78
85
  # Helper components grouped to reduce instance attributes
79
86
  self._helpers = DRYComponents(
80
87
  config_loader=ConfigLoader(),
@@ -133,6 +140,12 @@ class DRYRule(BaseLintRule):
133
140
  assert context.file_content is not None # nosec B101
134
141
 
135
142
  file_path = context.file_path
143
+ # Cache file content for ignore directive checking in finalize
144
+ self._file_contents[str(file_path)] = context.file_content
145
+ # Get project root from context metadata if available
146
+ if self._project_root is None:
147
+ self._project_root = self._get_project_root(context)
148
+
136
149
  self._helpers.inline_ignore.parse_file(file_path, context.file_content)
137
150
  self._ensure_storage_initialized(context, config)
138
151
  self._analyze_and_store(context, config)
@@ -182,21 +195,56 @@ class DRYRule(BaseLintRule):
182
195
  if extract_fn:
183
196
  self._constants.extend((file_path, c) for c in extract_fn(context.file_content))
184
197
 
198
+ def _get_project_root(self, context: BaseLintContext) -> Path | None:
199
+ """Get project root from context if available.
200
+
201
+ Args:
202
+ context: Lint context
203
+
204
+ Returns:
205
+ Project root path or None if not available
206
+ """
207
+ # Try to get from metadata (orchestrator sets this)
208
+ if hasattr(context, "metadata") and isinstance(context.metadata, dict):
209
+ project_root = context.metadata.get("project_root")
210
+ if project_root:
211
+ return Path(project_root)
212
+
213
+ # Fallback: derive from file path
214
+ if context.file_path:
215
+ return Path(context.file_path).parent
216
+
217
+ return None
218
+
185
219
  def finalize(self) -> list[Violation]:
186
220
  """Generate violations after all files processed."""
187
221
  if not self._storage or not self._config:
188
222
  return []
223
+
224
+ # Create ignore context for violation filtering
225
+ ignore_parser = IgnoreDirectiveParser(self._project_root)
226
+ ignore_ctx = IgnoreContext(
227
+ inline_ignore=self._helpers.inline_ignore,
228
+ shared_parser=ignore_parser,
229
+ file_contents=self._file_contents,
230
+ )
231
+
189
232
  violations = self._helpers.violation_generator.generate_violations(
190
- self._storage, self.rule_id, self._config, self._helpers.inline_ignore
233
+ self._storage, self.rule_id, self._config, ignore_ctx
191
234
  )
192
235
  if self._config.detect_duplicate_constants and self._constants:
193
- violations.extend(
194
- _generate_constant_violations(
195
- self._constants, self._config, self._helpers, self.rule_id
196
- )
236
+ constant_violations = _generate_constant_violations(
237
+ self._constants, self._config, self._helpers, self.rule_id
197
238
  )
239
+ # Filter constant violations through shared ignore parser
240
+ constant_violations = _filter_ignored_violations(
241
+ constant_violations, ignore_parser, self._file_contents
242
+ )
243
+ violations.extend(constant_violations)
244
+
198
245
  self._helpers.inline_ignore.clear()
199
246
  self._constants = []
247
+ self._file_contents = {}
200
248
  return violations
201
249
 
202
250
 
@@ -225,3 +273,26 @@ def _generate_constant_violations(
225
273
  groups = find_constant_groups(constants)
226
274
  helpers.constant_violation_builder.min_occurrences = config.min_constant_occurrences
227
275
  return helpers.constant_violation_builder.build_violations(groups, rule_id)
276
+
277
+
278
+ def _filter_ignored_violations(
279
+ violations: list[Violation],
280
+ ignore_parser: IgnoreDirectiveParser,
281
+ file_contents: dict[str, str],
282
+ ) -> list[Violation]:
283
+ """Filter violations through the shared ignore directive parser.
284
+
285
+ Args:
286
+ violations: List of violations to filter
287
+ ignore_parser: Shared ignore directive parser
288
+ file_contents: Cached file contents for checking ignore directives
289
+
290
+ Returns:
291
+ Filtered list of violations not matching ignore directives
292
+ """
293
+ filtered = []
294
+ for violation in violations:
295
+ file_content = file_contents.get(violation.file_path, "")
296
+ if not ignore_parser.should_ignore_violation(violation, file_content):
297
+ filtered.append(violation)
298
+ return filtered
@@ -10,14 +10,16 @@ Overview: Handles violation generation for duplicate code blocks. Queries storag
10
10
 
11
11
  Dependencies: DuplicateStorage, ViolationDeduplicator, DRYViolationBuilder, Violation, DRYConfig
12
12
 
13
- Exports: ViolationGenerator class
13
+ Exports: ViolationGenerator class, IgnoreContext dataclass
14
14
 
15
15
  Interfaces: ViolationGenerator.generate_violations(storage, rule_id, config) -> list[Violation]
16
16
 
17
17
  Implementation: Queries storage, deduplicates blocks, builds violations, filters by ignore patterns
18
18
  """
19
19
 
20
+ from dataclasses import dataclass
20
21
  from pathlib import Path
22
+ from typing import TYPE_CHECKING
21
23
 
22
24
  from src.core.types import Violation
23
25
  from src.orchestrator.language_detector import detect_language
@@ -28,6 +30,18 @@ from .duplicate_storage import DuplicateStorage
28
30
  from .inline_ignore import InlineIgnoreParser
29
31
  from .violation_builder import DRYViolationBuilder
30
32
 
33
+ if TYPE_CHECKING:
34
+ from src.linter_config.ignore import IgnoreDirectiveParser
35
+
36
+
37
+ @dataclass
38
+ class IgnoreContext:
39
+ """Context for ignore directive filtering."""
40
+
41
+ inline_ignore: InlineIgnoreParser
42
+ shared_parser: "IgnoreDirectiveParser | None" = None
43
+ file_contents: dict[str, str] | None = None
44
+
31
45
 
32
46
  class ViolationGenerator:
33
47
  """Generates violations from duplicate code blocks."""
@@ -42,7 +56,7 @@ class ViolationGenerator:
42
56
  storage: DuplicateStorage,
43
57
  rule_id: str,
44
58
  config: DRYConfig,
45
- inline_ignore: InlineIgnoreParser,
59
+ ignore_ctx: IgnoreContext,
46
60
  ) -> list[Violation]:
47
61
  """Generate violations from storage.
48
62
 
@@ -50,19 +64,42 @@ class ViolationGenerator:
50
64
  storage: Duplicate storage instance
51
65
  rule_id: Rule identifier for violations
52
66
  config: DRY configuration with ignore patterns
53
- inline_ignore: Parser with inline ignore directives
67
+ ignore_ctx: Context containing ignore parsers and file contents
54
68
 
55
69
  Returns:
56
70
  List of violations filtered by ignore patterns and inline directives
57
71
  """
58
- duplicate_hashes = storage.duplicate_hashes
59
- violations = []
72
+ raw_violations = self._collect_violations(storage, rule_id, config)
73
+ deduplicated = self._deduplicator.deduplicate_violations(raw_violations)
74
+ pattern_filtered = self._filter_ignored(deduplicated, config.ignore_patterns)
75
+ inline_filtered = self._filter_inline_ignored(pattern_filtered, ignore_ctx.inline_ignore)
76
+
77
+ # Apply shared ignore directive filtering for block and line directives
78
+ if ignore_ctx.shared_parser and ignore_ctx.file_contents:
79
+ return self._filter_shared_ignored(
80
+ inline_filtered, ignore_ctx.shared_parser, ignore_ctx.file_contents
81
+ )
82
+
83
+ return inline_filtered
60
84
 
61
- for hash_value in duplicate_hashes:
85
+ def _collect_violations(
86
+ self, storage: DuplicateStorage, rule_id: str, config: DRYConfig
87
+ ) -> list[Violation]:
88
+ """Collect raw violations from storage duplicate hashes.
89
+
90
+ Args:
91
+ storage: Duplicate storage instance
92
+ rule_id: Rule identifier for violations
93
+ config: DRY configuration
94
+
95
+ Returns:
96
+ List of raw violations before filtering
97
+ """
98
+ violations = []
99
+ for hash_value in storage.duplicate_hashes:
62
100
  blocks = storage.get_blocks_for_hash(hash_value)
63
101
  dedup_blocks = self._deduplicator.deduplicate_blocks(blocks)
64
102
 
65
- # Check min_occurrences threshold (language-aware)
66
103
  if not self._meets_min_occurrences(dedup_blocks, config):
67
104
  continue
68
105
 
@@ -70,9 +107,7 @@ class ViolationGenerator:
70
107
  violation = self._violation_builder.build_violation(block, dedup_blocks, rule_id)
71
108
  violations.append(violation)
72
109
 
73
- deduplicated = self._deduplicator.deduplicate_violations(violations)
74
- pattern_filtered = self._filter_ignored(deduplicated, config.ignore_patterns)
75
- return self._filter_inline_ignored(pattern_filtered, inline_ignore)
110
+ return violations
76
111
 
77
112
  def _meets_min_occurrences(self, blocks: list, config: DRYConfig) -> bool:
78
113
  """Check if blocks meet minimum occurrence threshold for the language.
@@ -169,3 +204,28 @@ class ViolationGenerator:
169
204
  return int(message[start:end])
170
205
  except (ValueError, IndexError):
171
206
  return 1
207
+
208
+ def _filter_shared_ignored(
209
+ self,
210
+ violations: list[Violation],
211
+ ignore_parser: "IgnoreDirectiveParser",
212
+ file_contents: dict[str, str],
213
+ ) -> list[Violation]:
214
+ """Filter violations using the shared ignore directive parser.
215
+
216
+ This enables standard # thailint: ignore-start/end directives for DRY linter.
217
+
218
+ Args:
219
+ violations: List of violations to filter
220
+ ignore_parser: Shared ignore directive parser
221
+ file_contents: Cached file contents for ignore checking
222
+
223
+ Returns:
224
+ Filtered list of violations
225
+ """
226
+ filtered = []
227
+ for violation in violations:
228
+ file_content = file_contents.get(violation.file_path, "")
229
+ if not ignore_parser.should_ignore_violation(violation, file_content):
230
+ filtered.append(violation)
231
+ return filtered
@@ -42,6 +42,10 @@ class LazyIgnoresConfig: # pylint: disable=too-many-instance-attributes
42
42
  # Orphaned detection
43
43
  check_orphaned: bool = True # Header entries without matching ignores
44
44
 
45
+ # Inline justification options
46
+ allow_inline_justifications: bool = True # Allow " - reason" syntax
47
+ min_justification_length: int = 10 # Minimum chars for valid justification
48
+
45
49
  # File patterns to ignore
46
50
  ignore_patterns: list[str] = field(
47
51
  default_factory=lambda: [
@@ -64,5 +68,7 @@ class LazyIgnoresConfig: # pylint: disable=too-many-instance-attributes
64
68
  check_thailint_ignore=config_dict.get("check_thailint_ignore", True),
65
69
  check_test_skips=config_dict.get("check_test_skips", True),
66
70
  check_orphaned=config_dict.get("check_orphaned", True),
71
+ allow_inline_justifications=config_dict.get("allow_inline_justifications", True),
72
+ min_justification_length=config_dict.get("min_justification_length", 10),
67
73
  ignore_patterns=config_dict.get("ignore_patterns", []),
68
74
  )
@@ -5,15 +5,21 @@ Scope: Common directive creation and path normalization for ignore detectors
5
5
 
6
6
  Overview: Provides shared utility functions used across Python, TypeScript, and test skip
7
7
  detectors. Centralizes logic for normalizing file paths, extracting rule IDs from
8
- regex matches, and creating IgnoreDirective objects to avoid code duplication.
8
+ regex matches, extracting inline justifications, and creating IgnoreDirective objects
9
+ to avoid code duplication.
9
10
 
10
11
  Dependencies: re for match handling, pathlib for file paths, types module for dataclasses
11
12
 
12
- Exports: normalize_path, extract_rule_ids, create_directive, create_directive_no_rules
13
+ Exports: normalize_path, extract_rule_ids, create_directive, create_directive_no_rules,
14
+ extract_inline_justification
13
15
 
14
16
  Interfaces: Pure utility functions with no state
15
17
 
16
18
  Implementation: Simple helper functions for directive creation
19
+
20
+ Suppressions:
21
+ too-many-arguments: create_directive needs all params for proper IgnoreDirective construction
22
+ too-many-positional-arguments: Factory function mirrors IgnoreDirective fields
17
23
  """
18
24
 
19
25
  import re
@@ -21,6 +27,9 @@ from pathlib import Path
21
27
 
22
28
  from src.linters.lazy_ignores.types import IgnoreDirective, IgnoreType
23
29
 
30
+ # Pattern for inline justification: space-dash-space followed by text
31
+ INLINE_JUSTIFICATION_PATTERN = re.compile(r"\s+-\s+(.+)$")
32
+
24
33
 
25
34
  def normalize_path(file_path: Path | str | None) -> Path:
26
35
  """Normalize file path to Path object.
@@ -38,6 +47,29 @@ def normalize_path(file_path: Path | str | None) -> Path:
38
47
  return file_path
39
48
 
40
49
 
50
+ def extract_inline_justification(raw_text: str) -> str | None:
51
+ """Extract inline justification from raw directive text.
52
+
53
+ Looks for the pattern " - " (space-dash-space) followed by justification text.
54
+ This allows inline justifications like:
55
+ # noqa: PLR0912 - state machine inherently complex
56
+ # type: ignore[arg-type] - library has typing bug
57
+
58
+ Args:
59
+ raw_text: The raw comment text containing the ignore directive
60
+
61
+ Returns:
62
+ The justification text if found, None otherwise.
63
+ Returns None for empty/whitespace-only justifications.
64
+ """
65
+ match = INLINE_JUSTIFICATION_PATTERN.search(raw_text)
66
+ if not match:
67
+ return None
68
+
69
+ justification = match.group(1).strip()
70
+ return justification if justification else None
71
+
72
+
41
73
  def _get_captured_group(match: re.Match[str]) -> str | None:
42
74
  """Get the first captured group from a regex match if it exists.
43
75
 
@@ -69,12 +101,13 @@ def extract_rule_ids(match: re.Match[str]) -> list[str]:
69
101
  return [rule_id for rule_id in ids if rule_id]
70
102
 
71
103
 
72
- def create_directive(
104
+ def create_directive( # pylint: disable=too-many-arguments,too-many-positional-arguments
73
105
  match: re.Match[str],
74
106
  ignore_type: IgnoreType,
75
107
  line_num: int,
76
108
  file_path: Path,
77
109
  rule_ids: tuple[str, ...] | None = None,
110
+ full_line: str | None = None,
78
111
  ) -> IgnoreDirective:
79
112
  """Create an IgnoreDirective from a regex match.
80
113
 
@@ -84,6 +117,7 @@ def create_directive(
84
117
  line_num: 1-indexed line number
85
118
  file_path: Path to source file
86
119
  rule_ids: Optional tuple of rule IDs; if None, extracts from match group 1
120
+ full_line: Optional full line text for extracting inline justification
87
121
 
88
122
  Returns:
89
123
  IgnoreDirective for this match
@@ -91,13 +125,22 @@ def create_directive(
91
125
  if rule_ids is None:
92
126
  rule_ids = tuple(extract_rule_ids(match))
93
127
 
128
+ # Use full line from match position to capture inline justification
129
+ if full_line is not None:
130
+ raw_text = full_line[match.start() :].strip()
131
+ else:
132
+ raw_text = match.group(0).strip()
133
+
134
+ inline_justification = extract_inline_justification(raw_text)
135
+
94
136
  return IgnoreDirective(
95
137
  ignore_type=ignore_type,
96
138
  rule_ids=rule_ids,
97
139
  line=line_num,
98
140
  column=match.start() + 1,
99
- raw_text=match.group(0).strip(),
141
+ raw_text=raw_text,
100
142
  file_path=file_path,
143
+ inline_justification=inline_justification,
101
144
  )
102
145
 
103
146
 
@@ -32,13 +32,15 @@ from .types import IgnoreDirective, IgnoreType
32
32
  class IgnoreSuppressionMatcher:
33
33
  """Matches ignore directives with header suppressions."""
34
34
 
35
- def __init__(self, parser: SuppressionsParser) -> None:
35
+ def __init__(self, parser: SuppressionsParser, min_justification_length: int = 10) -> None:
36
36
  """Initialize the matcher.
37
37
 
38
38
  Args:
39
39
  parser: SuppressionsParser for rule ID normalization.
40
+ min_justification_length: Minimum length for valid inline justification.
40
41
  """
41
42
  self._parser = parser
43
+ self._min_justification_length = min_justification_length
42
44
 
43
45
  def collect_used_rule_ids(self, ignores: list[IgnoreDirective]) -> set[str]:
44
46
  """Collect all normalized rule IDs used in ignore directives.
@@ -72,6 +74,9 @@ class IgnoreSuppressionMatcher:
72
74
  ) -> list[str]:
73
75
  """Find which rule IDs in an ignore are not justified.
74
76
 
77
+ Checks inline justification first (higher precedence), then falls back
78
+ to header-based suppressions.
79
+
75
80
  Args:
76
81
  ignore: The ignore directive to check.
77
82
  suppressions: Dict of normalized rule IDs to justifications.
@@ -79,17 +84,45 @@ class IgnoreSuppressionMatcher:
79
84
  Returns:
80
85
  List of unjustified rule IDs (original case preserved).
81
86
  """
87
+ if self._has_valid_inline_justification(ignore):
88
+ return []
89
+
82
90
  if not ignore.rule_ids:
83
- type_key = self._normalize(ignore.ignore_type.value)
84
- if type_key not in suppressions:
85
- return [ignore.ignore_type.value]
91
+ return self._check_bare_ignore(ignore, suppressions)
92
+
93
+ return self._check_rule_specific_ignore(ignore, suppressions)
94
+
95
+ def _check_bare_ignore(
96
+ self, ignore: IgnoreDirective, suppressions: dict[str, str]
97
+ ) -> list[str]:
98
+ """Check if a bare ignore (no specific rules) is justified."""
99
+ type_key = self._normalize(ignore.ignore_type.value)
100
+ if type_key in suppressions:
86
101
  return []
102
+ return [ignore.ignore_type.value]
87
103
 
88
- unjustified: list[str] = []
89
- for rule_id in ignore.rule_ids:
90
- if not self._is_rule_justified(ignore, rule_id, suppressions):
91
- unjustified.append(rule_id)
92
- return unjustified
104
+ def _check_rule_specific_ignore(
105
+ self, ignore: IgnoreDirective, suppressions: dict[str, str]
106
+ ) -> list[str]:
107
+ """Check which specific rule IDs are not justified."""
108
+ return [
109
+ rule_id
110
+ for rule_id in ignore.rule_ids
111
+ if not self._is_rule_justified(ignore, rule_id, suppressions)
112
+ ]
113
+
114
+ def _has_valid_inline_justification(self, ignore: IgnoreDirective) -> bool:
115
+ """Check if the ignore has a valid inline justification.
116
+
117
+ Args:
118
+ ignore: The ignore directive to check.
119
+
120
+ Returns:
121
+ True if the ignore has an inline justification meeting minimum length.
122
+ """
123
+ if not ignore.inline_justification:
124
+ return False
125
+ return len(ignore.inline_justification) >= self._min_justification_length
93
126
 
94
127
  def _is_rule_justified(
95
128
  self, ignore: IgnoreDirective, rule_id: str, suppressions: dict[str, str]
@@ -205,5 +205,5 @@ class PythonIgnoreDetector:
205
205
  continue
206
206
  if _is_pattern_in_string_literal(line, match.start()):
207
207
  continue
208
- found.append(create_directive(match, ignore_type, line_num, file_path))
208
+ found.append(create_directive(match, ignore_type, line_num, file_path, full_line=line))
209
209
  return found
@@ -59,6 +59,7 @@ class IgnoreDirective:
59
59
  column: int
60
60
  raw_text: str # Original comment text
61
61
  file_path: Path
62
+ inline_justification: str | None = None # Justification after " - " delimiter
62
63
 
63
64
 
64
65
  @dataclass(frozen=True)
@@ -72,8 +72,12 @@ def _build_unjustified_suggestion(rule_id: str) -> str:
72
72
 
73
73
  suppression_entries = "\n".join(f" {rid}: [Your justification here]" for rid in rule_ids)
74
74
 
75
- return f"""To fix, add an entry to the file header Suppressions section:
75
+ return f"""To fix, either:
76
76
 
77
+ 1. Add an inline justification (10+ chars) after the ignore directive:
78
+ # noqa: {rule_ids[0]} - [Your justification here]
79
+
80
+ 2. Or add an entry to the file header Suppressions section:
77
81
  Suppressions:
78
82
  {suppression_entries}
79
83