thailint 0.13.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 (32) hide show
  1. src/cli/linters/__init__.py +6 -0
  2. src/cli/linters/code_patterns.py +75 -333
  3. src/cli/linters/code_smells.py +47 -168
  4. src/cli/linters/documentation.py +21 -98
  5. src/cli/linters/performance.py +274 -0
  6. src/cli/linters/shared.py +232 -6
  7. src/cli/linters/structure.py +23 -21
  8. src/cli/linters/structure_quality.py +25 -21
  9. src/core/linter_utils.py +91 -6
  10. src/linters/file_header/atemporal_detector.py +54 -40
  11. src/linters/file_header/config.py +14 -0
  12. src/linters/lazy_ignores/python_analyzer.py +5 -1
  13. src/linters/lazy_ignores/types.py +2 -0
  14. src/linters/method_property/config.py +0 -1
  15. src/linters/method_property/linter.py +0 -6
  16. src/linters/nesting/linter.py +11 -6
  17. src/linters/nesting/violation_builder.py +1 -0
  18. src/linters/performance/__init__.py +91 -0
  19. src/linters/performance/config.py +43 -0
  20. src/linters/performance/constants.py +49 -0
  21. src/linters/performance/linter.py +149 -0
  22. src/linters/performance/python_analyzer.py +365 -0
  23. src/linters/performance/regex_analyzer.py +312 -0
  24. src/linters/performance/regex_linter.py +139 -0
  25. src/linters/performance/typescript_analyzer.py +236 -0
  26. src/linters/performance/violation_builder.py +160 -0
  27. src/templates/thailint_config_template.yaml +30 -0
  28. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/METADATA +3 -2
  29. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/RECORD +32 -22
  30. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
  31. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
  32. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
src/cli/linters/shared.py CHANGED
@@ -4,27 +4,172 @@ Purpose: Shared utilities for linter CLI commands
4
4
  Scope: Common helper functions and patterns used across all linter command modules
5
5
 
6
6
  Overview: Provides reusable utilities for linter CLI commands including config section management,
7
- config value setting with logging, and rule ID filtering. Centralizes shared patterns to reduce
8
- duplication across linter command modules (code_quality, code_patterns, structure, documentation).
9
- All utilities are designed to work with the orchestrator configuration system.
7
+ config value setting with logging, rule ID filtering, CLI context extraction, and help text
8
+ generation. Centralizes shared patterns to reduce duplication across linter command modules
9
+ (code_quality, code_patterns, structure, documentation, performance). All utilities are designed
10
+ to work with the orchestrator configuration system and Click CLI framework.
10
11
 
11
- Dependencies: logging for debug output, pathlib for Path type hints
12
+ Dependencies: logging for debug output, pathlib for Path type hints, click for Context type,
13
+ dataclasses for CommandContext
12
14
 
13
- Exports: ensure_config_section, set_config_value, filter_violations_by_prefix
15
+ Exports: ensure_config_section, set_config_value, filter_violations_by_prefix, CommandContext,
16
+ extract_command_context, make_linter_help, ExecuteParams, prepare_standard_command,
17
+ run_linter_command, standard_linter_options, filter_violations_by_startswith,
18
+ create_linter_command
14
19
 
15
- Interfaces: Orchestrator config dict manipulation, violation list filtering
20
+ Interfaces: Orchestrator config dict manipulation, violation list filtering, CLI context extraction,
21
+ help text generation
16
22
 
17
23
  Implementation: Pure helper functions with no side effects beyond config mutation and logging
