yuho 5.0.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.
- yuho/__init__.py +16 -0
- yuho/ast/__init__.py +196 -0
- yuho/ast/builder.py +926 -0
- yuho/ast/constant_folder.py +280 -0
- yuho/ast/dead_code.py +199 -0
- yuho/ast/exhaustiveness.py +503 -0
- yuho/ast/nodes.py +907 -0
- yuho/ast/overlap.py +291 -0
- yuho/ast/reachability.py +293 -0
- yuho/ast/scope_analysis.py +490 -0
- yuho/ast/transformer.py +490 -0
- yuho/ast/type_check.py +471 -0
- yuho/ast/type_inference.py +425 -0
- yuho/ast/visitor.py +239 -0
- yuho/cli/__init__.py +14 -0
- yuho/cli/commands/__init__.py +1 -0
- yuho/cli/commands/api.py +431 -0
- yuho/cli/commands/ast_viz.py +334 -0
- yuho/cli/commands/check.py +218 -0
- yuho/cli/commands/config.py +311 -0
- yuho/cli/commands/contribute.py +122 -0
- yuho/cli/commands/diff.py +487 -0
- yuho/cli/commands/explain.py +240 -0
- yuho/cli/commands/fmt.py +253 -0
- yuho/cli/commands/generate.py +316 -0
- yuho/cli/commands/graph.py +410 -0
- yuho/cli/commands/init.py +120 -0
- yuho/cli/commands/library.py +656 -0
- yuho/cli/commands/lint.py +503 -0
- yuho/cli/commands/lsp.py +36 -0
- yuho/cli/commands/preview.py +377 -0
- yuho/cli/commands/repl.py +444 -0
- yuho/cli/commands/serve.py +44 -0
- yuho/cli/commands/test.py +528 -0
- yuho/cli/commands/transpile.py +121 -0
- yuho/cli/commands/wizard.py +370 -0
- yuho/cli/completions.py +182 -0
- yuho/cli/error_formatter.py +193 -0
- yuho/cli/main.py +1064 -0
- yuho/config/__init__.py +46 -0
- yuho/config/loader.py +235 -0
- yuho/config/mask.py +194 -0
- yuho/config/schema.py +147 -0
- yuho/library/__init__.py +84 -0
- yuho/library/index.py +328 -0
- yuho/library/install.py +699 -0
- yuho/library/lockfile.py +330 -0
- yuho/library/package.py +421 -0
- yuho/library/resolver.py +791 -0
- yuho/library/signature.py +335 -0
- yuho/llm/__init__.py +45 -0
- yuho/llm/config.py +75 -0
- yuho/llm/factory.py +123 -0
- yuho/llm/prompts.py +146 -0
- yuho/llm/providers.py +383 -0
- yuho/llm/utils.py +470 -0
- yuho/lsp/__init__.py +14 -0
- yuho/lsp/code_action_handler.py +518 -0
- yuho/lsp/completion_handler.py +85 -0
- yuho/lsp/diagnostics.py +100 -0
- yuho/lsp/hover_handler.py +130 -0
- yuho/lsp/server.py +1425 -0
- yuho/mcp/__init__.py +10 -0
- yuho/mcp/server.py +1452 -0
- yuho/parser/__init__.py +8 -0
- yuho/parser/source_location.py +108 -0
- yuho/parser/wrapper.py +311 -0
- yuho/testing/__init__.py +48 -0
- yuho/testing/coverage.py +274 -0
- yuho/testing/fixtures.py +263 -0
- yuho/transpile/__init__.py +52 -0
- yuho/transpile/alloy_transpiler.py +546 -0
- yuho/transpile/base.py +100 -0
- yuho/transpile/blocks_transpiler.py +338 -0
- yuho/transpile/english_transpiler.py +470 -0
- yuho/transpile/graphql_transpiler.py +404 -0
- yuho/transpile/json_transpiler.py +217 -0
- yuho/transpile/jsonld_transpiler.py +250 -0
- yuho/transpile/latex_preamble.py +161 -0
- yuho/transpile/latex_transpiler.py +406 -0
- yuho/transpile/latex_utils.py +206 -0
- yuho/transpile/mermaid_transpiler.py +357 -0
- yuho/transpile/registry.py +275 -0
- yuho/verify/__init__.py +43 -0
- yuho/verify/alloy.py +352 -0
- yuho/verify/combined.py +218 -0
- yuho/verify/z3_solver.py +1155 -0
- yuho-5.0.0.dist-info/METADATA +186 -0
- yuho-5.0.0.dist-info/RECORD +91 -0
- yuho-5.0.0.dist-info/WHEEL +4 -0
- yuho-5.0.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exhaustiveness checker for match expressions using pattern matrix algorithm.
|
|
3
|
+
|
|
4
|
+
Validates that match expressions cover all possible cases based on the
|
|
5
|
+
scrutinee type. Uses the pattern matrix algorithm as described in
|
|
6
|
+
"Warnings for pattern matching" (Maranget, 2007).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum, auto
|
|
11
|
+
from typing import List, Optional, Set, Dict, Tuple, Any, FrozenSet
|
|
12
|
+
|
|
13
|
+
from yuho.ast import nodes
|
|
14
|
+
from yuho.ast.visitor import Visitor
|
|
15
|
+
from yuho.ast.type_inference import (
|
|
16
|
+
TypeAnnotation,
|
|
17
|
+
TypeInferenceResult,
|
|
18
|
+
BOOL_TYPE,
|
|
19
|
+
INT_TYPE,
|
|
20
|
+
STRING_TYPE,
|
|
21
|
+
UNKNOWN_TYPE,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PatternKind(Enum):
|
|
26
|
+
"""Classification of pattern types for exhaustiveness analysis."""
|
|
27
|
+
|
|
28
|
+
WILDCARD = auto() # _ or binding pattern
|
|
29
|
+
LITERAL = auto() # Specific value (bool, int, string)
|
|
30
|
+
STRUCT = auto() # Struct/enum destructuring
|
|
31
|
+
GUARD = auto() # Conditional pattern (treated as partial)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class AbstractPattern:
|
|
36
|
+
"""
|
|
37
|
+
Abstract representation of a pattern for exhaustiveness analysis.
|
|
38
|
+
|
|
39
|
+
Patterns are simplified to focus on exhaustiveness:
|
|
40
|
+
- Wildcards and bindings are equivalent (match anything)
|
|
41
|
+
- Literals must be tracked for finite types (bool, enum)
|
|
42
|
+
- Struct patterns track constructor and field patterns
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
kind: PatternKind
|
|
46
|
+
value: Any = None # Literal value or struct name
|
|
47
|
+
children: Tuple["AbstractPattern", ...] = ()
|
|
48
|
+
has_guard: bool = False
|
|
49
|
+
|
|
50
|
+
def is_wildcard(self) -> bool:
|
|
51
|
+
"""True if this pattern matches all values."""
|
|
52
|
+
return self.kind == PatternKind.WILDCARD and not self.has_guard
|
|
53
|
+
|
|
54
|
+
def covers(self, other: "AbstractPattern") -> bool:
|
|
55
|
+
"""Check if this pattern covers another pattern."""
|
|
56
|
+
if self.is_wildcard():
|
|
57
|
+
return True
|
|
58
|
+
if other.is_wildcard():
|
|
59
|
+
return False
|
|
60
|
+
if self.kind != other.kind:
|
|
61
|
+
return False
|
|
62
|
+
if self.value != other.value:
|
|
63
|
+
return False
|
|
64
|
+
if len(self.children) != len(other.children):
|
|
65
|
+
return False
|
|
66
|
+
return all(c1.covers(c2) for c1, c2 in zip(self.children, other.children))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class PatternRow:
|
|
71
|
+
"""Row in the pattern matrix representing one match arm."""
|
|
72
|
+
|
|
73
|
+
patterns: List[AbstractPattern]
|
|
74
|
+
arm_index: int
|
|
75
|
+
|
|
76
|
+
def is_empty(self) -> bool:
|
|
77
|
+
return len(self.patterns) == 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class PatternMatrix:
|
|
82
|
+
"""
|
|
83
|
+
Matrix of patterns for exhaustiveness checking.
|
|
84
|
+
|
|
85
|
+
Rows represent match arms, columns represent pattern positions.
|
|
86
|
+
The algorithm works by specialization: selecting a column and
|
|
87
|
+
splitting the matrix based on constructors that appear there.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
rows: List[PatternRow]
|
|
91
|
+
column_types: List[TypeAnnotation] = field(default_factory=list)
|
|
92
|
+
|
|
93
|
+
def is_empty(self) -> bool:
|
|
94
|
+
"""Matrix is empty if it has no rows."""
|
|
95
|
+
return len(self.rows) == 0
|
|
96
|
+
|
|
97
|
+
def has_empty_row(self) -> bool:
|
|
98
|
+
"""Check if matrix has a row with no patterns (matching all)."""
|
|
99
|
+
return any(row.is_empty() for row in self.rows)
|
|
100
|
+
|
|
101
|
+
def width(self) -> int:
|
|
102
|
+
"""Number of columns in the matrix."""
|
|
103
|
+
return self.rows[0].patterns if self.rows else 0
|
|
104
|
+
|
|
105
|
+
def specialize(self, col: int, constructor: AbstractPattern) -> "PatternMatrix":
|
|
106
|
+
"""
|
|
107
|
+
Specialize matrix by a constructor in the given column.
|
|
108
|
+
|
|
109
|
+
For each row:
|
|
110
|
+
- If pattern at col is wildcard: expand to constructor's arity
|
|
111
|
+
- If pattern at col matches constructor: include with children expanded
|
|
112
|
+
- Otherwise: exclude row
|
|
113
|
+
"""
|
|
114
|
+
new_rows = []
|
|
115
|
+
|
|
116
|
+
for row in self.rows:
|
|
117
|
+
if col >= len(row.patterns):
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
pattern = row.patterns[col]
|
|
121
|
+
|
|
122
|
+
if pattern.is_wildcard():
|
|
123
|
+
# Wildcard matches constructor; expand to wildcard children
|
|
124
|
+
new_patterns = (
|
|
125
|
+
row.patterns[:col] +
|
|
126
|
+
[AbstractPattern(PatternKind.WILDCARD) for _ in constructor.children] +
|
|
127
|
+
row.patterns[col + 1:]
|
|
128
|
+
)
|
|
129
|
+
new_rows.append(PatternRow(new_patterns, row.arm_index))
|
|
130
|
+
|
|
131
|
+
elif pattern.kind == constructor.kind and pattern.value == constructor.value:
|
|
132
|
+
# Same constructor; expand children
|
|
133
|
+
new_patterns = (
|
|
134
|
+
row.patterns[:col] +
|
|
135
|
+
list(pattern.children) +
|
|
136
|
+
row.patterns[col + 1:]
|
|
137
|
+
)
|
|
138
|
+
new_rows.append(PatternRow(new_patterns, row.arm_index))
|
|
139
|
+
|
|
140
|
+
return PatternMatrix(new_rows, self.column_types)
|
|
141
|
+
|
|
142
|
+
def default_matrix(self, col: int) -> "PatternMatrix":
|
|
143
|
+
"""
|
|
144
|
+
Create default matrix for patterns that don't match given constructors.
|
|
145
|
+
|
|
146
|
+
Keeps only rows with wildcards at the given column.
|
|
147
|
+
"""
|
|
148
|
+
new_rows = []
|
|
149
|
+
|
|
150
|
+
for row in self.rows:
|
|
151
|
+
if col >= len(row.patterns):
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
pattern = row.patterns[col]
|
|
155
|
+
|
|
156
|
+
if pattern.is_wildcard():
|
|
157
|
+
new_patterns = row.patterns[:col] + row.patterns[col + 1:]
|
|
158
|
+
new_rows.append(PatternRow(new_patterns, row.arm_index))
|
|
159
|
+
|
|
160
|
+
return PatternMatrix(new_rows, self.column_types)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class ExhaustivenessResult:
|
|
165
|
+
"""Result of exhaustiveness checking."""
|
|
166
|
+
|
|
167
|
+
is_exhaustive: bool
|
|
168
|
+
missing_patterns: List[str] = field(default_factory=list)
|
|
169
|
+
match_node: Optional[nodes.MatchExprNode] = None
|
|
170
|
+
|
|
171
|
+
def __str__(self) -> str:
|
|
172
|
+
if self.is_exhaustive:
|
|
173
|
+
return "Match is exhaustive"
|
|
174
|
+
patterns = ", ".join(self.missing_patterns) if self.missing_patterns else "unknown"
|
|
175
|
+
return f"Non-exhaustive match: missing {patterns}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class ExhaustivenessError:
|
|
180
|
+
"""Error information for non-exhaustive match."""
|
|
181
|
+
|
|
182
|
+
message: str
|
|
183
|
+
line: int = 0
|
|
184
|
+
column: int = 0
|
|
185
|
+
missing_patterns: List[str] = field(default_factory=list)
|
|
186
|
+
|
|
187
|
+
def __str__(self) -> str:
|
|
188
|
+
loc = f":{self.line}:{self.column}" if self.line else ""
|
|
189
|
+
return f"{loc} {self.message}"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class PatternExtractor(Visitor):
|
|
193
|
+
"""Extracts AbstractPattern from AST PatternNode."""
|
|
194
|
+
|
|
195
|
+
def __init__(self, type_info: Optional[TypeInferenceResult] = None):
|
|
196
|
+
self.type_info = type_info
|
|
197
|
+
|
|
198
|
+
def extract(self, pattern: nodes.PatternNode, has_guard: bool = False) -> AbstractPattern:
|
|
199
|
+
"""Extract abstract pattern from AST pattern node."""
|
|
200
|
+
result = self.visit(pattern)
|
|
201
|
+
if has_guard and result.kind != PatternKind.GUARD:
|
|
202
|
+
# Wrap in guard pattern to indicate partial coverage
|
|
203
|
+
return AbstractPattern(PatternKind.GUARD, has_guard=True)
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
def visit_wildcard_pattern(self, node: nodes.WildcardPattern) -> AbstractPattern:
|
|
207
|
+
return AbstractPattern(PatternKind.WILDCARD)
|
|
208
|
+
|
|
209
|
+
def visit_binding_pattern(self, node: nodes.BindingPattern) -> AbstractPattern:
|
|
210
|
+
# Binding patterns are wildcards from exhaustiveness perspective
|
|
211
|
+
return AbstractPattern(PatternKind.WILDCARD)
|
|
212
|
+
|
|
213
|
+
def visit_literal_pattern(self, node: nodes.LiteralPattern) -> AbstractPattern:
|
|
214
|
+
literal = node.literal
|
|
215
|
+
|
|
216
|
+
if isinstance(literal, nodes.BoolLit):
|
|
217
|
+
return AbstractPattern(PatternKind.LITERAL, value=literal.value)
|
|
218
|
+
elif isinstance(literal, nodes.IntLit):
|
|
219
|
+
return AbstractPattern(PatternKind.LITERAL, value=literal.value)
|
|
220
|
+
elif isinstance(literal, nodes.StringLit):
|
|
221
|
+
return AbstractPattern(PatternKind.LITERAL, value=literal.value)
|
|
222
|
+
else:
|
|
223
|
+
# Other literals treated as specific values
|
|
224
|
+
return AbstractPattern(PatternKind.LITERAL, value=str(literal))
|
|
225
|
+
|
|
226
|
+
def visit_struct_pattern(self, node: nodes.StructPattern) -> AbstractPattern:
|
|
227
|
+
children = tuple(
|
|
228
|
+
self.visit(fp.pattern) if fp.pattern else AbstractPattern(PatternKind.WILDCARD)
|
|
229
|
+
for fp in node.fields
|
|
230
|
+
)
|
|
231
|
+
return AbstractPattern(PatternKind.STRUCT, value=node.type_name, children=children)
|
|
232
|
+
|
|
233
|
+
def visit_field_pattern(self, node: nodes.FieldPattern) -> AbstractPattern:
|
|
234
|
+
if node.pattern:
|
|
235
|
+
return self.visit(node.pattern)
|
|
236
|
+
return AbstractPattern(PatternKind.WILDCARD)
|
|
237
|
+
|
|
238
|
+
def generic_visit(self, node: nodes.ASTNode) -> AbstractPattern:
|
|
239
|
+
# Unknown pattern types are treated as wildcards
|
|
240
|
+
return AbstractPattern(PatternKind.WILDCARD)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class ExhaustivenessChecker(Visitor):
|
|
244
|
+
"""
|
|
245
|
+
Checks exhaustiveness of match expressions using pattern matrix algorithm.
|
|
246
|
+
|
|
247
|
+
For each match expression with ensure_exhaustiveness=True, verifies that
|
|
248
|
+
all possible values of the scrutinee type are covered by at least one arm.
|
|
249
|
+
|
|
250
|
+
Usage:
|
|
251
|
+
type_visitor = TypeInferenceVisitor()
|
|
252
|
+
module.accept(type_visitor)
|
|
253
|
+
|
|
254
|
+
checker = ExhaustivenessChecker(type_visitor.result)
|
|
255
|
+
module.accept(checker)
|
|
256
|
+
|
|
257
|
+
for error in checker.errors:
|
|
258
|
+
print(error)
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def __init__(self, type_info: Optional[TypeInferenceResult] = None):
|
|
262
|
+
self.type_info = type_info or TypeInferenceResult()
|
|
263
|
+
self.errors: List[ExhaustivenessError] = []
|
|
264
|
+
self.results: List[ExhaustivenessResult] = []
|
|
265
|
+
self._extractor = PatternExtractor(type_info)
|
|
266
|
+
|
|
267
|
+
# Known enum/sum types and their constructors
|
|
268
|
+
self._enum_constructors: Dict[str, List[str]] = {}
|
|
269
|
+
|
|
270
|
+
def check(self, module: nodes.ModuleNode) -> List[ExhaustivenessError]:
|
|
271
|
+
"""Check all match expressions in module."""
|
|
272
|
+
# First pass: collect enum/struct definitions
|
|
273
|
+
self._collect_type_info(module)
|
|
274
|
+
|
|
275
|
+
# Second pass: check match expressions
|
|
276
|
+
self.visit(module)
|
|
277
|
+
|
|
278
|
+
return self.errors
|
|
279
|
+
|
|
280
|
+
def _collect_type_info(self, module: nodes.ModuleNode) -> None:
|
|
281
|
+
"""Collect enum and struct definitions for constructor analysis."""
|
|
282
|
+
for decl in module.declarations:
|
|
283
|
+
if isinstance(decl, nodes.StructDefNode):
|
|
284
|
+
# Check if this is an enum (fields without types are variants)
|
|
285
|
+
variants = []
|
|
286
|
+
for field_def in decl.fields:
|
|
287
|
+
if field_def.type_annotation is None:
|
|
288
|
+
# This is an enum variant
|
|
289
|
+
variants.append(field_def.name)
|
|
290
|
+
|
|
291
|
+
if variants:
|
|
292
|
+
self._enum_constructors[decl.name] = variants
|
|
293
|
+
|
|
294
|
+
def _get_type_constructors(self, type_ann: TypeAnnotation) -> List[AbstractPattern]:
|
|
295
|
+
"""
|
|
296
|
+
Get all constructors for a type.
|
|
297
|
+
|
|
298
|
+
For finite types (bool, enum), returns all possible values.
|
|
299
|
+
For infinite types (int, string), returns empty list (use default).
|
|
300
|
+
"""
|
|
301
|
+
type_name = type_ann.type_name
|
|
302
|
+
|
|
303
|
+
if type_name == "bool":
|
|
304
|
+
return [
|
|
305
|
+
AbstractPattern(PatternKind.LITERAL, value=True),
|
|
306
|
+
AbstractPattern(PatternKind.LITERAL, value=False),
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
if type_name in self._enum_constructors:
|
|
310
|
+
return [
|
|
311
|
+
AbstractPattern(PatternKind.STRUCT, value=variant)
|
|
312
|
+
for variant in self._enum_constructors[type_name]
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
# Infinite types - no enumerable constructors
|
|
316
|
+
return []
|
|
317
|
+
|
|
318
|
+
def _check_usefulness(self, matrix: PatternMatrix, scrutinee_type: TypeAnnotation) -> Optional[List[str]]:
|
|
319
|
+
"""
|
|
320
|
+
Check if a pattern would be useful (not covered by existing patterns).
|
|
321
|
+
|
|
322
|
+
Returns None if matrix covers all cases (exhaustive).
|
|
323
|
+
Returns list of missing patterns otherwise.
|
|
324
|
+
"""
|
|
325
|
+
# Base case: empty matrix means pattern is useful (not covered)
|
|
326
|
+
if matrix.is_empty():
|
|
327
|
+
return ["_"] # Wildcard represents uncovered case
|
|
328
|
+
|
|
329
|
+
# Base case: row with no patterns means all inputs matched
|
|
330
|
+
if matrix.has_empty_row():
|
|
331
|
+
return None # Exhaustive
|
|
332
|
+
|
|
333
|
+
# No patterns to check
|
|
334
|
+
if not matrix.rows or not matrix.rows[0].patterns:
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
# Select first column for analysis
|
|
338
|
+
col = 0
|
|
339
|
+
constructors = self._get_column_constructors(matrix, col, scrutinee_type)
|
|
340
|
+
|
|
341
|
+
if not constructors:
|
|
342
|
+
# Infinite type - check default matrix
|
|
343
|
+
default = matrix.default_matrix(col)
|
|
344
|
+
return self._check_usefulness(default, UNKNOWN_TYPE)
|
|
345
|
+
|
|
346
|
+
# Check if all constructors are covered
|
|
347
|
+
missing = []
|
|
348
|
+
all_constructors_present = self._all_constructors_present(matrix, col, constructors)
|
|
349
|
+
|
|
350
|
+
for ctor in constructors:
|
|
351
|
+
specialized = matrix.specialize(col, ctor)
|
|
352
|
+
|
|
353
|
+
# Recursively check specialized matrix
|
|
354
|
+
# For simplicity, treat struct children as unknown type
|
|
355
|
+
child_type = UNKNOWN_TYPE
|
|
356
|
+
sub_result = self._check_usefulness(specialized, child_type)
|
|
357
|
+
|
|
358
|
+
if sub_result is not None:
|
|
359
|
+
# This constructor has uncovered cases
|
|
360
|
+
if ctor.kind == PatternKind.LITERAL:
|
|
361
|
+
missing.append(str(ctor.value))
|
|
362
|
+
elif ctor.kind == PatternKind.STRUCT:
|
|
363
|
+
missing.append(str(ctor.value))
|
|
364
|
+
else:
|
|
365
|
+
missing.extend(sub_result)
|
|
366
|
+
|
|
367
|
+
# If not all constructors present, check default matrix
|
|
368
|
+
if not all_constructors_present:
|
|
369
|
+
default = matrix.default_matrix(col)
|
|
370
|
+
if not default.is_empty():
|
|
371
|
+
default_result = self._check_usefulness(default, UNKNOWN_TYPE)
|
|
372
|
+
if default_result is not None:
|
|
373
|
+
missing.extend(default_result)
|
|
374
|
+
else:
|
|
375
|
+
# Default cases not covered
|
|
376
|
+
missing.append("_")
|
|
377
|
+
|
|
378
|
+
return missing if missing else None
|
|
379
|
+
|
|
380
|
+
def _get_column_constructors(
|
|
381
|
+
self,
|
|
382
|
+
matrix: PatternMatrix,
|
|
383
|
+
col: int,
|
|
384
|
+
scrutinee_type: TypeAnnotation
|
|
385
|
+
) -> List[AbstractPattern]:
|
|
386
|
+
"""Get all constructors used in a column + type-defined constructors."""
|
|
387
|
+
seen: Set[Tuple[PatternKind, Any]] = set()
|
|
388
|
+
result = []
|
|
389
|
+
|
|
390
|
+
# First add type-defined constructors
|
|
391
|
+
type_ctors = self._get_type_constructors(scrutinee_type)
|
|
392
|
+
for ctor in type_ctors:
|
|
393
|
+
key = (ctor.kind, ctor.value)
|
|
394
|
+
if key not in seen:
|
|
395
|
+
seen.add(key)
|
|
396
|
+
result.append(ctor)
|
|
397
|
+
|
|
398
|
+
# Then add constructors from patterns (for non-finite types)
|
|
399
|
+
for row in matrix.rows:
|
|
400
|
+
if col < len(row.patterns):
|
|
401
|
+
pattern = row.patterns[col]
|
|
402
|
+
if not pattern.is_wildcard() and pattern.kind != PatternKind.GUARD:
|
|
403
|
+
key = (pattern.kind, pattern.value)
|
|
404
|
+
if key not in seen:
|
|
405
|
+
seen.add(key)
|
|
406
|
+
result.append(pattern)
|
|
407
|
+
|
|
408
|
+
return result
|
|
409
|
+
|
|
410
|
+
def _all_constructors_present(
|
|
411
|
+
self,
|
|
412
|
+
matrix: PatternMatrix,
|
|
413
|
+
col: int,
|
|
414
|
+
constructors: List[AbstractPattern]
|
|
415
|
+
) -> bool:
|
|
416
|
+
"""Check if all type constructors appear in the column."""
|
|
417
|
+
if not constructors:
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
present: Set[Tuple[PatternKind, Any]] = set()
|
|
421
|
+
|
|
422
|
+
for row in matrix.rows:
|
|
423
|
+
if col < len(row.patterns):
|
|
424
|
+
pattern = row.patterns[col]
|
|
425
|
+
if not pattern.is_wildcard():
|
|
426
|
+
present.add((pattern.kind, pattern.value))
|
|
427
|
+
|
|
428
|
+
required = {(c.kind, c.value) for c in constructors}
|
|
429
|
+
return required <= present
|
|
430
|
+
|
|
431
|
+
def visit_match_expr(self, node: nodes.MatchExprNode) -> Any:
|
|
432
|
+
"""Check exhaustiveness of match expression."""
|
|
433
|
+
# Visit children first
|
|
434
|
+
self.generic_visit(node)
|
|
435
|
+
|
|
436
|
+
# Skip if exhaustiveness check is disabled
|
|
437
|
+
if not node.ensure_exhaustiveness:
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
# Build pattern matrix from arms
|
|
441
|
+
rows = []
|
|
442
|
+
for i, arm in enumerate(node.arms):
|
|
443
|
+
pattern = self._extractor.extract(arm.pattern, has_guard=arm.guard is not None)
|
|
444
|
+
rows.append(PatternRow([pattern], i))
|
|
445
|
+
|
|
446
|
+
matrix = PatternMatrix(rows)
|
|
447
|
+
|
|
448
|
+
# Get scrutinee type
|
|
449
|
+
scrutinee_type = UNKNOWN_TYPE
|
|
450
|
+
if node.scrutinee and self.type_info:
|
|
451
|
+
scrutinee_type = self.type_info.get_type(node.scrutinee)
|
|
452
|
+
|
|
453
|
+
# Check exhaustiveness
|
|
454
|
+
missing = self._check_usefulness(matrix, scrutinee_type)
|
|
455
|
+
|
|
456
|
+
result = ExhaustivenessResult(
|
|
457
|
+
is_exhaustive=(missing is None),
|
|
458
|
+
missing_patterns=missing or [],
|
|
459
|
+
match_node=node,
|
|
460
|
+
)
|
|
461
|
+
self.results.append(result)
|
|
462
|
+
|
|
463
|
+
if not result.is_exhaustive:
|
|
464
|
+
# Extract source location
|
|
465
|
+
line = 0
|
|
466
|
+
column = 0
|
|
467
|
+
if node.source_location:
|
|
468
|
+
line = node.source_location.start_line
|
|
469
|
+
column = node.source_location.start_column
|
|
470
|
+
|
|
471
|
+
patterns_str = ", ".join(result.missing_patterns[:5])
|
|
472
|
+
if len(result.missing_patterns) > 5:
|
|
473
|
+
patterns_str += ", ..."
|
|
474
|
+
|
|
475
|
+
self.errors.append(ExhaustivenessError(
|
|
476
|
+
message=f"Non-exhaustive match: patterns not covered: {patterns_str}",
|
|
477
|
+
line=line,
|
|
478
|
+
column=column,
|
|
479
|
+
missing_patterns=result.missing_patterns,
|
|
480
|
+
))
|
|
481
|
+
|
|
482
|
+
def visit_module(self, node: nodes.ModuleNode) -> Any:
|
|
483
|
+
"""Entry point: check all match expressions in module."""
|
|
484
|
+
self._collect_type_info(node)
|
|
485
|
+
return self.generic_visit(node)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def check_exhaustiveness(
|
|
489
|
+
module: nodes.ModuleNode,
|
|
490
|
+
type_info: Optional[TypeInferenceResult] = None
|
|
491
|
+
) -> List[ExhaustivenessError]:
|
|
492
|
+
"""
|
|
493
|
+
Check exhaustiveness of all match expressions in a module.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
module: The module AST to check
|
|
497
|
+
type_info: Optional type inference result for better analysis
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
List of errors for non-exhaustive match expressions
|
|
501
|
+
"""
|
|
502
|
+
checker = ExhaustivenessChecker(type_info)
|
|
503
|
+
return checker.check(module)
|