thailint 0.10.0__py3-none-any.whl → 0.12.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.
- src/__init__.py +1 -0
- src/cli/__init__.py +27 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +478 -0
- src/cli/linters/__init__.py +58 -0
- src/cli/linters/code_patterns.py +372 -0
- src/cli/linters/code_smells.py +450 -0
- src/cli/linters/documentation.py +155 -0
- src/cli/linters/shared.py +89 -0
- src/cli/linters/structure.py +313 -0
- src/cli/linters/structure_quality.py +316 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +34 -0
- src/core/types.py +13 -0
- src/core/violation_utils.py +69 -0
- src/linter_config/ignore.py +32 -16
- src/linters/collection_pipeline/linter.py +2 -2
- src/linters/dry/block_filter.py +97 -1
- src/linters/dry/cache.py +94 -6
- src/linters/dry/config.py +47 -10
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +214 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/linter.py +89 -48
- src/linters/dry/python_analyzer.py +12 -415
- src/linters/dry/python_constant_extractor.py +101 -0
- src/linters/dry/single_statement_detector.py +415 -0
- src/linters/dry/token_hasher.py +5 -5
- src/linters/dry/typescript_analyzer.py +5 -354
- src/linters/dry/typescript_constant_extractor.py +134 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +66 -0
- src/linters/file_header/linter.py +2 -2
- src/linters/file_placement/linter.py +2 -2
- src/linters/file_placement/pattern_matcher.py +19 -5
- src/linters/magic_numbers/linter.py +8 -67
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/nesting/linter.py +12 -9
- src/linters/print_statements/linter.py +7 -24
- src/linters/srp/class_analyzer.py +9 -9
- src/linters/srp/heuristics.py +2 -2
- src/linters/srp/linter.py +2 -2
- src/linters/stateless_class/linter.py +2 -2
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +190 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +137 -0
- src/linters/stringly_typed/ignore_checker.py +102 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +344 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +344 -0
- src/linters/stringly_typed/python/call_tracker.py +172 -0
- src/linters/stringly_typed/python/comparison_tracker.py +252 -0
- src/linters/stringly_typed/python/condition_extractor.py +131 -0
- src/linters/stringly_typed/python/conditional_detector.py +176 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +88 -0
- src/linters/stringly_typed/python/validation_detector.py +186 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +630 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +329 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +372 -0
- src/linters/stringly_typed/violation_generator.py +376 -0
- src/orchestrator/core.py +241 -12
- {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/METADATA +9 -3
- {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/RECORD +74 -28
- thailint-0.12.0.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -2141
- thailint-0.10.0.dist-info/entry_points.txt +0 -4
- {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/WHEEL +0 -0
- {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -29,7 +29,8 @@ from pathlib import Path
|
|
|
29
29
|
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
30
30
|
from src.core.linter_utils import load_linter_config
|
|
31
31
|
from src.core.types import Violation
|
|
32
|
-
from src.
|
|
32
|
+
from src.core.violation_utils import get_violation_line, has_python_noqa, has_typescript_noqa
|
|
33
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
33
34
|
|
|
34
35
|
from .config import PrintStatementConfig
|
|
35
36
|
from .python_analyzer import PythonPrintStatementAnalyzer
|
|
@@ -42,7 +43,7 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
42
43
|
|
|
43
44
|
def __init__(self) -> None:
|
|
44
45
|
"""Initialize the print statements rule."""
|
|
45
|
-
self._ignore_parser =
|
|
46
|
+
self._ignore_parser = get_ignore_parser()
|
|
46
47
|
self._violation_builder = ViolationBuilder(self.rule_id)
|
|
47
48
|
|
|
48
49
|
@property
|
|
@@ -255,27 +256,16 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
255
256
|
Returns:
|
|
256
257
|
True if line has generic ignore directive
|
|
257
258
|
"""
|
|
258
|
-
line_text =
|
|
259
|
+
line_text = get_violation_line(violation, context)
|
|
259
260
|
if line_text is None:
|
|
260
261
|
return False
|
|
261
262
|
return self._has_generic_ignore_directive(line_text)
|
|
262
263
|
|
|
263
|
-
def _get_violation_line(self, violation: Violation, context: BaseLintContext) -> str | None:
|
|
264
|
-
"""Get the line text for a violation."""
|
|
265
|
-
if not context.file_content:
|
|
266
|
-
return None
|
|
267
|
-
|
|
268
|
-
lines = context.file_content.splitlines()
|
|
269
|
-
if violation.line <= 0 or violation.line > len(lines):
|
|
270
|
-
return None
|
|
271
|
-
|
|
272
|
-
return lines[violation.line - 1].lower()
|
|
273
|
-
|
|
274
264
|
def _has_generic_ignore_directive(self, line_text: str) -> bool:
|
|
275
265
|
"""Check if line has generic ignore directive."""
|
|
276
266
|
if self._has_generic_thailint_ignore(line_text):
|
|
277
267
|
return True
|
|
278
|
-
return
|
|
268
|
+
return has_python_noqa(line_text)
|
|
279
269
|
|
|
280
270
|
def _has_generic_thailint_ignore(self, line_text: str) -> bool:
|
|
281
271
|
"""Check for generic thailint: ignore (no brackets)."""
|
|
@@ -284,10 +274,6 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
284
274
|
after_ignore = line_text.split("# thailint: ignore")[1].split("#")[0]
|
|
285
275
|
return "[" not in after_ignore
|
|
286
276
|
|
|
287
|
-
def _has_noqa_directive(self, line_text: str) -> bool:
|
|
288
|
-
"""Check for noqa-style comments."""
|
|
289
|
-
return "# noqa" in line_text
|
|
290
|
-
|
|
291
277
|
def _check_typescript(
|
|
292
278
|
self, context: BaseLintContext, config: PrintStatementConfig
|
|
293
279
|
) -> list[Violation]:
|
|
@@ -400,7 +386,7 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
400
386
|
Returns:
|
|
401
387
|
True if line has ignore directive
|
|
402
388
|
"""
|
|
403
|
-
line_text =
|
|
389
|
+
line_text = get_violation_line(violation, context)
|
|
404
390
|
if line_text is None:
|
|
405
391
|
return False
|
|
406
392
|
return self._has_typescript_ignore_directive(line_text)
|
|
@@ -422,7 +408,4 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
422
408
|
if "[" not in after_ignore:
|
|
423
409
|
return True
|
|
424
410
|
|
|
425
|
-
|
|
426
|
-
return True
|
|
427
|
-
|
|
428
|
-
return False
|
|
411
|
+
return has_typescript_noqa(line_text)
|
|
@@ -32,8 +32,10 @@ class ClassAnalyzer:
|
|
|
32
32
|
"""Coordinates class analysis for Python and TypeScript."""
|
|
33
33
|
|
|
34
34
|
def __init__(self) -> None:
|
|
35
|
-
"""Initialize the class analyzer."""
|
|
36
|
-
|
|
35
|
+
"""Initialize the class analyzer with singleton analyzers."""
|
|
36
|
+
# Singleton analyzers for performance (avoid recreating per-file)
|
|
37
|
+
self._python_analyzer = PythonSRPAnalyzer()
|
|
38
|
+
self._typescript_analyzer = TypeScriptSRPAnalyzer()
|
|
37
39
|
|
|
38
40
|
def analyze_python(
|
|
39
41
|
self, context: BaseLintContext, config: SRPConfig
|
|
@@ -51,10 +53,9 @@ class ClassAnalyzer:
|
|
|
51
53
|
if isinstance(tree, list): # Syntax error violations
|
|
52
54
|
return tree
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
classes = analyzer.find_all_classes(tree)
|
|
56
|
+
classes = self._python_analyzer.find_all_classes(tree)
|
|
56
57
|
return [
|
|
57
|
-
|
|
58
|
+
self._python_analyzer.analyze_class(class_node, context.file_content or "", config)
|
|
58
59
|
for class_node in classes
|
|
59
60
|
]
|
|
60
61
|
|
|
@@ -70,14 +71,13 @@ class ClassAnalyzer:
|
|
|
70
71
|
Returns:
|
|
71
72
|
List of class metrics dicts
|
|
72
73
|
"""
|
|
73
|
-
|
|
74
|
-
root_node = analyzer.parse_typescript(context.file_content or "")
|
|
74
|
+
root_node = self._typescript_analyzer.parse_typescript(context.file_content or "")
|
|
75
75
|
if not root_node:
|
|
76
76
|
return []
|
|
77
77
|
|
|
78
|
-
classes =
|
|
78
|
+
classes = self._typescript_analyzer.find_all_classes(root_node)
|
|
79
79
|
return [
|
|
80
|
-
|
|
80
|
+
self._typescript_analyzer.analyze_class(class_node, context.file_content or "", config)
|
|
81
81
|
for class_node in classes
|
|
82
82
|
]
|
|
83
83
|
|
src/linters/srp/heuristics.py
CHANGED
|
@@ -57,8 +57,8 @@ def count_loc(class_node: ast.ClassDef, source: str) -> int:
|
|
|
57
57
|
end_line = class_node.end_lineno or start_line
|
|
58
58
|
lines = source.split("\n")[start_line - 1 : end_line]
|
|
59
59
|
|
|
60
|
-
# Filter out blank lines and comments
|
|
61
|
-
code_lines = [
|
|
60
|
+
# Filter out blank lines and comments (using walrus operator to avoid double strip)
|
|
61
|
+
code_lines = [s for line in lines if (s := line.strip()) and not s.startswith("#")]
|
|
62
62
|
return len(code_lines)
|
|
63
63
|
|
|
64
64
|
|
src/linters/srp/linter.py
CHANGED
|
@@ -21,7 +21,7 @@ Implementation: Composition pattern with helper classes, heuristic-based SRP ana
|
|
|
21
21
|
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
22
22
|
from src.core.linter_utils import load_linter_config
|
|
23
23
|
from src.core.types import Violation
|
|
24
|
-
from src.linter_config.ignore import
|
|
24
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
25
25
|
|
|
26
26
|
from .class_analyzer import ClassAnalyzer
|
|
27
27
|
from .config import SRPConfig
|
|
@@ -34,7 +34,7 @@ class SRPRule(MultiLanguageLintRule):
|
|
|
34
34
|
|
|
35
35
|
def __init__(self) -> None:
|
|
36
36
|
"""Initialize the SRP rule."""
|
|
37
|
-
self._ignore_parser =
|
|
37
|
+
self._ignore_parser = get_ignore_parser()
|
|
38
38
|
self._class_analyzer = ClassAnalyzer()
|
|
39
39
|
self._violation_builder = ViolationBuilder()
|
|
40
40
|
|
|
@@ -26,7 +26,7 @@ from pathlib import Path
|
|
|
26
26
|
|
|
27
27
|
from src.core.base import BaseLintContext, BaseLintRule
|
|
28
28
|
from src.core.types import Severity, Violation
|
|
29
|
-
from src.linter_config.ignore import
|
|
29
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
30
30
|
|
|
31
31
|
from .config import StatelessClassConfig
|
|
32
32
|
from .python_analyzer import ClassInfo, StatelessClassAnalyzer
|
|
@@ -37,7 +37,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
37
37
|
|
|
38
38
|
def __init__(self) -> None:
|
|
39
39
|
"""Initialize the rule with analyzer and ignore parser."""
|
|
40
|
-
self._ignore_parser =
|
|
40
|
+
self._ignore_parser = get_ignore_parser()
|
|
41
41
|
|
|
42
42
|
@property
|
|
43
43
|
def rule_id(self) -> str:
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Stringly-typed linter package exports
|
|
3
|
+
|
|
4
|
+
Scope: Public API for stringly-typed linter module
|
|
5
|
+
|
|
6
|
+
Overview: Provides the public interface for the stringly-typed linter package. Exports
|
|
7
|
+
StringlyTypedConfig for configuration and StringlyTypedRule for linting. The stringly-typed
|
|
8
|
+
linter detects code patterns where plain strings are used instead of proper enums or typed
|
|
9
|
+
alternatives, helping identify potential type safety improvements. Supports cross-file
|
|
10
|
+
detection to find repeated string patterns across the codebase. Includes IgnoreChecker
|
|
11
|
+
for inline ignore directive support.
|
|
12
|
+
|
|
13
|
+
Dependencies: .config for StringlyTypedConfig, .linter for StringlyTypedRule,
|
|
14
|
+
.storage for StringlyTypedStorage, .ignore_checker for IgnoreChecker
|
|
15
|
+
|
|
16
|
+
Exports: StringlyTypedConfig, StringlyTypedRule, StringlyTypedStorage, StoredPattern,
|
|
17
|
+
IgnoreChecker
|
|
18
|
+
|
|
19
|
+
Interfaces: Configuration loading via StringlyTypedConfig.from_dict(),
|
|
20
|
+
StringlyTypedRule.check() and finalize() for linting, IgnoreChecker.filter_violations()
|
|
21
|
+
|
|
22
|
+
Implementation: Module-level exports with __all__ definition
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from src.linters.stringly_typed.config import StringlyTypedConfig
|
|
26
|
+
from src.linters.stringly_typed.ignore_checker import IgnoreChecker
|
|
27
|
+
from src.linters.stringly_typed.linter import StringlyTypedRule
|
|
28
|
+
from src.linters.stringly_typed.storage import StoredPattern, StringlyTypedStorage
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"StringlyTypedConfig",
|
|
32
|
+
"IgnoreChecker",
|
|
33
|
+
"StringlyTypedRule",
|
|
34
|
+
"StringlyTypedStorage",
|
|
35
|
+
"StoredPattern",
|
|
36
|
+
]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration dataclass for stringly-typed linter
|
|
3
|
+
|
|
4
|
+
Scope: Define configurable options for stringly-typed pattern detection
|
|
5
|
+
|
|
6
|
+
Overview: Provides StringlyTypedConfig for customizing linter behavior including minimum
|
|
7
|
+
occurrences required to flag patterns, enum value thresholds, cross-file detection
|
|
8
|
+
settings, and ignore patterns. The stringly-typed linter detects code patterns where
|
|
9
|
+
plain strings are used instead of proper enums or typed alternatives. Integrates with
|
|
10
|
+
the orchestrator's configuration system to allow users to customize detection via
|
|
11
|
+
.thailint.yaml configuration files. Follows the same configuration pattern as other
|
|
12
|
+
thai-lint linters.
|
|
13
|
+
|
|
14
|
+
Dependencies: dataclasses, typing
|
|
15
|
+
|
|
16
|
+
Exports: StringlyTypedConfig dataclass, default constants
|
|
17
|
+
|
|
18
|
+
Interfaces: StringlyTypedConfig.from_dict() class method for configuration loading
|
|
19
|
+
|
|
20
|
+
Implementation: Dataclass with sensible defaults, validation in __post_init__, and config
|
|
21
|
+
loading from dictionary with language-specific override support. Pylint
|
|
22
|
+
too-many-instance-attributes suppressed because configuration dataclasses inherently
|
|
23
|
+
require multiple cohesive fields (8 attributes for detection thresholds, filtering,
|
|
24
|
+
cross-file settings). Splitting would reduce cohesion without benefit. This follows
|
|
25
|
+
the established pattern in DRYConfig which has the same suppression.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
# Default thresholds
|
|
32
|
+
DEFAULT_MIN_OCCURRENCES = 2
|
|
33
|
+
DEFAULT_MIN_VALUES_FOR_ENUM = 2
|
|
34
|
+
DEFAULT_MAX_VALUES_FOR_ENUM = 6
|
|
35
|
+
|
|
36
|
+
# Default ignore patterns - test directories are excluded by default
|
|
37
|
+
# because test fixtures commonly use string literals for mocking
|
|
38
|
+
DEFAULT_IGNORE_PATTERNS: list[str] = [
|
|
39
|
+
"**/tests/**",
|
|
40
|
+
"**/test/**",
|
|
41
|
+
"**/*_test.py",
|
|
42
|
+
"**/*_test.ts",
|
|
43
|
+
"**/*.test.ts",
|
|
44
|
+
"**/*.test.tsx",
|
|
45
|
+
"**/*.spec.ts",
|
|
46
|
+
"**/*.spec.tsx",
|
|
47
|
+
"**/*.stories.ts",
|
|
48
|
+
"**/*.stories.tsx",
|
|
49
|
+
"**/conftest.py",
|
|
50
|
+
"**/fixtures/**",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class StringlyTypedConfig: # pylint: disable=too-many-instance-attributes
|
|
56
|
+
"""Configuration for stringly-typed linter.
|
|
57
|
+
|
|
58
|
+
Note: Pylint too-many-instance-attributes disabled. This is a configuration
|
|
59
|
+
dataclass serving as a data container for related stringly-typed linter settings.
|
|
60
|
+
All 8 attributes are cohesively related (detection thresholds, filtering options,
|
|
61
|
+
cross-file settings, exclusion patterns). Splitting would reduce cohesion and make
|
|
62
|
+
configuration loading more complex without meaningful benefit. This follows the
|
|
63
|
+
established pattern in DRYConfig.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
enabled: bool = True
|
|
67
|
+
"""Whether the linter is enabled."""
|
|
68
|
+
|
|
69
|
+
min_occurrences: int = DEFAULT_MIN_OCCURRENCES
|
|
70
|
+
"""Minimum number of cross-file occurrences required to flag a violation."""
|
|
71
|
+
|
|
72
|
+
min_values_for_enum: int = DEFAULT_MIN_VALUES_FOR_ENUM
|
|
73
|
+
"""Minimum number of unique string values to suggest an enum."""
|
|
74
|
+
|
|
75
|
+
max_values_for_enum: int = DEFAULT_MAX_VALUES_FOR_ENUM
|
|
76
|
+
"""Maximum number of unique string values to suggest an enum (above this, not enum-worthy)."""
|
|
77
|
+
|
|
78
|
+
require_cross_file: bool = True
|
|
79
|
+
"""Whether to require cross-file occurrences to flag violations."""
|
|
80
|
+
|
|
81
|
+
ignore: list[str] = field(default_factory=list)
|
|
82
|
+
"""File patterns to ignore. Defaults merged with test directories in from_dict."""
|
|
83
|
+
|
|
84
|
+
allowed_string_sets: list[list[str]] = field(default_factory=list)
|
|
85
|
+
"""String sets that are allowed and should not be flagged."""
|
|
86
|
+
|
|
87
|
+
exclude_variables: list[str] = field(default_factory=list)
|
|
88
|
+
"""Variable names to exclude from detection."""
|
|
89
|
+
|
|
90
|
+
def __post_init__(self) -> None:
|
|
91
|
+
"""Validate configuration values."""
|
|
92
|
+
if self.min_occurrences < 1:
|
|
93
|
+
raise ValueError(f"min_occurrences must be at least 1, got {self.min_occurrences}")
|
|
94
|
+
if self.min_values_for_enum < 2:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"min_values_for_enum must be at least 2, got {self.min_values_for_enum}"
|
|
97
|
+
)
|
|
98
|
+
if self.max_values_for_enum < self.min_values_for_enum:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"max_values_for_enum ({self.max_values_for_enum}) must be >= "
|
|
101
|
+
f"min_values_for_enum ({self.min_values_for_enum})"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_dict(
|
|
106
|
+
cls, config: dict[str, Any], language: str | None = None
|
|
107
|
+
) -> "StringlyTypedConfig":
|
|
108
|
+
"""Load configuration from dictionary.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
config: Dictionary containing configuration values
|
|
112
|
+
language: Programming language for language-specific overrides
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
StringlyTypedConfig instance with values from dictionary
|
|
116
|
+
"""
|
|
117
|
+
# Check for language-specific overrides first
|
|
118
|
+
if language and language in config:
|
|
119
|
+
lang_config = config[language]
|
|
120
|
+
return cls._from_merged_config(config, lang_config)
|
|
121
|
+
|
|
122
|
+
return cls._from_base_config(config)
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def _from_base_config(cls, config: dict[str, Any]) -> "StringlyTypedConfig":
|
|
126
|
+
"""Create config from base configuration dictionary.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
config: Base configuration dictionary
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
StringlyTypedConfig instance
|
|
133
|
+
"""
|
|
134
|
+
# Merge user ignore patterns with defaults
|
|
135
|
+
user_ignore = config.get("ignore", [])
|
|
136
|
+
merged_ignore = DEFAULT_IGNORE_PATTERNS.copy() + user_ignore
|
|
137
|
+
|
|
138
|
+
return cls(
|
|
139
|
+
enabled=config.get("enabled", True),
|
|
140
|
+
min_occurrences=config.get("min_occurrences", DEFAULT_MIN_OCCURRENCES),
|
|
141
|
+
min_values_for_enum=config.get("min_values_for_enum", DEFAULT_MIN_VALUES_FOR_ENUM),
|
|
142
|
+
max_values_for_enum=config.get("max_values_for_enum", DEFAULT_MAX_VALUES_FOR_ENUM),
|
|
143
|
+
require_cross_file=config.get("require_cross_file", True),
|
|
144
|
+
ignore=merged_ignore,
|
|
145
|
+
allowed_string_sets=config.get("allowed_string_sets", []),
|
|
146
|
+
exclude_variables=config.get("exclude_variables", []),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def _from_merged_config(
|
|
151
|
+
cls, base_config: dict[str, Any], lang_config: dict[str, Any]
|
|
152
|
+
) -> "StringlyTypedConfig":
|
|
153
|
+
"""Create config with language-specific overrides merged.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
base_config: Base configuration dictionary
|
|
157
|
+
lang_config: Language-specific configuration overrides
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
StringlyTypedConfig instance with merged values
|
|
161
|
+
"""
|
|
162
|
+
# Merge user ignore patterns with defaults
|
|
163
|
+
user_ignore = lang_config.get("ignore", base_config.get("ignore", []))
|
|
164
|
+
merged_ignore = DEFAULT_IGNORE_PATTERNS.copy() + user_ignore
|
|
165
|
+
|
|
166
|
+
return cls(
|
|
167
|
+
enabled=lang_config.get("enabled", base_config.get("enabled", True)),
|
|
168
|
+
min_occurrences=lang_config.get(
|
|
169
|
+
"min_occurrences",
|
|
170
|
+
base_config.get("min_occurrences", DEFAULT_MIN_OCCURRENCES),
|
|
171
|
+
),
|
|
172
|
+
min_values_for_enum=lang_config.get(
|
|
173
|
+
"min_values_for_enum",
|
|
174
|
+
base_config.get("min_values_for_enum", DEFAULT_MIN_VALUES_FOR_ENUM),
|
|
175
|
+
),
|
|
176
|
+
max_values_for_enum=lang_config.get(
|
|
177
|
+
"max_values_for_enum",
|
|
178
|
+
base_config.get("max_values_for_enum", DEFAULT_MAX_VALUES_FOR_ENUM),
|
|
179
|
+
),
|
|
180
|
+
require_cross_file=lang_config.get(
|
|
181
|
+
"require_cross_file", base_config.get("require_cross_file", True)
|
|
182
|
+
),
|
|
183
|
+
ignore=merged_ignore,
|
|
184
|
+
allowed_string_sets=lang_config.get(
|
|
185
|
+
"allowed_string_sets", base_config.get("allowed_string_sets", [])
|
|
186
|
+
),
|
|
187
|
+
exclude_variables=lang_config.get(
|
|
188
|
+
"exclude_variables", base_config.get("exclude_variables", [])
|
|
189
|
+
),
|
|
190
|
+
)
|