18
24
  """
19
25
 
20
26
  import logging
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
21
29
  from typing import TYPE_CHECKING, Any
22
30
 
31
+ import click
32
+
33
+ from src.cli.utils import format_option, get_project_root_from_context, handle_linting_error
23
34
  from src.core.types import Violation
24
35
 
25
36
  if TYPE_CHECKING:
37
+ from collections.abc import Callable
38
+
26
39
  from src.orchestrator.core import Orchestrator
27
40
 
41
+
42
+ def standard_linter_options(f: Any) -> Any:
43
+ """Apply standard linter CLI options to a command.
44
+
45
+ Bundles the common options used by most linter commands:
46
+ - paths argument (variadic)
47
+ - config file option
48
+ - format option
49
+ - recursive option
50
+ - pass_context
51
+
52
+ Usage:
53
+ @cli.command("my-linter")
54
+ @standard_linter_options
55
+ def my_linter(ctx, paths, config_file, format, recursive):
56
+ ...
57
+ """
58
+ f = click.pass_context(f)
59
+ f = click.option(
60
+ "--recursive/--no-recursive", default=True, help="Scan directories recursively"
61
+ )(f)
62
+ f = format_option(f)
63
+ f = click.option(
64
+ "--config", "-c", "config_file", type=click.Path(), help="Path to config file"
65
+ )(f)
66
+ f = click.argument("paths", nargs=-1, type=click.Path())(f)
67
+ return f
68
+
69
+
70
+ @dataclass
71
+ class CommandContext:
72
+ """Extracted context from CLI command invocation.
73
+
74
+ Consolidates common CLI command setup into a reusable structure.
75
+ """
76
+
77
+ verbose: bool
78
+ project_root: Path | None
79
+ path_objs: list[Path]
80
+
81
+
82
+ @dataclass
83
+ class ExecuteParams:
84
+ """Parameters for linter execution functions.
85
+
86
+ Bundles the common parameters passed to _execute_*_lint functions
87
+ to reduce function signature duplication across CLI modules.
88
+ """
89
+
90
+ path_objs: list[Path]
91
+ config_file: str | None
92
+ format: str
93
+ recursive: bool
94
+ verbose: bool
95
+ project_root: Path | None
96
+
97
+
98
+ def extract_command_context(ctx: click.Context, paths: tuple[str, ...]) -> CommandContext:
99
+ """Extract common context values from CLI command invocation.
100
+
101
+ Consolidates the repeated pattern of extracting verbose, project_root,
102
+ and converting paths to Path objects with default handling.
103
+
104
+ Args:
105
+ ctx: Click context from command invocation
106
+ paths: Tuple of path strings from command arguments
107
+
108
+ Returns:
109
+ CommandContext with extracted values
110
+ """
111
+ verbose: bool = ctx.obj.get("verbose", False)
112
+ project_root = get_project_root_from_context(ctx)
113
+
114
+ if not paths:
115
+ paths = (".",)
116
+
117
+ path_objs = [Path(p) for p in paths]
118
+
119
+ return CommandContext(verbose=verbose, project_root=project_root, path_objs=path_objs)
120
+
121
+
122
+ def prepare_standard_command(
123
+ ctx: click.Context,
124
+ paths: tuple[str, ...],
125
+ config_file: str | None,
126
+ format: str,
127
+ recursive: bool,
128
+ ) -> ExecuteParams:
129
+ """Prepare standard linter command execution parameters.
130
+
131
+ Combines context extraction and ExecuteParams creation into a single call.
132
+ Use with commands that have the standard options (config, format, recursive).
133
+
134
+ Args:
135
+ ctx: Click context from command invocation
136
+ paths: Tuple of path strings from command arguments
137
+ config_file: Optional config file path
138
+ format: Output format
139
+ recursive: Whether to scan recursively
140
+
141
+ Returns:
142
+ ExecuteParams ready for _execute_*_lint function
143
+ """
144
+ cmd_ctx = extract_command_context(ctx, paths)
145
+ return ExecuteParams(
146
+ path_objs=cmd_ctx.path_objs,
147
+ config_file=config_file,
148
+ format=format,
149
+ recursive=recursive,
150
+ verbose=cmd_ctx.verbose,
151
+ project_root=cmd_ctx.project_root,
152
+ )
153
+
154
+
155
+ def run_linter_command(
156
+ execute_fn: "Callable[[ExecuteParams], None]",
157
+ params: ExecuteParams,
158
+ ) -> None:
159
+ """Run a linter command with standard error handling.
160
+
161
+ Wraps the try/except pattern used by all linter commands.
162
+
163
+ Args:
164
+ execute_fn: The _execute_*_lint function to call
165
+ params: ExecuteParams for the execution
166
+ """
167
+ try:
168
+ execute_fn(params)
169
+ except Exception as e:
170
+ handle_linting_error(e, params.verbose)
171
+
172
+
28
173
  # Configure module logger
29
174
  logger = logging.getLogger(__name__)
30
175
 
@@ -87,3 +232,84 @@ def filter_violations_by_startswith(violations: list[Violation], prefix: str) ->
87
232
  Filtered list of violations where rule_id starts with the prefix
88
233
  """
89
234
  return [v for v in violations if v.rule_id.startswith(prefix)]
