thailint 0.12.0__py3-none-any.whl → 0.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. src/analyzers/__init__.py +4 -3
  2. src/analyzers/ast_utils.py +54 -0
  3. src/analyzers/typescript_base.py +4 -0
  4. src/cli/__init__.py +3 -0
  5. src/cli/config.py +12 -12
  6. src/cli/config_merge.py +241 -0
  7. src/cli/linters/__init__.py +9 -0
  8. src/cli/linters/code_patterns.py +107 -257
  9. src/cli/linters/code_smells.py +48 -165
  10. src/cli/linters/documentation.py +21 -95
  11. src/cli/linters/performance.py +274 -0
  12. src/cli/linters/shared.py +232 -6
  13. src/cli/linters/structure.py +26 -21
  14. src/cli/linters/structure_quality.py +28 -21
  15. src/cli_main.py +3 -0
  16. src/config.py +2 -1
  17. src/core/base.py +3 -2
  18. src/core/cli_utils.py +3 -1
  19. src/core/config_parser.py +5 -2
  20. src/core/constants.py +54 -0
  21. src/core/linter_utils.py +95 -6
  22. src/core/rule_discovery.py +5 -1
  23. src/core/violation_builder.py +3 -0
  24. src/linter_config/directive_markers.py +109 -0
  25. src/linter_config/ignore.py +225 -383
  26. src/linter_config/pattern_utils.py +65 -0
  27. src/linter_config/rule_matcher.py +89 -0
  28. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  29. src/linters/collection_pipeline/ast_utils.py +40 -0
  30. src/linters/collection_pipeline/config.py +12 -0
  31. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  32. src/linters/collection_pipeline/detector.py +262 -32
  33. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  34. src/linters/collection_pipeline/linter.py +18 -35
  35. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  36. src/linters/dry/base_token_analyzer.py +16 -9
  37. src/linters/dry/block_filter.py +7 -4
  38. src/linters/dry/cache.py +7 -2
  39. src/linters/dry/config.py +7 -1
  40. src/linters/dry/constant_matcher.py +34 -25
  41. src/linters/dry/file_analyzer.py +4 -2
  42. src/linters/dry/inline_ignore.py +7 -16
  43. src/linters/dry/linter.py +48 -25
  44. src/linters/dry/python_analyzer.py +18 -10
  45. src/linters/dry/python_constant_extractor.py +51 -52
  46. src/linters/dry/single_statement_detector.py +14 -12
  47. src/linters/dry/token_hasher.py +115 -115
  48. src/linters/dry/typescript_analyzer.py +11 -6
  49. src/linters/dry/typescript_constant_extractor.py +4 -0
  50. src/linters/dry/typescript_statement_detector.py +208 -208
  51. src/linters/dry/typescript_value_extractor.py +3 -0
  52. src/linters/dry/violation_filter.py +1 -4
  53. src/linters/dry/violation_generator.py +1 -4
  54. src/linters/file_header/atemporal_detector.py +58 -40
  55. src/linters/file_header/base_parser.py +4 -0
  56. src/linters/file_header/bash_parser.py +4 -0
  57. src/linters/file_header/config.py +14 -0
  58. src/linters/file_header/field_validator.py +5 -8
  59. src/linters/file_header/linter.py +19 -12
  60. src/linters/file_header/markdown_parser.py +6 -0
  61. src/linters/file_placement/config_loader.py +3 -1
  62. src/linters/file_placement/linter.py +22 -8
  63. src/linters/file_placement/pattern_matcher.py +21 -4
  64. src/linters/file_placement/pattern_validator.py +21 -7
  65. src/linters/file_placement/rule_checker.py +2 -2
  66. src/linters/lazy_ignores/__init__.py +43 -0
  67. src/linters/lazy_ignores/config.py +66 -0
  68. src/linters/lazy_ignores/directive_utils.py +121 -0
  69. src/linters/lazy_ignores/header_parser.py +177 -0
  70. src/linters/lazy_ignores/linter.py +158 -0
  71. src/linters/lazy_ignores/matcher.py +135 -0
  72. src/linters/lazy_ignores/python_analyzer.py +205 -0
  73. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  74. src/linters/lazy_ignores/skip_detector.py +298 -0
  75. src/linters/lazy_ignores/types.py +69 -0
  76. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  77. src/linters/lazy_ignores/violation_builder.py +131 -0
  78. src/linters/lbyl/__init__.py +29 -0
  79. src/linters/lbyl/config.py +63 -0
  80. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  81. src/linters/lbyl/pattern_detectors/base.py +46 -0
  82. src/linters/magic_numbers/context_analyzer.py +227 -229
  83. src/linters/magic_numbers/linter.py +20 -15
  84. src/linters/magic_numbers/python_analyzer.py +4 -16
  85. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  86. src/linters/method_property/config.py +4 -1
  87. src/linters/method_property/linter.py +5 -10
  88. src/linters/method_property/python_analyzer.py +5 -4
  89. src/linters/method_property/violation_builder.py +3 -0
  90. src/linters/nesting/linter.py +11 -6
  91. src/linters/nesting/typescript_analyzer.py +6 -12
  92. src/linters/nesting/typescript_function_extractor.py +0 -4
  93. src/linters/nesting/violation_builder.py +1 -0
  94. src/linters/performance/__init__.py +91 -0
  95. src/linters/performance/config.py +43 -0
  96. src/linters/performance/constants.py +49 -0
  97. src/linters/performance/linter.py +149 -0
  98. src/linters/performance/python_analyzer.py +365 -0
  99. src/linters/performance/regex_analyzer.py +312 -0
  100. src/linters/performance/regex_linter.py +139 -0
  101. src/linters/performance/typescript_analyzer.py +236 -0
  102. src/linters/performance/violation_builder.py +160 -0
  103. src/linters/print_statements/linter.py +6 -4
  104. src/linters/print_statements/python_analyzer.py +85 -81
  105. src/linters/print_statements/typescript_analyzer.py +6 -15
  106. src/linters/srp/heuristics.py +4 -4
  107. src/linters/srp/linter.py +12 -12
  108. src/linters/srp/violation_builder.py +0 -4
  109. src/linters/stateless_class/linter.py +30 -36
  110. src/linters/stateless_class/python_analyzer.py +11 -20
  111. src/linters/stringly_typed/config.py +4 -5
  112. src/linters/stringly_typed/context_filter.py +410 -410
  113. src/linters/stringly_typed/function_call_violation_builder.py +93 -95
  114. src/linters/stringly_typed/linter.py +48 -16
  115. src/linters/stringly_typed/python/analyzer.py +5 -1
  116. src/linters/stringly_typed/python/call_tracker.py +8 -5
  117. src/linters/stringly_typed/python/comparison_tracker.py +10 -5
  118. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  119. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  120. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  121. src/linters/stringly_typed/python/validation_detector.py +3 -0
  122. src/linters/stringly_typed/storage.py +14 -14
  123. src/linters/stringly_typed/typescript/call_tracker.py +9 -3
  124. src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
  125. src/linters/stringly_typed/violation_generator.py +288 -259
  126. src/orchestrator/core.py +13 -4
  127. src/templates/thailint_config_template.yaml +196 -0
  128. src/utils/project_root.py +3 -0
  129. thailint-0.14.0.dist-info/METADATA +185 -0
  130. thailint-0.14.0.dist-info/RECORD +199 -0
  131. thailint-0.12.0.dist-info/METADATA +0 -1667
  132. thailint-0.12.0.dist-info/RECORD +0 -164
  133. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
  134. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
  135. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -19,8 +19,10 @@ Implementation: Click decorators for command definition, orchestrator-based lint
