thailint 0.11.0__py3-none-any.whl → 0.13.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 (129) 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 +3 -0
  8. src/cli/linters/code_patterns.py +113 -5
  9. src/cli/linters/code_smells.py +118 -7
  10. src/cli/linters/documentation.py +3 -0
  11. src/cli/linters/structure.py +3 -0
  12. src/cli/linters/structure_quality.py +3 -0
  13. src/cli/utils.py +29 -9
  14. src/cli_main.py +3 -0
  15. src/config.py +2 -1
  16. src/core/base.py +3 -2
  17. src/core/cli_utils.py +3 -1
  18. src/core/config_parser.py +5 -2
  19. src/core/constants.py +54 -0
  20. src/core/linter_utils.py +4 -0
  21. src/core/rule_discovery.py +5 -1
  22. src/core/violation_builder.py +3 -0
  23. src/linter_config/directive_markers.py +109 -0
  24. src/linter_config/ignore.py +225 -383
  25. src/linter_config/pattern_utils.py +65 -0
  26. src/linter_config/rule_matcher.py +89 -0
  27. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  28. src/linters/collection_pipeline/ast_utils.py +40 -0
  29. src/linters/collection_pipeline/config.py +12 -0
  30. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  31. src/linters/collection_pipeline/detector.py +262 -32
  32. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  33. src/linters/collection_pipeline/linter.py +18 -35
  34. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  35. src/linters/dry/base_token_analyzer.py +16 -9
  36. src/linters/dry/block_filter.py +7 -4
  37. src/linters/dry/cache.py +7 -2
  38. src/linters/dry/config.py +7 -1
  39. src/linters/dry/constant_matcher.py +34 -25
  40. src/linters/dry/file_analyzer.py +4 -2
  41. src/linters/dry/inline_ignore.py +7 -16
  42. src/linters/dry/linter.py +48 -25
  43. src/linters/dry/python_analyzer.py +18 -10
  44. src/linters/dry/python_constant_extractor.py +51 -52
  45. src/linters/dry/single_statement_detector.py +14 -12
  46. src/linters/dry/token_hasher.py +115 -115
  47. src/linters/dry/typescript_analyzer.py +11 -6
  48. src/linters/dry/typescript_constant_extractor.py +4 -0
  49. src/linters/dry/typescript_statement_detector.py +208 -208
  50. src/linters/dry/typescript_value_extractor.py +3 -0
  51. src/linters/dry/violation_filter.py +1 -4
  52. src/linters/dry/violation_generator.py +1 -4
  53. src/linters/file_header/atemporal_detector.py +4 -0
  54. src/linters/file_header/base_parser.py +4 -0
  55. src/linters/file_header/bash_parser.py +4 -0
  56. src/linters/file_header/field_validator.py +5 -8
  57. src/linters/file_header/linter.py +19 -12
  58. src/linters/file_header/markdown_parser.py +6 -0
  59. src/linters/file_placement/config_loader.py +3 -1
  60. src/linters/file_placement/linter.py +22 -8
  61. src/linters/file_placement/pattern_matcher.py +21 -4
  62. src/linters/file_placement/pattern_validator.py +21 -7
  63. src/linters/file_placement/rule_checker.py +2 -2
  64. src/linters/lazy_ignores/__init__.py +43 -0
  65. src/linters/lazy_ignores/config.py +66 -0
  66. src/linters/lazy_ignores/directive_utils.py +121 -0
  67. src/linters/lazy_ignores/header_parser.py +177 -0
  68. src/linters/lazy_ignores/linter.py +158 -0
  69. src/linters/lazy_ignores/matcher.py +135 -0
  70. src/linters/lazy_ignores/python_analyzer.py +201 -0
  71. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  72. src/linters/lazy_ignores/skip_detector.py +298 -0
  73. src/linters/lazy_ignores/types.py +67 -0
  74. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  75. src/linters/lazy_ignores/violation_builder.py +131 -0
  76. src/linters/lbyl/__init__.py +29 -0
  77. src/linters/lbyl/config.py +63 -0
  78. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  79. src/linters/lbyl/pattern_detectors/base.py +46 -0
  80. src/linters/magic_numbers/context_analyzer.py +227 -229
  81. src/linters/magic_numbers/linter.py +20 -15
  82. src/linters/magic_numbers/python_analyzer.py +4 -16
  83. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  84. src/linters/method_property/config.py +4 -0
  85. src/linters/method_property/linter.py +5 -4
  86. src/linters/method_property/python_analyzer.py +5 -4
  87. src/linters/method_property/violation_builder.py +3 -0
  88. src/linters/nesting/typescript_analyzer.py +6 -12
  89. src/linters/nesting/typescript_function_extractor.py +0 -4
  90. src/linters/print_statements/linter.py +6 -4
  91. src/linters/print_statements/python_analyzer.py +85 -81
  92. src/linters/print_statements/typescript_analyzer.py +6 -15
  93. src/linters/srp/heuristics.py +4 -4
  94. src/linters/srp/linter.py +12 -12
  95. src/linters/srp/violation_builder.py +0 -4
  96. src/linters/stateless_class/linter.py +30 -36
  97. src/linters/stateless_class/python_analyzer.py +11 -20
  98. src/linters/stringly_typed/__init__.py +22 -9
  99. src/linters/stringly_typed/config.py +32 -8
  100. src/linters/stringly_typed/context_filter.py +451 -0
  101. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  102. src/linters/stringly_typed/ignore_checker.py +102 -0
  103. src/linters/stringly_typed/ignore_utils.py +51 -0
  104. src/linters/stringly_typed/linter.py +376 -0
  105. src/linters/stringly_typed/python/__init__.py +9 -5
  106. src/linters/stringly_typed/python/analyzer.py +159 -9
  107. src/linters/stringly_typed/python/call_tracker.py +175 -0
  108. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  109. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  110. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  111. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  112. src/linters/stringly_typed/python/validation_detector.py +3 -0
  113. src/linters/stringly_typed/storage.py +630 -0
  114. src/linters/stringly_typed/storage_initializer.py +45 -0
  115. src/linters/stringly_typed/typescript/__init__.py +28 -0
  116. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  117. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  118. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  119. src/linters/stringly_typed/violation_generator.py +405 -0
  120. src/orchestrator/core.py +13 -4
  121. src/templates/thailint_config_template.yaml +166 -0
  122. src/utils/project_root.py +3 -0
  123. thailint-0.13.0.dist-info/METADATA +184 -0
  124. thailint-0.13.0.dist-info/RECORD +189 -0
  125. thailint-0.11.0.dist-info/METADATA +0 -1661
  126. thailint-0.11.0.dist-info/RECORD +0 -150
  127. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
  128. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
  129. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,18 +1,18 @@