235
+
236
+
237
+ def create_linter_command(
238
+ name: str,
239
+ execute_fn: "Callable[[ExecuteParams], None]",
240
+ brief: str,
241
+ description: str,
242
+ ) -> "Callable[..., None]":
243
+ """Create a standard linter CLI command.
244
+
245
+ Factory function that generates Click commands with consistent structure,
246
+ eliminating boilerplate duplication across linter command modules.
247
+
248
+ Args:
249
+ name: CLI command name (e.g., "magic-numbers")
250
+ execute_fn: The _execute_*_lint function to call
251
+ brief: Brief one-line description
252
+ description: Detailed multi-line description
253
+
254
+ Returns:
255
+ Decorated Click command function
256
+ """
257
+ from src.cli.main import cli
258
+
259
+ @cli.command(name, help=make_linter_help(name, brief, description))
260
+ @standard_linter_options
261
+ def command(
262
+ ctx: click.Context,
263
+ paths: tuple[str, ...],
264
+ config_file: str | None,
265
+ format: str,
266
+ recursive: bool,
267
+ ) -> None:
268
+ params = prepare_standard_command(ctx, paths, config_file, format, recursive)
269
+ run_linter_command(execute_fn, params)
270
+
271
+ return command
272
+
273
+
274
+ def make_linter_help(command: str, brief: str, description: str) -> str:
275
+ """Generate standardized CLI help text for linter commands.
276
+
277
+ Creates consistent help text following the established pattern with
278
+ examples showing common usage patterns.
279
+
280
+ Args:
281
+ command: CLI command name (e.g., "magic-numbers")
282
+ brief: Brief one-line description
283
+ description: Detailed multi-line description
284
+
285
+ Returns:
286
+ Formatted help text string for Click command
287
+ """
288
+ return f"""{brief}
289
+
290
+ {description}
291
+
292
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
293
+
294
+ Examples:
295
+
296
+ \\b
297
+ # Check current directory (all files recursively)
298
+ thai-lint {command}
299
+
300
+ \\b
301
+ # Check specific directory
302
+ thai-lint {command} src/
303
+
304
+ \\b
305
+ # Check single file
306
+ thai-lint {command} src/app.py
307
+
308
+ \\b
309
+ # Get JSON output
310
+ thai-lint {command} --format json .
311
+
312
+ \\b
313
+ # Use custom config file
314
+ thai-lint {command} --config .thailint.yaml src/
315
+ """
@@ -23,7 +23,6 @@ SRP Exception: CLI command modules follow Click framework patterns requiring sim
23
23
  Suppressions:
24
24
  - too-many-arguments,too-many-positional-arguments: Click commands require many parameters by framework design
25
25
  """
26
- # dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
27
26
 
28
27
  import json
29
28
  import logging
@@ -33,13 +32,16 @@ from typing import TYPE_CHECKING, Any, NoReturn
33
32
 
34
33
  import click
35
34
 
36
- 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
+ )
37
40
  from src.cli.main import cli
38
41
  from src.cli.utils import (
39
42
  execute_linting_on_paths,
40
43
  format_option,
41
44
  get_or_detect_project_root,
42
- get_project_root_from_context,
43
45
  handle_linting_error,
44
46
  load_config_file,
45
47
  setup_base_orchestrator,
@@ -155,20 +157,20 @@ def file_placement( # pylint: disable=too-many-arguments,too-many-positional-ar
155
157
  # Inline JSON rules
156
158
  thai-lint file-placement --rules '{"allow": [".*\\.py$"]}' .
157
159
  """
158
- verbose: bool = ctx.obj.get("verbose", False)
159
- project_root = get_project_root_from_context(ctx)
160
-
161
- if not paths:
162
- paths = (".",)
163
-
164
- path_objs = [Path(p) for p in paths]
160
+ cmd_ctx = extract_command_context(ctx, paths)
165
161
 
166
162
  try:
167
163
  _execute_file_placement_lint(
168
- 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,
169
171
  )
170
172
  except Exception as e:
171
- handle_linting_error(e, verbose)
173
+ handle_linting_error(e, cmd_ctx.verbose)
172
174
 
173
175
 
174
176
  def _execute_file_placement_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
@@ -278,20 +280,20 @@ def pipeline( # pylint: disable=too-many-arguments,too-many-positional-argument
278
280
  # Use custom config file
279
281
  thai-lint pipeline --config .thailint.yaml src/
280
282
  """
281
- verbose: bool = ctx.obj.get("verbose", False)
282
- project_root = get_project_root_from_context(ctx)
283
-
284
- if not paths:
285
- paths = (".",)
286
-
287
- path_objs = [Path(p) for p in paths]
283
+ cmd_ctx = extract_command_context(ctx, paths)
288
284
 
289
285
  try:
290
286
  _execute_pipeline_lint(
291
- 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,
292
294
  )
293
295
  except Exception as e:
294
- handle_linting_error(e, verbose)
296
+ handle_linting_error(e, cmd_ctx.verbose)
295
297
 
296
298
 
297
299
  def _execute_pipeline_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
@@ -23,7 +23,6 @@ SRP Exception: CLI command modules follow Click framework patterns requiring sim
23
23
  Suppressions:
24
24
  - too-many-arguments,too-many-positional-arguments: Click commands require many parameters by framework design
25
25
  """
26
- # dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
27
26
 
28
27
  import logging
29
28
  import sys
@@ -32,12 +31,15 @@ from typing import TYPE_CHECKING, NoReturn
32
31
 
33
32
  import click
34
33
 
35
- 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
+ )
36
39
  from src.cli.main import cli
