feat(lsp): unknown-name warnings, Nil validation and match exhaustiveness
`Nil` is rejected outside nullable/`Option` matches in both the compiler and the language server, non-exhaustive nullable matches report the missing branch names, and unresolved plain names are published as warnings. Diagnostics now honour ParseError.severity.
This commit is contained in:
1 parent
2781ba7466
commit
7289b027ec
4 files changed
+163
-7
No files matched your search
@@ -81,8 +81,10 @@ novirtual class MatchGenMixin:
|
||||
dpat: ast.AST* = ast.MatchCase*(dcase).pattern
|
||||
if dpat is not None:
|
||||
if dpat.kind() == ast.ASTKind.MatchAs and ast.MatchAs*(dpat).pattern is None:
|
||||
# Count only guardless wildcard arms; guarded `case _ if ...` arms may repeat and are not counted as default.
|
||||
if ast.MatchCase*(dcase).guard is None: default_cnt = default_cnt + 1
|
||||
# `case Nil` is an Option pattern, not a wildcard; count only guardless `case _`.
|
||||
dname_chk: str = ast.MatchAs*(dpat).name
|
||||
if dname_chk is None or string.strcmp(dname_chk, "Nil") != 0:
|
||||
if ast.MatchCase*(dcase).guard is None: default_cnt = default_cnt + 1
|
||||
elif m_node.is_switch == 0:
|
||||
dcj: int = dci + 1
|
||||
while dcj < ccn:
|
||||
@@ -554,6 +556,8 @@ novirtual class MatchGenMixin:
|
||||
elif pk == ast.ASTKind.MatchAs:
|
||||
ma: ast.MatchAs* = ast.MatchAs*(pat)
|
||||
if ma.pattern is None:
|
||||
if ma.name is not None and string.strcmp(ma.name, "Nil") == 0:
|
||||
self.Gen.TypeGenErr.fatal_error(pat, "`Nil` is only valid for a nullable Option match (`own T?` / `T?`), not a renum match", "TypeError")
|
||||
is_def = 1
|
||||
has_default = 1
|
||||
default_case_node = case_node
|
||||
@@ -1102,10 +1106,30 @@ novirtual class MatchGenMixin:
|
||||
gzero: mlirlite.Value* = mlirlite.const_int(pool, gbits if gbits > 0 else 32, 0)
|
||||
return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, gv, gzero)
|
||||
|
||||
def _pattern_is_nil(self, pat: ast.AST*) -> int:
|
||||
"""1 when a pattern is spelled `Nil` / `Nil()`."""
|
||||
if pat is None: return 0
|
||||
if pat.kind() == ast.ASTKind.MatchAs:
|
||||
ma: ast.MatchAs* = ast.MatchAs*(pat)
|
||||
if ma.pattern is None and ma.name is not None and string.strcmp(ma.name, "Nil") == 0: return 1
|
||||
return 0
|
||||
if pat.kind() == ast.ASTKind.MatchClass:
|
||||
vnm_n: str = self._extract_variant_name_from_cls(pat)
|
||||
if vnm_n is not None and string.strcmp(vnm_n, "Nil") == 0: return 1
|
||||
return 0
|
||||
|
||||
def _handle_simple_match(self, trans: HT.Translator*, node: ast.Match*, subject_val: mlirlite.Value*) -> int:
|
||||
pool: memhub.MemBuddy* = trans.Pool
|
||||
builder: mlirlite.IRBuilder* = trans._cur_builder
|
||||
func: mlirlite.Function* = trans._cur_func
|
||||
# `Nil` is an Option pattern, never a wildcard/capture.
|
||||
if node.cases is not None:
|
||||
ni: int = 0
|
||||
while ni < int(len(node.cases)):
|
||||
nc: ast.MatchCase* = ast.MatchCase*(node.cases.get(sizet(ni)))
|
||||
ni += 1
|
||||
if nc is not None and self._pattern_is_nil(nc.pattern) != 0:
|
||||
self.Gen.TypeGenErr.fatal_error(nc.pattern, "`Nil` is only valid for a nullable Option match (`own T?` / `T?`) or the builtin Option renum; it is not a wildcard", "TypeError")
|
||||
# Same as the main path: locally track this simple match's br end_bb and overwrite _ctrl_merge_reachable at the end.
|
||||
sm_merge_reach: int = 0
|
||||
|
||||
|
||||
+134
-2
@@ -608,7 +608,8 @@ def _ExprLen(doc: LspDocuments.LspDocument*, node: ast.AST*) -> int:
|
||||
|
||||
|
||||
def _EmitTypeError(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
|
||||
node: ast.AST*, msg: str, errors: list[ast.ParseError*]*):
|
||||
node: ast.AST*, msg: str, errors: list[ast.ParseError*]*,
|
||||
severity: int = 1):
|
||||
if node is None or msg is None or errors is None:
|
||||
return
|
||||
if int(len(errors)) >= LSP_CHECK_DIAG_CAP:
|
||||
@@ -638,6 +639,7 @@ def _EmitTypeError(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
|
||||
e.col = sc
|
||||
e.end_line = line0 + 1
|
||||
e.end_col = ec
|
||||
e.severity = severity
|
||||
errors.append(e)
|
||||
|
||||
|
||||
@@ -996,6 +998,97 @@ def _NullableDetail(det: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _OccAtExpr(a: LspSymbols.LspAnalysis*, expr: ast.AST*) -> LspSymbols.LspOccurrence*:
|
||||
"""Occurrence at a Name / Attribute expression's identifier position."""
|
||||
if a is None or expr is None: return None
|
||||
line0: int = 0
|
||||
col: int = 0
|
||||
name: str = None
|
||||
if expr.kind() == ast.ASTKind.Name:
|
||||
nm: ast.Name* = (ast.Name*)(expr)
|
||||
if nm.id is None: return None
|
||||
line0 = nm.lineno - 1
|
||||
col = nm.col_offset
|
||||
name = nm.id
|
||||
elif expr.kind() == ast.ASTKind.Attribute:
|
||||
at: ast.Attribute* = (ast.Attribute*)(expr)
|
||||
if at.attr is None: return None
|
||||
line0 = at.lineno - 1
|
||||
col = at.col_offset
|
||||
name = at.attr
|
||||
else:
|
||||
return None
|
||||
colu: int = col
|
||||
if a.doc is not None: colu = LspSource.ByteColToUtf16(a.doc, line0, col)
|
||||
if a.occs is not None:
|
||||
i: int = 0
|
||||
while i < int(len(a.occs)):
|
||||
o: LspSymbols.LspOccurrence* = a.occs.get(sizet(i))
|
||||
i += 1
|
||||
if o is not None and o.line == line0 and o.sc == colu and o.name is not None:
|
||||
if string.strcmp(o.name, name) == 0: return o
|
||||
return None
|
||||
|
||||
|
||||
def _IsKnownPlainName(name: str) -> int:
|
||||
if name is None: return 0
|
||||
if string.strcmp(name, "self") == 0 or string.strcmp(name, "cls") == 0: return 1
|
||||
if string.strcmp(name, "None") == 0 or string.strcmp(name, "True") == 0 or string.strcmp(name, "False") == 0: return 1
|
||||
if string.strcmp(name, "Some") == 0 or string.strcmp(name, "Nil") == 0: return 1
|
||||
if LspSymbols._IsBuiltinTypeName(name) != 0: return 1
|
||||
if string.strcmp(name, "print") == 0 or string.strcmp(name, "len") == 0 or string.strcmp(name, "range") == 0: return 1
|
||||
if string.strcmp(name, "malloc") == 0 or string.strcmp(name, "free") == 0 or string.strcmp(name, "sizeof") == 0: return 1
|
||||
if string.strcmp(name, "memcpy") == 0 or string.strcmp(name, "memset") == 0: return 1
|
||||
if string.strcmp(name, "exit") == 0 or string.strcmp(name, "abort") == 0: return 1
|
||||
if string.strcmp(name, "min") == 0 or string.strcmp(name, "max") == 0 or string.strcmp(name, "abs") == 0: return 1
|
||||
if string.strcmp(name, "ord") == 0 or string.strcmp(name, "chr") == 0: return 1
|
||||
return 0
|
||||
|
||||
|
||||
def _LspNilPattern(pat: ast.AST*) -> int:
|
||||
"""1 when a pattern is spelled `Nil` / `Nil()` (not the MatchSingleton None)."""
|
||||
if pat is None: return 0
|
||||
k: int = pat.kind()
|
||||
if k == ast.ASTKind.MatchAs:
|
||||
ma: ast.MatchAs* = (ast.MatchAs*)(pat)
|
||||
if ma.pattern is None and ma.name is not None and string.strcmp(ma.name, "Nil") == 0: return 1
|
||||
return 0
|
||||
if k == ast.ASTKind.MatchClass:
|
||||
cls: ast.AST* = (ast.MatchClass*)(pat).cls
|
||||
if cls is not None and cls.kind() == ast.ASTKind.Attribute:
|
||||
at: ast.Attribute* = (ast.Attribute*)(cls)
|
||||
if at.attr is not None and string.strcmp(at.attr, "Nil") == 0: return 1
|
||||
if cls is not None and cls.kind() == ast.ASTKind.Name:
|
||||
nm: ast.Name* = (ast.Name*)(cls)
|
||||
if nm.id is not None and string.strcmp(nm.id, "Nil") == 0: return 1
|
||||
return 0
|
||||
|
||||
|
||||
def _LspOptPatternKind(pat: ast.AST*) -> int:
|
||||
"""1=Some, 2=Nil/None, 3=wildcard/capture, 0=other."""
|
||||
if pat is None: return 0
|
||||
k: int = pat.kind()
|
||||
if k == ast.ASTKind.MatchSingleton: return 2
|
||||
if k == ast.ASTKind.MatchAs:
|
||||
ma: ast.MatchAs* = (ast.MatchAs*)(pat)
|
||||
if ma.pattern is None:
|
||||
if ma.name is None: return 3
|
||||
if string.strcmp(ma.name, "Nil") == 0: return 2
|
||||
return 3
|
||||
return 0
|
||||
if k == ast.ASTKind.MatchClass:
|
||||
cls: ast.AST* = (ast.MatchClass*)(pat).cls
|
||||
vnm: str = None
|
||||
if cls is not None and cls.kind() == ast.ASTKind.Attribute:
|
||||
vnm = (ast.Attribute*)(cls).attr
|
||||
elif cls is not None and cls.kind() == ast.ASTKind.Name:
|
||||
vnm = (ast.Name*)(cls).id
|
||||
if vnm is not None:
|
||||
if string.strcmp(vnm, "Some") == 0: return 1
|
||||
if string.strcmp(vnm, "Nil") == 0: return 2
|
||||
return 0
|
||||
|
||||
|
||||
def _SymDetailAt(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*,
|
||||
expr: ast.AST*) -> str:
|
||||
"""Resolved symbol detail (annotation text) at a Name/Attribute expression."""
|
||||
@@ -1046,6 +1139,10 @@ def _WalkChecksExprs(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
|
||||
nm_m: ast.Name* = ast.Name*(e)
|
||||
if nm_m.id is not None and _MovedIs(nm_m.id) != 0:
|
||||
_EmitTypeError(pool, doc, e, f"cannot use '{nm_m.id}': its value was moved (ownership transferred to another binding)", errors)
|
||||
elif nm_m.id is not None and _IsKnownPlainName(nm_m.id) == 0:
|
||||
occ_u: LspSymbols.LspOccurrence* = _OccAtExpr(a, e)
|
||||
if occ_u is not None and occ_u.sym is None and occ_u.head is None and occ_u.builtin_type == 0:
|
||||
_EmitTypeError(pool, doc, e, f"unknown name '{nm_m.id}'", errors, 2)
|
||||
return
|
||||
if k == ast.ASTKind.Call:
|
||||
call: ast.Call* = ast.Call*(e)
|
||||
@@ -1400,7 +1497,42 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
|
||||
_WalkChecks(pool, doc, a, ws, table_pool, s.children, errors)
|
||||
continue
|
||||
if k == ast.ASTKind.Match:
|
||||
_WalkChecks(pool, doc, a, ws, table_pool, s.children, errors)
|
||||
mt: ast.Match* = ast.Match*(s)
|
||||
_WalkChecksExprs(pool, doc, a, ws, table_pool, mt.subject, errors)
|
||||
det_s: str = _SymDetailAt(pool, a, mt.subject)
|
||||
nullable_s: int = _NullableDetail(det_s)
|
||||
opt_renum: int = 0
|
||||
if det_s is not None and string.strstr(det_s, "Option[") is not None: opt_renum = 1
|
||||
has_some: int = 0
|
||||
has_nil: int = 0
|
||||
has_wild: int = 0
|
||||
if mt.cases is not None:
|
||||
ci: int = 0
|
||||
while ci < int(len(mt.cases)):
|
||||
ck: ast.MatchCase* = (ast.MatchCase*)(mt.cases.get(sizet(ci)))
|
||||
ci += 1
|
||||
if ck is None: continue
|
||||
pk_o: int = _LspOptPatternKind(ck.pattern)
|
||||
if pk_o == 1: has_some = 1
|
||||
elif pk_o == 2: has_nil = 1
|
||||
elif pk_o == 3: has_wild = 1
|
||||
elif _LspNilPattern(ck.pattern) != 0: has_nil = 1
|
||||
if nullable_s == 0 and opt_renum == 0 and _LspNilPattern(ck.pattern) != 0:
|
||||
_EmitTypeError(pool, doc, ck.pattern,
|
||||
"`Nil` is only valid for a nullable Option match (`own T?` / `T?`) or the builtin Option renum", errors)
|
||||
_WalkChecks(pool, doc, a, ws, table_pool, ck.children, errors)
|
||||
if nullable_s != 0 or opt_renum != 0:
|
||||
miss: str = ""
|
||||
if has_some == 0: miss = "Some(...)"
|
||||
if has_nil == 0 and has_wild == 0:
|
||||
if string.strlen(miss) > 0: miss = f"{miss}, Nil"
|
||||
else: miss = "Nil"
|
||||
if string.strlen(miss) > 0:
|
||||
msg_e: bytes = pool.alloc(sizet(320))
|
||||
if msg_e is not None:
|
||||
stdio.snprintf(msg_e, sizet(320),
|
||||
"match is not exhaustive: missing branch(es): %s (add the missing `case` or `case _`)", miss)
|
||||
_EmitTypeError(pool, doc, mt, str(msg_e), errors)
|
||||
continue
|
||||
|
||||
|
||||
|
||||
@@ -65,7 +65,9 @@ def _AddDiagnostic(pool: memhub.MemManager*, arr: json.JsonValue*,
|
||||
if diag is None:
|
||||
return
|
||||
json.object_set(pool, diag, "range", rng)
|
||||
json.object_set(pool, diag, "severity", json.int_val(pool, 1))
|
||||
sev: int = e.severity
|
||||
if sev <= 0: sev = 1
|
||||
json.object_set(pool, diag, "severity", json.int_val(pool, int64(sev)))
|
||||
json.object_set(pool, diag, "source", json.string_val(pool, "viperc"))
|
||||
msg: str = e.message
|
||||
if msg is None:
|
||||
|
||||
@@ -539,8 +539,6 @@ def _LexStream(pool: memhub.MemManager*,
|
||||
typ = ST_KEYWORD
|
||||
elif tok.type == ast.TokenType.Name and tok.str_val is not None and string.strcmp(tok.str_val, "Some") == 0:
|
||||
typ = ST_CLASS
|
||||
elif tok.type == ast.TokenType.Name and tok.str_val is not None and string.strcmp(tok.str_val, "Nil") == 0:
|
||||
typ = ST_PROPERTY
|
||||
elif tok.type == ast.TokenType.Name and _IsBuiltinFunctionName(tok.str_val) != 0:
|
||||
typ = ST_FUNCTION
|
||||
end_line0: int = line0
|
||||
|
||||
Reference in new issue
Block a user