1
1
  """
2
- Purpose: CLI commands for code smell linters (dry, magic-numbers)
2
+ Purpose: CLI commands for code smell linters (dry, magic-numbers, stringly-typed)
3
3
 
4
- Scope: Commands that detect code smells like duplicate code and magic numbers
4
+ Scope: Commands that detect code smells like duplicate code, magic numbers, and stringly-typed patterns
5
5
 
6
6
  Overview: Provides CLI commands for code smell detection: dry finds duplicate code blocks using
7
- token-based hashing with SQLite caching, and magic-numbers detects unnamed numeric literals that
8
- should be extracted as named constants. Each command supports standard options (config, format,
9
- recursive) plus linter-specific options (min-lines, no-cache, clear-cache) and integrates with
10
- the orchestrator for execution.
7
+ token-based hashing with SQLite caching, magic-numbers detects unnamed numeric literals that
8
+ should be extracted as named constants, and stringly-typed detects string patterns that should
9
+ use enums. Each command supports standard options (config, format, recursive) plus linter-specific
10
+ options and integrates with the orchestrator for execution.
11
11
 
12
12
  Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities,
13
13
  src.cli.linters.shared for linter-specific helpers, yaml for config loading
14
14
 
15
- Exports: dry command, magic_numbers command
15
+ Exports: dry command, magic_numbers command, stringly_typed command
16
16
 
17
17
  Interfaces: Click CLI commands registered to main CLI group
18
18
 
@@ -20,6 +20,10 @@ Implementation: Click decorators for command definition, orchestrator-based lint
20
20
 
21
21
  SRP Exception: CLI command modules follow Click framework patterns requiring similar command
22
22
  structure across all linter commands. This is intentional design for consistency.
23
+
24
+ Suppressions:
25
+ too-many-arguments: Click commands require many parameters by framework design
26
+ too-many-positional-arguments: Click positional params match CLI arg structure
23
27
  """
