feat(lsp): flag direct dot access on nullable values

Attribute reads now check the base symbol detail (`T?` annotation) and emit the same error as the compiler when `?.` is missing. `?.` and pattern-bound non-null captures stay clean.
This commit is contained in:
TermiNexus committed 2026-11-03 15:41:14 +08:00
1 parent 1911f70818
commit 2781ba7466
1 file changed
+53 -1
+53 -1
View File
@@ -989,6 +989,48 @@ def _CheckAssignValue(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
_EmitTypeError(pool, doc, value, msg, errors)
def _NullableDetail(det: str) -> int:
"""1 when an annotation text is the nullable `T?` form."""
if det is None: return 0
if string.strchr(det, '?') is not None: return 1
return 0
def _SymDetailAt(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*,
expr: ast.AST*) -> str:
"""Resolved symbol detail (annotation text) at a Name/Attribute expression."""
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:
if o.sym is not None: return o.sym.detail
return None
return None
def _WalkChecksExprs(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
a: LspSymbols.LspAnalysis*, ws: LspWorkspace.LspWorkspace*,
table_pool: memhub.MemManager*, e: ast.AST*,
@@ -1041,7 +1083,17 @@ def _WalkChecksExprs(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
i += 1
return
if k == ast.ASTKind.Attribute:
_WalkChecksExprs(pool, doc, a, ws, table_pool, ast.Attribute*(e).value, errors)
at_c: ast.Attribute* = ast.Attribute*(e)
if at_c.optional == 0 and at_c.attr is not None:
det_b: str = _SymDetailAt(pool, a, at_c.value)
if _NullableDetail(det_b) != 0:
msg_b: bytes = pool.alloc(sizet(320))
if msg_b is not None:
stdio.snprintf(msg_b, sizet(320),
"cannot use '.%s' on a nullable value: it may be None; use `?.` or check it against None first",
at_c.attr)
_EmitTypeError(pool, doc, e, str(msg_b), errors)
_WalkChecksExprs(pool, doc, a, ws, table_pool, at_c.value, errors)
return
if k == ast.ASTKind.Subscript:
sb: ast.Subscript* = ast.Subscript*(e)