Skip to content

Commit 1697da2

Browse files
authored
fix(tolk/inspections): add NeedNotNullUnwrapping inspection (#72)
Fixes #50
1 parent 895c7dc commit 1697da2

File tree

6 files changed

+194
-1
lines changed

6 files changed

+194
-1
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,8 @@
434434
"deprecated-symbol-usage",
435435
"unused-import",
436436
"struct-initialization",
437-
"cannot-reassign"
437+
"cannot-reassign",
438+
"need-not-null-unwrapping"
438439
]
439440
},
440441
"default": [
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
========================================================================
2+
Simple nullable builtin type
3+
========================================================================
4+
fun int.someMethod(self) {}
5+
6+
fun main(value: int?) {
7+
value.someMethod();
8+
}
9+
------------------------------------------------------------------------
10+
3 0:8 to 0:18 Method 'someMethod' is never used (tolk)
11+
0 3:10 to 3:20 Cannot call method `someMethod` on nullable type `int?`, you need to unwrap it with `!` or explicitly check for `value != null` (tolk)
12+
13+
========================================================================
14+
Simple nullable builtin type, fixed
15+
========================================================================
16+
fun int.someMethod(self) {}
17+
18+
fun main(value: int?) {
19+
value!.someMethod();
20+
}
21+
------------------------------------------------------------------------
22+
no issues
23+
24+
========================================================================
25+
Simple nullable builtin type, fixed 2
26+
========================================================================
27+
fun int.someMethod(self) {}
28+
29+
fun main(value: int?) {
30+
if (value == null) {
31+
return
32+
}
33+
value.someMethod();
34+
}
35+
------------------------------------------------------------------------
36+
no issues
37+
38+
========================================================================
39+
Simple nullable builtin type, fixed 3
40+
========================================================================
41+
fun int.someMethod(self) {}
42+
43+
fun main(value: int?) {
44+
if (value != null) {
45+
value.someMethod();
46+
}
47+
}
48+
------------------------------------------------------------------------
49+
no issues
50+
51+
========================================================================
52+
Tensor nullable builtin type
53+
========================================================================
54+
fun (int, slice).someMethod(self) {}
55+
56+
fun main(value: (int, slice)?) {
57+
value.someMethod();
58+
}
59+
------------------------------------------------------------------------
60+
3 0:17 to 0:27 Method 'someMethod' is never used (tolk)
61+
0 3:10 to 3:20 Cannot call method `someMethod` on nullable type `(int, slice)?`, you need to unwrap it with `!` or explicitly check for `value != null` (tolk)
62+
63+
========================================================================
64+
Simple nullable builtin type without methods
65+
========================================================================
66+
fun main(value: int?) {
67+
value.someMethod();
68+
}
69+
------------------------------------------------------------------------
70+
no issues
71+
72+
========================================================================
73+
Simple nullable builtin type with several methods
74+
========================================================================
75+
fun (int, slice).someMethod(self) {}
76+
fun T.someMethod(self) {}
77+
78+
fun main(value: int?) {
79+
value.someMethod();
80+
}
81+
------------------------------------------------------------------------
82+
3 0:17 to 0:27 Method 'someMethod' is never used (tolk)
83+
84+
========================================================================
85+
Simple nullable builtin type with complex qualifier
86+
========================================================================
87+
fun int.someMethod(self) {}
88+
89+
fun getData(): int? { return null }
90+
91+
fun main() {
92+
getData().someMethod();
93+
}
94+
------------------------------------------------------------------------
95+
3 0:8 to 0:18 Method 'someMethod' is never used (tolk)
96+
0 5:14 to 5:24 Cannot call method `someMethod` on nullable type `int?`, you need to unwrap it with `!` or explicitly check for `getData() != null` (tolk)

server/src/languages/tolk/inspections/Inspection.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const InspectionIds = {
1313
STRUCT_INITIALIZATION: "struct-initialization",
1414
TYPE_COMPATIBILITY: "type-compatibility",
1515
CANNOT_REASSIGN: "cannot-reassign",
16+
NEED_NOT_NULL_UNWRAPPING: "need-not-null-unwrapping",
1617
} as const
1718

1819
export type InspectionId = (typeof InspectionIds)[keyof typeof InspectionIds]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright © 2025 TON Core
3+
import * as lsp from "vscode-languageserver"
4+
import type {TolkFile} from "@server/languages/tolk/psi/TolkFile"
5+
import {asLspPosition, asLspRange} from "@server/utils/position"
6+
import {RecursiveVisitor} from "@server/visitor/visitor"
7+
import {Inspection, InspectionIds} from "./Inspection"
8+
import {inferenceOf, methodCandidates} from "@server/languages/tolk/type-inference"
9+
import {CallLike} from "@server/languages/tolk/psi/TolkNode"
10+
import {Ty, UnknownTy, UnionTy} from "@server/languages/tolk/types/ty"
11+
import {Node as SyntaxNode} from "web-tree-sitter"
12+
import {FileDiff} from "@server/utils/FileDiff"
13+
14+
export class NeedNotNullUnwrappingInspection implements Inspection {
15+
public readonly id: "need-not-null-unwrapping" = InspectionIds.NEED_NOT_NULL_UNWRAPPING
16+
17+
public inspect(file: TolkFile): lsp.Diagnostic[] {
18+
if (file.fromStdlib) return []
19+
const diagnostics: lsp.Diagnostic[] = []
20+
21+
RecursiveVisitor.visit(file.rootNode, node => {
22+
if (node.type !== "function_call") return true
23+
const call = new CallLike(node, file)
24+
const calleeName = call.calleeName()
25+
if (!calleeName) return true
26+
27+
const inference = inferenceOf(node, file)
28+
if (!inference) return true
29+
30+
const resolved = inference.resolve(calleeName)
31+
if (resolved) return true // already resolved, all okay
32+
33+
const qualifier = call.calleeQualifier()
34+
if (!qualifier) return true // simple call like `foo()`
35+
36+
const qualifierType = inference.typeOf(qualifier)
37+
if (!qualifierType) return true
38+
39+
if (qualifierType instanceof UnionTy && qualifierType.asNullable()) {
40+
// try to find methods for unwrapped T?
41+
const asNullable =
42+
qualifierType.asNullable() ??
43+
([UnknownTy.UNKNOWN as Ty, UnknownTy.UNKNOWN as Ty] as const)
44+
const innerTy = asNullable[0]
45+
46+
const methods = methodCandidates(inference.ctx, innerTy, calleeName.text)
47+
if (methods.length === 0) return true // no options for this type
48+
49+
diagnostics.push({
50+
severity: lsp.DiagnosticSeverity.Error,
51+
range: asLspRange(calleeName),
52+
message: `Cannot call method \`${calleeName.text}\` on nullable type \`${qualifierType.name()}\`, you need to unwrap it with \`!\` or explicitly check for \`${qualifier.text} != null\``,
53+
source: "tolk",
54+
data: this.unwrapWithNotNull(file, qualifier),
55+
})
56+
}
57+
58+
return true
59+
})
60+
61+
return diagnostics
62+
}
63+
64+
private unwrapWithNotNull(file: TolkFile, qualifier: SyntaxNode): undefined | lsp.CodeAction {
65+
const diff = FileDiff.forFile(file.uri)
66+
67+
diff.appendTo(asLspPosition(qualifier.endPosition), "!")
68+
69+
const edit = diff.toWorkspaceEdit()
70+
return {
71+
edit,
72+
title: `Unwrap with \`!\` (unsafe)`,
73+
isPreferred: true,
74+
}
75+
}
76+
}

server/src/languages/tolk/inspections/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {TypeCompatibilityInspection} from "@server/languages/tolk/inspections/Ty
1111
import {CannotReassignInspection} from "@server/languages/tolk/inspections/CannotReassignInspection"
1212
import {UnusedTopLevelDeclarationInspection} from "@server/languages/tolk/inspections/UnusedTopLevelDeclarationInspection"
1313
import {UnusedTypeParameterInspection} from "@server/languages/tolk/inspections/UnusedTypeParameterInspection"
14+
import {NeedNotNullUnwrappingInspection} from "@server/languages/tolk/inspections/NeedNotNullUnwrappingInspection"
1415

1516
export async function runTolkInspections(
1617
uri: string,
@@ -27,6 +28,7 @@ export async function runTolkInspections(
2728
new StructInitializationInspection(),
2829
new TypeCompatibilityInspection(),
2930
new CannotReassignInspection(),
31+
new NeedNotNullUnwrappingInspection(),
3032
]
3133

3234
const settings = await getDocumentSettings(uri)

server/src/languages/tolk/type-inference.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,15 @@ class InferenceWalker {
363363

364364
public constructor(public ctx: InferenceContext) {}
365365

366+
public static methodCandidates(
367+
ctx: InferenceContext,
368+
qualifierTy: Ty,
369+
searchName: string,
370+
): MethodBase[] {
371+
const walker = new InferenceWalker(ctx)
372+
return walker.methodCandidates(qualifierTy, searchName)
373+
}
374+
366375
public inferConstant(constant: Constant, flow: FlowContext): FlowContext {
367376
const expression = constant.value()?.node
368377
if (!expression) return flow
@@ -2659,3 +2668,11 @@ export function functionTypeOf(func: FunctionBase): FuncTy | null {
26592668
}
26602669
return null
26612670
}
2671+
2672+
export function methodCandidates(
2673+
ctx: InferenceContext,
2674+
qualifierTy: Ty,
2675+
searchName: string,
2676+
): MethodBase[] {
2677+
return InferenceWalker.methodCandidates(ctx, qualifierTy, searchName)
2678+
}

0 commit comments

Comments
 (0)