24
28
  # dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
25
29
 
@@ -341,3 +345,110 @@ def _execute_magic_numbers_lint( # pylint: disable=too-many-arguments,too-many-
341
345
 
342
346
  format_violations(magic_numbers_violations, format)
343
347
  sys.exit(1 if magic_numbers_violations else 0)
348
+
349
+
350
+ # =============================================================================
351
+ # Stringly-Typed Command
352
+ # =============================================================================
353
+
354
+
355
+ def _setup_stringly_typed_orchestrator(
356
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
357
+ ) -> "Orchestrator":
358
+ """Set up orchestrator for stringly-typed command."""
359
+ return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
360
+
361
+
362
+ def _run_stringly_typed_lint(
363
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
364
+ ) -> list[Violation]:
365
+ """Execute stringly-typed lint on files or directories."""
366
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
367
+ return [v for v in all_violations if "stringly-typed" in v.rule_id]
368
+
369
+
370
+ @cli.command("stringly-typed")
371
+ @click.argument("paths", nargs=-1, type=click.Path())
372
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
373
+ @format_option
374
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
375
+ @click.pass_context
376
+ def stringly_typed( # pylint: disable=too-many-arguments,too-many-positional-arguments
377
+ ctx: click.Context,
378
+ paths: tuple[str, ...],
379
+ config_file: str | None,
380
+ format: str,
381
+ recursive: bool,
382
+ ) -> None:
383
+ """Check for stringly-typed patterns in code.
384
+
385
+ Detects string patterns in Python and TypeScript/JavaScript code that should
386
+ use enums or typed alternatives. Finds membership validation, equality chains,
387
+ and function calls with limited string values across multiple files.
388
+
389
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
390
+
391
+ Examples:
392
+
393
+ \b
394
+ # Check current directory (all files recursively)
395
+ thai-lint stringly-typed
396
+
397
+ \b
398
+ # Check specific directory
399
+ thai-lint stringly-typed src/
400
+
401
+ \b
402
+ # Check single file
403
+ thai-lint stringly-typed src/handlers.py
404
+
405
+ \b
406
+ # Check multiple files
407
+ thai-lint stringly-typed src/handlers.py src/services.py
408
+
409
+ \b
410
+ # Get JSON output
411
+ thai-lint stringly-typed --format json .
412
+
413
+ \b
414
+ # Get SARIF output for IDE integration
415
+ thai-lint stringly-typed --format sarif .
416
+
417
+ \b
418
+ # Use custom config file
419
+ thai-lint stringly-typed --config .thailint.yaml src/
420
+ """
421
+ verbose: bool = ctx.obj.get("verbose", False)
422
+ project_root = get_project_root_from_context(ctx)
423
+
424
+ if not paths:
425
+ paths = (".",)
426
+
427
+ path_objs = [Path(p) for p in paths]
428
+
429
+ try:
430
+ _execute_stringly_typed_lint(
431
+ path_objs, config_file, format, recursive, verbose, project_root
432
+ )
433
+ except Exception as e:
434
+ handle_linting_error(e, verbose)
435
+
436
+
437
+ def _execute_stringly_typed_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
438
+ path_objs: list[Path],
439
+ config_file: str | None,
440
+ format: str,
441
+ recursive: bool,
442
+ verbose: bool,
443
+ project_root: Path | None = None,
444
+ ) -> NoReturn:
445
+ """Execute stringly-typed lint."""
446
+ validate_paths_exist(path_objs)
447
+ orchestrator = _setup_stringly_typed_orchestrator(path_objs, config_file, verbose, project_root)
448
+ stringly_violations = _run_stringly_typed_lint(orchestrator, path_objs, recursive)
449
+
450
+ if verbose:
451
+ logger.info(f"Found {len(stringly_violations)} stringly-typed violation(s)")
452
+
453
+ format_violations(stringly_violations, format)
454
+ sys.exit(1 if stringly_violations else 0)
@@ -19,6 +19,9 @@ 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
26
  # dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
