Skip to content

Commit 794764d

Browse files
author
Trinketer22
committed
Global file search and results caching
1 parent cacc818 commit 794764d

File tree

1 file changed

+139
-37
lines changed

1 file changed

+139
-37
lines changed

server/src/languages/func/inspections/UnusedImpure.ts

Lines changed: 139 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,40 +13,105 @@ import { asLspRange } from "@server/utils/position"
1313
import { closestNamedSibling, parentOfType, parentOfTypeWithCb } from "@server/psi/utils"
1414
import { Referent } from "@server/languages/func/psi/Referent"
1515
import { FunCBindingResolver } from "../psi/BindingResolver"
16+
import { FUNC_PARSED_FILES_CACHE } from "@server/files"
1617

1718

1819
export class UnusedImpureInspection extends UnusedInspection implements Inspection {
1920
public readonly id: "unused-impure" = InspectionIds.UNUSED_IMPURE;
2021

22+
private impureMap: Map<string, Func>;
23+
private dropableMap: Map<string, Func>;
24+
private resultsCache: Map<string, boolean>;
25+
private impureBuiltins: Set<string>;
26+
27+
constructor() {
28+
super();
29+
this.resultsCache = new Map();
30+
this.impureMap = new Map();
31+
this.dropableMap = new Map();
32+
this.impureBuiltins = new Set([
33+
"throw",
34+
"throw_if",
35+
"throw_unless",
36+
"throw_arg",
37+
"throw_arg_op",
38+
"throw_arg_unless",
39+
"~dump",
40+
"~strdump"
41+
]);
42+
}
43+
44+
private getCallDef(call: Node, mode: 'dropable' | 'impure' = 'dropable') {
45+
let callDef: Func | undefined;
46+
const lookupMap = mode == 'dropable' ? this.dropableMap : this.impureMap;
47+
const callType = call.type;
48+
if (callType == "function_application") {
49+
const funcIdentifier = call.childForFieldName("callee");
50+
if (funcIdentifier) {
51+
callDef = lookupMap.get(funcIdentifier.text);
52+
}
53+
} else if (callType == "method_call") {
54+
const funcIdentifier = call.childForFieldName("method_name");
55+
if (funcIdentifier) {
56+
const methodName = funcIdentifier.text;
57+
callDef = lookupMap.get(methodName);
58+
if (!callDef) {
59+
callDef = lookupMap.get("~" + methodName);
60+
}
61+
}
62+
} else {
63+
throw new Error(`Unsupported call type ${call}`)
64+
}
65+
66+
return callDef;
67+
}
68+
private isImpureBuiltIn(call: Node) {
69+
switch (call.type) {
70+
case "function_application":
71+
return this.impureBuiltins.has(call.childForFieldName("callee")!.text);
72+
case "method_call":
73+
return this.impureBuiltins.has(call.childForFieldName("method_name")!.text);
74+
}
75+
return false;
76+
}
77+
78+
private isCall(call: Node) {
79+
return call.type == "function_application" || call.type == "method_call";
80+
}
81+
82+
private setCache(node: Node, result: boolean) {
83+
const cacheKey = [node.startPosition.row, node.startPosition.column, node.endPosition.row, node.endPosition.column].join(':');
84+
this.resultsCache.set(cacheKey, result);
85+
}
86+
private getCache(node: Node) {
87+
const cacheKey = [node.startPosition.row, node.startPosition.column, node.endPosition.row, node.endPosition.column].join(':');
88+
return this.resultsCache.get(cacheKey);
89+
}
90+
2191
protected checkFile(file: FuncFile, diagnostics: lsp.Diagnostic[]): void {
22-
const impureMap: Map<string, Func> = new Map();
2392
// Populate impure functions map
24-
file.getFunctions().forEach(f => {
25-
if (!f.isImpure) {
26-
impureMap.set(f.name(true), f);
27-
}
28-
});
93+
FUNC_PARSED_FILES_CACHE.forEach(parsedFile => {
94+
parsedFile.getFunctions().forEach(f => {
95+
if (f.isImpure) {
96+
this.impureMap.set(f.name(true), f);
97+
} else {
98+
this.dropableMap.set(f.name(true), f);
99+
}
100+
});
101+
})
29102
const bindResolver = new FunCBindingResolver(file);
30103
RecursiveVisitor.visit(file.rootNode, (node): boolean => {
31-
let droppableDef: Func | undefined;
32-
33-
if (node.type == "function_application") {
34-
const funcIdentifier = node.childForFieldName("callee");
35-
if (funcIdentifier) {
36-
droppableDef = impureMap.get(funcIdentifier.text);
37-
}
38-
} else if (node.type == "method_call") {
39-
const funcIdentifier = node.childForFieldName("method_name");
40-
if (funcIdentifier) {
41-
const methodName = funcIdentifier.text;
42-
droppableDef = impureMap.get(methodName);
43-
if (!droppableDef) {
44-
droppableDef = impureMap.get("~" + methodName);
45-
}
46-
}
104+
if (!this.isCall(node)) {
105+
return true;
47106
}
48-
49-
if (droppableDef && this.checkCallWillDrop(node, droppableDef, file, bindResolver)) {
107+
let willDrop = false;
108+
// Skip impure builtins calls
109+
if (this.isImpureBuiltIn(node)) {
110+
return true;
111+
}
112+
// const droppableDef = this.getCallDef(node)
113+
if (this.checkCallWillDrop(node, file, bindResolver)) {
114+
willDrop = true;
50115
const range = asLspRange(node);
51116
diagnostics.push({
52117
severity: lsp.DiagnosticSeverity.Error,
@@ -55,11 +120,26 @@ export class UnusedImpureInspection extends UnusedInspection implements Inspecti
55120
source: "func"
56121
})
57122
}
123+
this.setCache(node, willDrop);
58124
return true;
59125
})
60126
}
61127

62-
private checkCallWillDrop(node: Node, definition: Func, file: FuncFile, bindResolver: FunCBindingResolver) {
128+
private checkCallWillDrop(node: Node, file: FuncFile, bindResolver: FunCBindingResolver) {
129+
const cachedRes = this.getCache(node);
130+
if (cachedRes !== undefined) {
131+
return cachedRes;
132+
}
133+
134+
const definition = this.getCallDef(node, 'dropable')
135+
136+
if (!definition) {
137+
// If no dropable def found, check that impure is implicit just in case
138+
const willDrop = !(this.getCallDef(node, 'impure') || this.isImpureBuiltIn(node))
139+
this.setCache(node, willDrop);
140+
return willDrop;
141+
}
142+
63143
const returnExp = definition.returnType();
64144
if (returnExp !== null) {
65145
// If return type of a function is empty tensor - check no more.
@@ -81,17 +161,24 @@ export class UnusedImpureInspection extends UnusedInspection implements Inspecti
81161
"repeat_statement",
82162
"return_statement"
83163
);
164+
if (!expressionParent) {
165+
// Could happen in incomplete code
166+
return false;
167+
}
168+
const parentType = expressionParent.parent.type;
84169
// If call is in the block_statement of any kind, it will be a child of expression_statement
85170
// Otherwise it is in condition block of if/while/do while
86171
// Or in arguments clause of other function_application/method_call
87-
if (!expressionParent || expressionParent.parent.type !== "expression_statement") {
172+
if (parentType !== "expression_statement") {
173+
if (parentType == "function_application" || parentType == "method_call") {
174+
return this.checkCallWillDrop(expressionParent.parent, file, bindResolver)
175+
}
88176
// If expression is in condition or return statement it will not be dropped
89177
return false;
90178
}
91179

92180
// We are in the expression expression_statement
93-
// Closest previous sibling got to be lvalue expression
94-
// (identifier/tensor_expression/tuple_expression)
181+
// Bind the values from the expression
95182
const resolvedBinding = bindResolver.resolve(expressionParent.parent);
96183
// If no lvalue, non-impure call will drop
97184
if (resolvedBinding.bindings.size == 0) {
@@ -100,12 +187,12 @@ export class UnusedImpureInspection extends UnusedInspection implements Inspecti
100187
// If no identifiers referenced in lvalue, means those are whole type and will be dropped
101188
// const affectedIdentifiers = resolvedBinding.bindings.values()
102189

103-
for (let refValue of resolvedBinding.bindings.values()) {
104-
if (!refValue) {
105-
continue;
106-
}
107-
const references = new Referent(refValue.identifier, file).findReferences({}) // we need at least one reference
108-
// Has to be referenced in call, conditional or return statement;
190+
for (let boundValue of resolvedBinding.bindings.values()) {
191+
// Find references to the bound variables from below the current expression.
192+
const references = new Referent(boundValue.identifier, file).findReferences({ limit: Infinity }).filter(
193+
ref => ref.node.startIndex >= expressionParent.parent.endIndex
194+
);
195+
// Has to be referenced in non impure call, conditional or return statement to not drop
109196
for (let ref of references) {
110197
const parent = parentOfType(ref.node,
111198
"expression_statement", // But don't go above expression_statement
@@ -117,12 +204,27 @@ export class UnusedImpureInspection extends UnusedInspection implements Inspecti
117204
"repeat_statement",
118205
"return_statement"
119206
)
120-
if (parent && parent.type !== "expression_statement") {
121-
return false;
207+
if (!parent) {
208+
continue;
209+
}
210+
if (parent.type !== "expression_statement") {
211+
let willDrop = false;
212+
if (this.isCall(parent)) {
213+
willDrop = this.checkCallWillDrop(parent, file, bindResolver)
214+
this.setCache(parent, willDrop);
215+
}
216+
return willDrop;
217+
}
218+
// Check reference in method call
219+
const refSibling = closestNamedSibling(ref.node, 'next', (sibl => sibl.type == "method_call"))
220+
if (refSibling) {
221+
// If this is a droppable call, go to next ref, else expression is not droppable
222+
if (!this.checkCallWillDrop(refSibling, file, bindResolver)) {
223+
return false;
224+
}
122225
}
123226
}
124227
}
125-
126228
return true;
127229
}
128230
}

0 commit comments

Comments
 (0)