19
19
 
20
20
  SRP Exception: CLI command modules follow Click framework patterns requiring similar command
21
21
  structure across all linter commands. This is intentional design for consistency.
22
+
23
+ Suppressions:
24
+ - too-many-arguments,too-many-positional-arguments: Click commands require many parameters by framework design
22
25
  """
23
- # dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
24
26
 
25
27
  import json
26
28
  import logging
@@ -30,13 +32,16 @@ from typing import TYPE_CHECKING, Any, NoReturn
30
32
 
31
33
  import click
32
34
 
33
- from src.cli.linters.shared import ensure_config_section, set_config_value
35
+ from src.cli.linters.shared import (
36
+ ensure_config_section,
37
+ extract_command_context,
38
+ set_config_value,
39
+ )
34
40
  from src.cli.main import cli
35
41
  from src.cli.utils import (
36
42
  execute_linting_on_paths,
37
43
  format_option,
38
44
  get_or_detect_project_root,
39
- get_project_root_from_context,
40
45
  handle_linting_error,
41
46
  load_config_file,
42
47
  setup_base_orchestrator,
@@ -152,20 +157,20 @@ def file_placement( # pylint: disable=too-many-arguments,too-many-positional-ar
152
157
  # Inline JSON rules
153
158
  thai-lint file-placement --rules '{"allow": [".*\\.py$"]}' .
154
159
  """