24
27
 
@@ -19,6 +19,9 @@ 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
26
  # dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
24
27
 
@@ -19,6 +19,9 @@ 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
26
  # dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
24
27
 
src/cli/utils.py CHANGED
@@ -172,34 +172,54 @@ def _autodetect_project_root(
172
172
  return auto_root
173
173
 
174
174
 
175
- def get_project_root_from_context(ctx: click.Context) -> Path:
175
+ def get_project_root_from_context(ctx: click.Context) -> Path | None:
176
176
  """Get or determine project root from Click context.
177
177
 
178
178
  This function defers the actual determination until needed to avoid
179
179
  importing pyprojroot in test environments where it may not be available.
180
180
 
181
+ Returns None when no explicit root is specified (via --project-root or --config),
182
+ allowing the orchestrator to auto-detect from target paths instead of CWD.
183
+
181
184
  Args:
182
185
  ctx: Click context containing CLI options
183
186
 
184
187
  Returns:
185
- Path to determined project root
188
+ Path to determined project root, or None for auto-detection from target paths
186
189
  """
187
190
  # Check if already determined and cached
188
191
  if "project_root" in ctx.obj:
189
- cached_root: Path = ctx.obj["project_root"]
190
- return cached_root
192
+ cached: Path | None = ctx.obj["project_root"]
193
+ return cached
194
+
195
+ project_root = _determine_project_root_for_context(ctx)
196
+ ctx.obj["project_root"] = project_root
197
+ return project_root
198
+
199
+
200
+ def _determine_project_root_for_context(ctx: click.Context) -> Path | None:
201
+ """Determine project root from context options.
202
+
203
+ Args:
204
+ ctx: Click context containing CLI options
191
205
 
192
- # Determine project root using stored CLI options
206
+ Returns:
207
+ Path if explicit root or config specified, None for auto-detection
208
+ """
193
209
  explicit_root = ctx.obj.get("cli_project_root")
194
210
  config_path = ctx.obj.get("cli_config_path")
195
211
  verbose = ctx.obj.get("verbose", False)
196
212
 
197
- project_root = _determine_project_root(explicit_root, config_path, verbose)
213
+ if explicit_root:
214
+ return _resolve_explicit_project_root(explicit_root, verbose)
198
215
 
199
- # Cache for future use
200
- ctx.obj["project_root"] = project_root
216
+ if config_path:
217
+ return _infer_root_from_config(config_path, verbose)
201
218
 
202
- return project_root
219
+ # No explicit root - return None for auto-detection from target paths
220
+ if verbose:
221
+ logger.debug("No explicit project root, will auto-detect from target paths")
222
+ return None
203
223
 
204
224
 
205
225
  # =============================================================================
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
@@ -16,6 +16,10 @@ Exports: get_metadata, get_metadata_value, load_linter_config, has_file_content
16
16
  Interfaces: All functions take BaseLintContext and return typed values (dict, str, bool, Any)
17
17
 
18
18
  Implementation: Type-safe metadata access with fallbacks, generic config loading with language support
19
+
20
+ Suppressions:
21
+ - invalid-name: T type variable follows Python generic naming convention
22
+ - type:ignore[return-value]: Generic config factory with runtime type checking
19
23
  """
20
24
 
21
25
  from typing import Any, Protocol, TypeVar
@@ -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