@@ -13,40 +13,105 @@ import { asLspRange } from "@server/utils/position"
1313import { closestNamedSibling , parentOfType , parentOfTypeWithCb } from "@server/psi/utils"
1414import { Referent } from "@server/languages/func/psi/Referent"
1515import { FunCBindingResolver } from "../psi/BindingResolver"
16+ import { FUNC_PARSED_FILES_CACHE } from "@server/files"
1617
1718
1819export 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