155
- verbose: bool = ctx.obj.get("verbose", False)
156
- project_root = get_project_root_from_context(ctx)
157
-
158
- if not paths:
159
- paths = (".",)
160
-
161
- path_objs = [Path(p) for p in paths]
160
+ cmd_ctx = extract_command_context(ctx, paths)
162
161
 
163
162
  try:
164
163
  _execute_file_placement_lint(
165
- path_objs, config_file, rules, format, recursive, verbose, project_root
164
+ cmd_ctx.path_objs,
165
+ config_file,
166
+ rules,
167
+ format,
168
+ recursive,
169
+ cmd_ctx.verbose,
170
+ cmd_ctx.project_root,
166
171
  )
167
172
  except Exception as e:
168
- handle_linting_error(e, verbose)
173
+ handle_linting_error(e, cmd_ctx.verbose)
169
174
 
170
175
 
171
176
  def _execute_file_placement_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
@@ -275,20 +280,20 @@ def pipeline( # pylint: disable=too-many-arguments,too-many-positional-argument
275
280
  # Use custom config file
276
281
  thai-lint pipeline --config .thailint.yaml src/
277
282
  """
278
- verbose: bool = ctx.obj.get("verbose", False)
279
- project_root = get_project_root_from_context(ctx)
280
-
281
- if not paths:
282
- paths = (".",)
283
-
284
- path_objs = [Path(p) for p in paths]
283
+ cmd_ctx = extract_command_context(ctx, paths)
285
284
 
286
285
  try:
287
286
  _execute_pipeline_lint(
288
- path_objs, config_file, format, min_continues, recursive, verbose, project_root
287
+ cmd_ctx.path_objs,
288
+ config_file,
289
+ format,
290
+ min_continues,
291
+ recursive,
292
+ cmd_ctx.verbose,
293
+ cmd_ctx.project_root,
289
294
  )
290
295
  except Exception as e:
291
- handle_linting_error(e, verbose)
296
+ handle_linting_error(e, cmd_ctx.verbose)
292
297
 
293
298
 
294
299
  def _execute_pipeline_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
@@ -19,8 +19,10 @@ Implementation: Click decorators for command definition, orchestrator-based lint
19
19
 
20
20
  SRP Exception: CLI command modules follow Click framework patterns requiring similar command
21
21
  structure across all linter commands. This is intentional design for consistency.
22
+
23
+ Suppressions:
24
+ - too-many-arguments,too-many-positional-arguments: Click commands require many parameters by framework design
22
25
  """
23
- # dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
24
26
 
25
27
  import logging
26
28
  import sys
@@ -29,12 +31,15 @@ from typing import TYPE_CHECKING, NoReturn
29
31
 
30
32
  import click
31
33
 
32
- from src.cli.linters.shared import ensure_config_section, set_config_value
34
+ from src.cli.linters.shared import (
35
+ ensure_config_section,
36
+ extract_command_context,
37
+ set_config_value,
38
+ )
33
39
  from src.cli.main import cli
34
40
  from src.cli.utils import (
35
41
  execute_linting_on_paths,
36
42
  format_option,
37
- get_project_root_from_context,
38
43
  handle_linting_error,
39
44
  parallel_option,
40
45
  setup_base_orchestrator,
@@ -150,20 +155,21 @@ def nesting( # pylint: disable=too-many-arguments,too-many-positional-arguments
150
155
  # Use custom config file
151
156
  thai-lint nesting --config .thailint.yaml src/
152
157
  """
153
- verbose: bool = ctx.obj.get("verbose", False)
154
- project_root = get_project_root_from_context(ctx)
155
-
156
- if not paths:
157
- paths = (".",)
158
-
159
- path_objs = [Path(p) for p in paths]
158
+ cmd_ctx = extract_command_context(ctx, paths)
160
159
 
161
160
  try:
162
161
  _execute_nesting_lint(
163
- path_objs, config_file, format, max_depth, recursive, parallel, verbose, project_root
162
+ cmd_ctx.path_objs,
163
+ config_file,
164
+ format,
165
+ max_depth,
166
+ recursive,
167
+ parallel,
168
+ cmd_ctx.verbose,
169
+ cmd_ctx.project_root,
164
170
  )
165
171
  except Exception as e:
166
- handle_linting_error(e, verbose)
172
+ handle_linting_error(e, cmd_ctx.verbose)
167
173
 
168
174
 
169
175
  def _execute_nesting_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
@@ -277,20 +283,21 @@ def srp( # pylint: disable=too-many-arguments,too-many-positional-arguments
277
283
  # Use custom config file
278
284
  thai-lint srp --config .thailint.yaml src/
279
285
  """
280
- verbose: bool = ctx.obj.get("verbose", False)
281
- project_root = get_project_root_from_context(ctx)
282
-
283
- if not paths:
284
- paths = (".",)
285
-
286
- path_objs = [Path(p) for p in paths]
286
+ cmd_ctx = extract_command_context(ctx, paths)
287
287
 
288
288
  try:
289
289
  _execute_srp_lint(
290
- path_objs, config_file, format, max_methods, max_loc, recursive, verbose, project_root
290
+ cmd_ctx.path_objs,
291
+ config_file,
292
+ format,
293
+ max_methods,
294
+ max_loc,
295
+ recursive,
296
+ cmd_ctx.verbose,
297
+ cmd_ctx.project_root,
291
298
  )
292
299
  except Exception as e:
293
- handle_linting_error(e, verbose)
300
+ handle_linting_error(e, cmd_ctx.verbose)
294
301
 
295
302
 
296
303
  def _execute_srp_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
src/cli_main.py CHANGED
@@ -17,6 +17,9 @@ Exports: cli (main command group with all commands registered)
17
17
  Interfaces: Click CLI commands, integration with Orchestrator for linting execution
18
18
 
19
19
  Implementation: Module imports trigger command registration via Click decorator side effects
20
+
21
+ Suppressions:
22
+ - F401: Module re-exports and imports trigger Click command registration via decorator side effects
20
23
  """
21
24
 
22
25
  # Import the main CLI group from the modular package
src/config.py CHANGED
@@ -26,6 +26,7 @@ from typing import Any
26
26
  import yaml
27
27
 
28
28
  from src.core.config_parser import ConfigParseError, parse_config_file
29
+ from src.core.constants import CONFIG_EXTENSIONS
29
30
 
30
31
  logger = logging.getLogger(__name__)
31
32
 
@@ -170,7 +171,7 @@ def _validate_before_save(config: dict[str, Any]) -> None:
170
171
 
171
172
  def _write_config_file(config: dict[str, Any], path: Path) -> None:
172
173
  """Write config to file based on extension."""
173
- if path.suffix in [".yaml", ".yml"]:
174
+ if path.suffix in CONFIG_EXTENSIONS:
174
175
  _write_yaml_config(config, path)
175
176
  elif path.suffix == ".json":
176
177
  _write_json_config(config, path)
src/core/base.py CHANGED
@@ -31,6 +31,7 @@ from abc import ABC, abstractmethod
31
31
  from pathlib import Path
32
32
  from typing import Any
33
33
 
34
+ from .constants import Language
34
35
  from .types import Violation
35
36
 
36
37
 
@@ -176,10 +177,10 @@ class MultiLanguageLintRule(BaseLintRule):
176
177
  if not config.enabled:
177
178
  return []
178
179
 
179
- if context.language == "python":
180
+ if context.language == Language.PYTHON:
180
181
  return self._check_python(context, config)
181
182
 
182
- if context.language in ("typescript", "javascript"):
183
+ if context.language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
183
184
  return self._check_typescript(context, config)
184
185
 
185
186
  return []
src/core/cli_utils.py CHANGED
@@ -26,6 +26,8 @@ from typing import Any
26
26
 
27
27
  import click
28
28
 
29
+ from src.core.constants import CONFIG_EXTENSIONS
30
+
29
31
 
30
32
  def common_linter_options(func: Callable) -> Callable:
31
33
  """Add common linter CLI options to command.
@@ -103,7 +105,7 @@ def _load_config_by_format(config_file: Path) -> dict[str, Any]:
103
105
  Returns:
104
106
  Loaded configuration dictionary
105
107
  """
106
- if config_file.suffix in {".yaml", ".yml"}:
108
+ if config_file.suffix in CONFIG_EXTENSIONS:
107
109
  return _load_yaml_config(config_file)
108
110
  if config_file.suffix == ".json":
109
111
  return _load_json_config(config_file)
src/core/config_parser.py CHANGED
@@ -27,6 +27,8 @@ from typing import Any, TextIO
27
27
 
28
28
  import yaml
29
29
 
30
+ from src.core.constants import CONFIG_EXTENSIONS
31
+
30
32
 
31
33
  class ConfigParseError(Exception):
32
34
  """Configuration file parsing errors."""
@@ -113,11 +115,12 @@ def parse_config_file(path: Path, encoding: str = "utf-8") -> dict[str, Any]:
113
115
  """
114
116
  suffix = path.suffix.lower()
115
117
 
116
- if suffix not in [".yaml", ".yml", ".json"]:
118
+ valid_suffixes = (*CONFIG_EXTENSIONS, ".json")
119
+ if suffix not in valid_suffixes:
117
120
  raise ConfigParseError(f"Unsupported config format: {suffix}")
118
121
 
119
122
  with path.open(encoding=encoding) as f:
120
- if suffix in [".yaml", ".yml"]:
123
+ if suffix in CONFIG_EXTENSIONS:
121
124
  config = parse_yaml(f, path)
122
125
  else:
123
126
  config = parse_json(f, path)
src/core/constants.py ADDED
@@ -0,0 +1,54 @@
1
+ """
2
+ Purpose: Core constants and enums used across the thai-lint codebase
3
+
4
+ Scope: Centralized definitions for language names, storage modes, config extensions
5
+
6
+ Overview: Provides type-safe enums and constants for consistent stringly-typed patterns
7
+ across the codebase. Includes Language enum for programming language detection,
8
+ StorageMode for cache storage options, and CONFIG_EXTENSIONS for config file
9
+ discovery. Using enums ensures compile-time safety and IDE autocompletion.
10
+
11
+ Dependencies: enum module
12
+
13
+ Exports: Language enum, StorageMode enum, CONFIG_EXTENSIONS, IgnoreDirective enum,
14
+ HEADER_SCAN_LINES, MAX_ATTRIBUTE_CHAIN_DEPTH
15
+
16
+ Interfaces: Use enum values instead of string literals throughout codebase
17
+
18
+ Implementation: Standard Python enums with string values for compatibility
19
+ """
20
+
21
+ from enum import Enum
22
+
23
+
24
+ class Language(str, Enum):
25
+ """Supported programming languages for linting."""
26
+
27
+ PYTHON = "python"
28
+ TYPESCRIPT = "typescript"
29
+ JAVASCRIPT = "javascript"
30
+ MARKDOWN = "markdown"
31
+
32
+
33
+ class StorageMode(str, Enum):
34
+ """Storage modes for DRY linter cache."""
35
+
36
+ MEMORY = "memory"
37
+ TEMPFILE = "tempfile"
38
+
39
+
40
+ class IgnoreDirective(str, Enum):
41
+ """Inline ignore directive types."""
42
+
43
+ IGNORE = "ignore"
44
+ IGNORE_FILE = "ignore-file"
45
+
46
+
47
+ # Valid config file extensions
48
+ CONFIG_EXTENSIONS: tuple[str, str] = (".yaml", ".yml")
49
+
50
+ # Number of lines to scan at file start for ignore directives and headers
51
+ HEADER_SCAN_LINES: int = 10
52
+
53
+ # Maximum depth for attribute chain traversal (e.g., obj.attr.attr2.attr3)
54
+ MAX_ATTRIBUTE_CHAIN_DEPTH: int = 3
src/core/linter_utils.py CHANGED
@@ -1,26 +1,48 @@
1
1
  """
2
2
  Purpose: Shared utility functions for linter framework patterns
3
3
 
4
- Scope: Common config loading, metadata access, and context validation utilities for all linters
4
+ Scope: Common config loading, metadata access, context validation, and AST parsing utilities
5
5
 
6
6
  Overview: Provides reusable helper functions to eliminate duplication across linter implementations.
7
7
  Includes utilities for loading configuration from context metadata with language-specific overrides,
8
- extracting metadata fields safely with type validation, and validating context state. Standardizes
9
- common patterns used by srp, nesting, dry, and file_placement linters. Reduces boilerplate code
10
- while maintaining type safety and proper error handling.
8
+ extracting metadata fields safely with type validation, validating context state, and parsing
9
+ Python AST with syntax error handling. Standardizes common patterns used by srp, nesting, dry,
10
+ performance, and file_placement linters. Reduces boilerplate code while maintaining type safety
11
+ and proper error handling.
11
12
 
12
- Dependencies: BaseLintContext from src.core.base
13
+ Dependencies: BaseLintContext from src.core.base, ast for Python parsing
13
14
 
14
- Exports: get_metadata, get_metadata_value, load_linter_config, has_file_content
15
+ Exports: get_metadata, get_metadata_value, load_linter_config, has_file_content, parse_python_ast,
16
+ with_parsed_python
15
17
 
16
18
  Interfaces: All functions take BaseLintContext and return typed values (dict, str, bool, Any)
17
19
 
18
20
  Implementation: Type-safe metadata access with fallbacks, generic config loading with language support
21
+
22
+ Suppressions:
23
+ - invalid-name: T type variable follows Python generic naming convention
24
+ - type:ignore[return-value]: Generic config factory with runtime type checking
25
+ - unnecessary-ellipsis: Protocol method bodies use ellipsis per PEP 544
26
+ - B101: Assert used to narrow type after parse_python_ast returns non-None tree
19
27
  """
20
28
 
29
+ import ast
30
+ from collections.abc import Callable
21
31
  from typing import Any, Protocol, TypeVar
22
32
 
23
33
  from src.core.base import BaseLintContext
34
+ from src.core.types import Violation
35
+
36
+
37
+ # Protocol for violation builders that support syntax error handling
38
+ class SyntaxErrorViolationBuilder(Protocol):
39
+ """Protocol for violation builders that can create syntax error violations."""
40
+
41
+ def create_syntax_error_violation(
42
+ self, error: SyntaxError, context: BaseLintContext
43
+ ) -> Violation:
44
+ """Create a violation for a syntax error."""
45
+ ... # pylint: disable=unnecessary-ellipsis
24
46
 
25
47
 
26
48
  # Protocol for config classes that support from_dict
@@ -166,3 +188,70 @@ def should_process_file(context: BaseLintContext) -> bool:
166
188
  True if file has both content and path available
167
189
  """
168
190
  return has_file_content(context) and has_file_path(context)
191
+
192
+
193
+ def parse_python_ast(
194
+ context: BaseLintContext,
195
+ violation_builder: SyntaxErrorViolationBuilder,
196
+ ) -> tuple[ast.Module | None, list[Violation]]:
197
+ """Parse Python AST from context, handling syntax errors gracefully.
198
+
199
+ Provides a standard pattern for Python linters to parse AST and handle
200
+ syntax errors by returning a violation instead of crashing.
201
+
202
+ Args:
203
+ context: Lint context containing file content
204
+ violation_builder: Builder to create syntax error violations
205
+
206
+ Returns:
207
+ Tuple of (ast_tree, violations):
208
+ - On success: (ast.Module, [])
209
+ - On syntax error: (None, [syntax_error_violation])
210
+
211
+ Example:
212
+ tree, errors = parse_python_ast(context, self._violation_builder)
213
+ if errors:
214
+ return errors
215
+ # ... use tree for analysis
216
+ """
217
+ try:
218
+ tree = ast.parse(context.file_content or "")
219
+ return tree, []
220
+ except SyntaxError as e:
221
+ violation = violation_builder.create_syntax_error_violation(e, context)
222
+ return None, [violation]
223
+
224
+
225
+ def with_parsed_python(
226
+ context: BaseLintContext,
227
+ violation_builder: SyntaxErrorViolationBuilder,
228
+ on_success: Callable[[ast.Module], list[Violation]],
229
+ ) -> list[Violation]:
230
+ """Parse Python and call on_success with the AST, or return parse errors.
231
+
232
+ Eliminates the repeated parse-check-assert pattern across Python linters.
233
+ On parse success, calls on_success with a guaranteed non-None AST tree.
234
+ On parse failure, returns syntax error violations.
235
+
236
+ Args:
237
+ context: Lint context containing file content
238
+ violation_builder: Builder to create syntax error violations
239
+ on_success: Callback receiving the parsed AST tree, returns violations
240
+
241
+ Returns:
242
+ Violations from on_success callback, or syntax error violations
243
+
244
+ Example:
245
+ def _check_python(self, context, config):
246
+ return with_parsed_python(
247
+ context,
248
+ self._violation_builder,
249
+ lambda tree: self._analyze_tree(tree, config, context),
250
+ )
251
+ """
252
+ tree, errors = parse_python_ast(context, violation_builder)
253
+ if errors:
254
+ return errors
255
+ # tree is guaranteed non-None when errors is empty (parse_python_ast contract)
256
+ assert tree is not None # nosec B101
257
+ return on_success(tree)
@@ -19,12 +19,15 @@ Implementation: Package traversal with pkgutil, class introspection with inspect
19
19
 
20
20
  import importlib
21
21
  import inspect
22
+ import logging
22
23
  import pkgutil
23
24
  from types import ModuleType
24
25
  from typing import Any
25
26
 
26
27
  from .base import BaseLintRule
27
28
 
29
+ logger = logging.getLogger(__name__)
30
+
28
31
 
29
32
  def discover_from_package(package_path: str) -> list[BaseLintRule]:
30
33
  """Discover rules from a package and its modules.
@@ -37,7 +40,8 @@ def discover_from_package(package_path: str) -> list[BaseLintRule]:
37
40
  """
38
41
  try:
39
42
  package = importlib.import_module(package_path)
40
- except ImportError:
43
+ except ImportError as e:
44
+ logger.debug("Failed to import package %s: %s", package_path, e)
41
45
  return []
42
46
 
43
47
  if not hasattr(package, "__path__"):
@@ -21,6 +21,9 @@ Interfaces: ViolationInfo(rule_id, file_path, line, message, column, severity),
21
21
 
22
22
  Implementation: Uses dataclass for type-safe violation info, functions provide build logic
23
23
  that constructs Violation objects with proper defaults
24
+
25
+ Suppressions:
26
+ - too-many-arguments,too-many-positional-arguments: Violation fields as parameters
24
27
  """
25
28
 
26
29
  from dataclasses import dataclass
@@ -0,0 +1,109 @@
1
+ """
2
+ Purpose: Ignore directive marker detection for thailint comments
3
+
4
+ Scope: Detection of thailint and design-lint ignore markers in source code
5
+
6
+ Overview: Provides functions for detecting various ignore directive markers in code
7
+ comments. Supports file-level ignores, line-level ignores, block ignores, and
8
+ next-line ignores. Works with both Python (#) and JavaScript (//) comment styles.
9
+ All checks are case-insensitive.
10
+
11
+ Dependencies: None (pure string operations)
12
+
13
+ Exports: Marker detection functions for various ignore directive types
14
+
15
+ Interfaces: has_*_marker(line) -> bool for each marker type
16
+
17
+ Implementation: String-based pattern detection with case-insensitive matching
18
+ """
19
+
20
+
21
+ def has_ignore_directive_marker(line: str) -> bool:
22
+ """Check if line contains a file-level ignore directive marker.
23
+
24
+ Args:
25
+ line: Line of code to check
26
+
27
+ Returns:
28
+ True if line has ignore-file marker
29
+ """
30
+ line_lower = line.lower()
31
+ return "# thailint: ignore-file" in line_lower or "# design-lint: ignore-file" in line_lower
32
+
33
+
34
+ def has_line_ignore_marker(code: str) -> bool:
35
+ """Check if code line has an inline ignore marker.
36
+
37
+ Args:
38
+ code: Line of code to check
39
+
40
+ Returns:
41
+ True if line has inline ignore marker
42
+ """
43
+ code_lower = code.lower()
44
+ return (
45
+ "# thailint: ignore" in code_lower
46
+ or "# design-lint: ignore" in code_lower
47
+ or "// thailint: ignore" in code_lower
48
+ or "// design-lint: ignore" in code_lower
49
+ )
50
+
51
+
52
+ def has_ignore_next_line_marker(line: str) -> bool:
53
+ """Check if line has ignore-next-line marker.
54
+
55
+ Args:
56
+ line: Line of code to check
57
+
58
+ Returns:
59
+ True if line has ignore-next-line marker
60
+ """
61
+ return "# thailint: ignore-next-line" in line or "# design-lint: ignore-next-line" in line
62
+
63
+
64
+ def has_ignore_start_marker(line: str) -> bool:
65
+ """Check if line has ignore-start comment marker.
66
+
67
+ Only matches actual comment lines (starting with # or //), not strings
68
+ containing the marker text.
69
+
70
+ Args:
71
+ line: Line of code to check
72
+
73
+ Returns:
74
+ True if line is a proper ignore-start comment
75
+ """
76
+ stripped = line.strip().lower()
77
+ if not (stripped.startswith("#") or stripped.startswith("//")):
78
+ return False
79
+ return "ignore-start" in stripped and ("thailint:" in stripped or "design-lint:" in stripped)
80
+
81
+
82
+ def has_ignore_end_marker(line: str) -> bool:
83
+ """Check if line has ignore-end comment marker.
84
+
85
+ Only matches actual comment lines (starting with # or //), not strings
86
+ containing the marker text.
87
+
88
+ Args:
89
+ line: Line of code to check
90
+
91
+ Returns:
92
+ True if line is a proper ignore-end comment
93
+ """
94
+ stripped = line.strip().lower()
95
+ if not (stripped.startswith("#") or stripped.startswith("//")):
96
+ return False
97
+ return "ignore-end" in stripped and ("thailint:" in stripped or "design-lint:" in stripped)
98
+
99
+
100
+ def check_general_ignore(line: str) -> bool:
101
+ """Check if line has general ignore directive (no specific rules).
102
+
103
+ Args:
104
+ line: Line containing ignore directive
105
+
106
+ Returns:
107
+ True if no specific rules are specified (not bracket syntax)
108
+ """
109
+ return "ignore-file[" not in line