37
40
  from src.cli.utils import (
38
41
  execute_linting_on_paths,
39
42
  format_option,
40
- get_project_root_from_context,
41
43
  handle_linting_error,
42
44
  parallel_option,
43
45
  setup_base_orchestrator,
@@ -153,20 +155,21 @@ def nesting( # pylint: disable=too-many-arguments,too-many-positional-arguments
153
155
  # Use custom config file
154
156
  thai-lint nesting --config .thailint.yaml src/
155
157
  """
156
- verbose: bool = ctx.obj.get("verbose", False)
157
- project_root = get_project_root_from_context(ctx)
158
-
159
- if not paths:
160
- paths = (".",)
161
-
162
- path_objs = [Path(p) for p in paths]
158
+ cmd_ctx = extract_command_context(ctx, paths)
163
159
 
164
160
  try:
165
161
  _execute_nesting_lint(
166
- 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,
167
170
  )
168
171
  except Exception as e:
169
- handle_linting_error(e, verbose)
172
+ handle_linting_error(e, cmd_ctx.verbose)
170
173
 
171
174
 
172
175
  def _execute_nesting_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
@@ -280,20 +283,21 @@ def srp( # pylint: disable=too-many-arguments,too-many-positional-arguments
280
283
  # Use custom config file
281
284
  thai-lint srp --config .thailint.yaml src/
282
285
  """
283
- verbose: bool = ctx.obj.get("verbose", False)
284
- project_root = get_project_root_from_context(ctx)
285
-
286
- if not paths:
287
- paths = (".",)
288
-
289
- path_objs = [Path(p) for p in paths]
286
+ cmd_ctx = extract_command_context(ctx, paths)
290
287
 
291
288
  try:
292
289
  _execute_srp_lint(
293
- 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,
294
298
  )
295
299
  except Exception as e:
296
- handle_linting_error(e, verbose)
300
+ handle_linting_error(e, cmd_ctx.verbose)
297
301
 
298
302
 
299
303
  def _execute_srp_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
src/core/linter_utils.py CHANGED
@@ -1,17 +1,19 @@
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
 
@@ -20,11 +22,27 @@ Implementation: Type-safe metadata access with fallbacks, generic config loading
20
22
  Suppressions:
21
23
  - invalid-name: T type variable follows Python generic naming convention
22
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
23
27
  """
24
28
 
29
+ import ast
30
+ from collections.abc import Callable
25
31
  from typing import Any, Protocol, TypeVar
26
32
 
27
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
28
46
 
29
47
 
30
48
  # Protocol for config classes that support from_dict
@@ -170,3 +188,70 @@ def should_process_file(context: BaseLintContext) -> bool:
170
188
  True if file has both content and path available
171
189
  """
172
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)
@@ -15,7 +15,7 @@ Exports: AtemporalDetector class with detect_violations method
15
15
 
16
16
  Interfaces: detect_violations(text) -> list[tuple[str, str, int]] returns pattern matches with line numbers
17
17
 
18
- Implementation: Regex-based pattern matching with predefined patterns organized by category
18
+ Implementation: Regex-based pattern matching with pre-compiled patterns organized by category
19
19
 
20
20
  Suppressions:
21
21
  - nesting: detect_violations iterates over pattern categories and their patterns.
@@ -23,46 +23,60 @@ Suppressions:
23
23
  """
24
24
 
25
25
  import re
26
+ from re import Pattern
27
+
28
+
29
+ def _compile_patterns(patterns: list[tuple[str, str]]) -> list[tuple[Pattern[str], str]]:
30
+ """Compile regex patterns for efficient reuse."""
31
+ return [(re.compile(pattern, re.IGNORECASE), desc) for pattern, desc in patterns]
26
32
 
27
33
 
28
34
  class AtemporalDetector:
29
35
  """Detects temporal language patterns in text."""
30
36
 
31
- # Date patterns
32
- DATE_PATTERNS = [
33
- (r"\d{4}-\d{2}-\d{2}", "ISO date format (YYYY-MM-DD)"),
34
- (
35
- r"(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}",
36
- "Month Year format",
37
- ),
38
- (r"(?:Created|Updated|Modified):\s*\d{4}", "Date metadata"),
39
- ]
40
-
41
- # Temporal qualifiers
42
- TEMPORAL_QUALIFIERS = [
43
- (r"\bcurrently\b", 'temporal qualifier "currently"'),
44
- (r"\bnow\b", 'temporal qualifier "now"'),
45
- (r"\brecently\b", 'temporal qualifier "recently"'),
46
- (r"\bsoon\b", 'temporal qualifier "soon"'),
47
- (r"\bfor now\b", 'temporal qualifier "for now"'),
48
- ]
49
-
50
- # State change language
51
- STATE_CHANGE = [
52
- (r"\breplaces?\b", 'state change "replaces"'),
53
- (r"\bmigrated from\b", 'state change "migrated from"'),
54
- (r"\bformerly\b", 'state change "formerly"'),
55
- (r"\bold implementation\b", 'state change "old"'),
56
- (r"\bnew implementation\b", 'state change "new"'),
57
- ]
58
-
59
- # Future references
60
- FUTURE_REFS = [
61
- (r"\bwill be\b", 'future reference "will be"'),
62
- (r"\bplanned\b", 'future reference "planned"'),
63
- (r"\bto be added\b", 'future reference "to be added"'),
64
- (r"\bcoming soon\b", 'future reference "coming soon"'),
65
- ]
37
+ # Pre-compiled date patterns
38
+ DATE_PATTERNS = _compile_patterns(
39
+ [
40
+ (r"\d{4}-\d{2}-\d{2}", "ISO date format (YYYY-MM-DD)"),
41
+ (
42
+ r"(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}",
43
+ "Month Year format",
44
+ ),
45
+ (r"(?:Created|Updated|Modified):\s*\d{4}", "Date metadata"),
46
+ ]
47
+ )
48
+
49
+ # Pre-compiled temporal qualifiers
50
+ TEMPORAL_QUALIFIERS = _compile_patterns(
51
+ [
52
+ (r"\bcurrently\b", 'temporal qualifier "currently"'),
53
+ (r"\bnow\b", 'temporal qualifier "now"'),
54
+ (r"\brecently\b", 'temporal qualifier "recently"'),
55
+ (r"\bsoon\b", 'temporal qualifier "soon"'),
56
+ (r"\bfor now\b", 'temporal qualifier "for now"'),
57
+ ]
58
+ )
59
+
60
+ # Pre-compiled state change language
61
+ STATE_CHANGE = _compile_patterns(
62
+ [
63
+ (r"\breplaces?\b", 'state change "replaces"'),
64
+ (r"\bmigrated from\b", 'state change "migrated from"'),
65
+ (r"\bformerly\b", 'state change "formerly"'),
66
+ (r"\bold implementation\b", 'state change "old"'),
67
+ (r"\bnew implementation\b", 'state change "new"'),
68
+ ]
69
+ )
70
+
71
+ # Pre-compiled future references
72
+ FUTURE_REFS = _compile_patterns(
73
+ [
74
+ (r"\bwill be\b", 'future reference "will be"'),
75
+ (r"\bplanned\b", 'future reference "planned"'),
76
+ (r"\bto be added\b", 'future reference "to be added"'),
77
+ (r"\bcoming soon\b", 'future reference "coming soon"'),
78
+ ]
79
+ )
66
80
 
67
81
  def detect_violations( # thailint: ignore[nesting]
68
82
  self, text: str
@@ -77,15 +91,15 @@ class AtemporalDetector:
77
91
  """
78
92
  violations = []
79
93
 
80
- # Check all pattern categories
94
+ # Check all pattern categories (patterns are pre-compiled)
81
95
  all_patterns = (
82
96
  self.DATE_PATTERNS + self.TEMPORAL_QUALIFIERS + self.STATE_CHANGE + self.FUTURE_REFS
83
97
  )
84
98
 
85
99
  lines = text.split("\n")
86
100
  for line_num, line in enumerate(lines, start=1):
87
- for pattern, description in all_patterns:
88
- if re.search(pattern, line, re.IGNORECASE):
89
- violations.append((pattern, description, line_num))
101
+ for compiled_pattern, description in all_patterns:
102
+ if compiled_pattern.search(line):
103
+ violations.append((compiled_pattern.pattern, description, line_num))
90
104
 
91
105
  return violations