@@ -63,49 +63,241 @@ public static void assertNoLeakedHashtableNativeCalls(String luaCode) {
6363 }
6464 }
6565
66+ /**
67+ * Collects all function names that appear as CALLS in the Lua source.
68+ *
69+ * Skips string literals, comments, and function declaration names (including
70+ * method-syntax declarations like {@code function Foo:bar()} or
71+ * {@code function Foo.bar()}) to avoid false positives.
72+ */
6673 static Set <String > collectCalledFunctionNames (String text ) {
6774 Set <String > result = new HashSet <>();
6875 int length = text .length ();
6976 int index = 0 ;
7077 while (index < length ) {
71- if (!isIdentifierStart (text .charAt (index ))) {
72- index ++;
78+ char ch = text .charAt (index );
79+
80+ // Skip Lua comments: -- short or --[[ long ]]
81+ if (ch == '-' && index + 1 < length && text .charAt (index + 1 ) == '-' ) {
82+ int longLevel = countLongBracketLevel (text , index + 2 );
83+ if (longLevel >= 0 ) {
84+ index = skipLongString (text , index + 2 , longLevel );
85+ } else {
86+ // Short comment: skip to end of line
87+ while (index < length && text .charAt (index ) != '\n' ) {
88+ index ++;
89+ }
90+ }
7391 continue ;
7492 }
75- int end = scanIdentifierEnd (text , index + 1 );
76- int next = skipWhitespace (text , end );
77- if (next < length && text .charAt (next ) == '(' ) {
78- result .add (text .substring (index , end ));
93+
94+ // Skip string literals: "..." or '...'
95+ if (ch == '"' || ch == '\'' ) {
96+ index = skipQuotedString (text , index , ch );
97+ continue ;
98+ }
99+
100+ // Skip long strings: [[...]] or [=[...]=]
101+ if (ch == '[' ) {
102+ int longLevel = countLongBracketLevel (text , index );
103+ if (longLevel >= 0 ) {
104+ index = skipLongString (text , index , longLevel );
105+ continue ;
106+ }
79107 }
80- index = end ;
108+
109+ // Skip function declarations: after the 'function' keyword the name tokens
110+ // (including A.B or A:B method syntax) are NOT calls.
111+ if (matchesWord (text , index , "function" )) {
112+ index = skipFunctionDeclarationName (text , index + "function" .length ());
113+ continue ;
114+ }
115+
116+ // Check identifier followed by '(' → function call
117+ if (isIdentifierStart (ch )) {
118+ int end = scanIdentifierEnd (text , index + 1 );
119+ int next = skipWhitespace (text , end );
120+ if (next < length && text .charAt (next ) == '(' ) {
121+ result .add (text .substring (index , end ));
122+ }
123+ index = end ;
124+ continue ;
125+ }
126+
127+ index ++;
81128 }
82129 return result ;
83130 }
84131
132+ /**
133+ * Collects function names that appear as DEFINITIONS in the Lua source.
134+ *
135+ * Handles both simple ({@code function name(}) and method-syntax
136+ * ({@code function A:name(} or {@code function A.name(}) declarations.
137+ * Skips string literals and comments.
138+ */
85139 static Set <String > collectDefinedFunctionNames (String text ) {
86140 Set <String > result = new HashSet <>();
87141 int length = text .length ();
88142 int index = 0 ;
89143 while (index < length ) {
144+ char ch = text .charAt (index );
145+
146+ // Skip comments
147+ if (ch == '-' && index + 1 < length && text .charAt (index + 1 ) == '-' ) {
148+ int longLevel = countLongBracketLevel (text , index + 2 );
149+ if (longLevel >= 0 ) {
150+ index = skipLongString (text , index + 2 , longLevel );
151+ } else {
152+ while (index < length && text .charAt (index ) != '\n' ) {
153+ index ++;
154+ }
155+ }
156+ continue ;
157+ }
158+
159+ // Skip string literals
160+ if (ch == '"' || ch == '\'' ) {
161+ index = skipQuotedString (text , index , ch );
162+ continue ;
163+ }
164+ if (ch == '[' ) {
165+ int longLevel = countLongBracketLevel (text , index );
166+ if (longLevel >= 0 ) {
167+ index = skipLongString (text , index , longLevel );
168+ continue ;
169+ }
170+ }
171+
90172 if (!matchesWord (text , index , "function" )) {
91173 index ++;
92174 continue ;
93175 }
94- int nameStart = skipWhitespace (text , index + "function" .length ());
95- if (nameStart >= length || !isIdentifierStart (text .charAt (nameStart ))) {
176+
177+ // Skip past 'function', then scan the name
178+ int pos = skipWhitespace (text , index + "function" .length ());
179+ if (pos >= length || !isIdentifierStart (text .charAt (pos ))) {
96180 index ++;
97181 continue ;
98182 }
99- int nameEnd = scanIdentifierEnd (text , nameStart + 1 );
100- int next = skipWhitespace (text , nameEnd );
101- if (next < length && text .charAt (next ) == '(' ) {
102- result .add (text .substring (nameStart , nameEnd ));
183+
184+ // Walk A.B.C or A:B chains, keeping track of the last identifier
185+ String lastName = null ;
186+ while (pos < length && isIdentifierStart (text .charAt (pos ))) {
187+ int nameEnd = scanIdentifierEnd (text , pos + 1 );
188+ lastName = text .substring (pos , nameEnd );
189+ pos = nameEnd ;
190+ if (pos < length && (text .charAt (pos ) == '.' || text .charAt (pos ) == ':' )) {
191+ pos ++; // consume '.' or ':'
192+ } else {
193+ break ;
194+ }
195+ }
196+
197+ int next = skipWhitespace (text , pos );
198+ if (lastName != null && next < length && text .charAt (next ) == '(' ) {
199+ result .add (lastName );
103200 }
104- index = nameEnd ;
201+ index = pos ;
105202 }
106203 return result ;
107204 }
108205
206+ /**
207+ * After the {@code function} keyword, skip past the declaration name
208+ * (which may include {@code A.B} or {@code A:B} qualifiers) and return
209+ * the position after the opening {@code (}.
210+ *
211+ * If there is no valid name, returns the position just after the keyword.
212+ */
213+ private static int skipFunctionDeclarationName (String text , int index ) {
214+ int length = text .length ();
215+ int pos = skipWhitespace (text , index );
216+
217+ if (pos >= length || !isIdentifierStart (text .charAt (pos ))) {
218+ // Anonymous function: 'function(' — no name to skip
219+ return pos ;
220+ }
221+
222+ // Walk A.B.C or A:B chains
223+ while (pos < length && isIdentifierStart (text .charAt (pos ))) {
224+ pos = scanIdentifierEnd (text , pos + 1 );
225+ if (pos < length && (text .charAt (pos ) == '.' || text .charAt (pos ) == ':' )) {
226+ pos ++; // consume '.' or ':'
227+ } else {
228+ break ;
229+ }
230+ }
231+
232+ // Skip to just after '(' so the outer loop doesn't re-examine the '('
233+ pos = skipWhitespace (text , pos );
234+ if (pos < length && text .charAt (pos ) == '(' ) {
235+ pos ++;
236+ }
237+ return pos ;
238+ }
239+
240+ /**
241+ * Returns the long-bracket level of a {@code [=..=[} opener at {@code index},
242+ * or -1 if there is no valid long-bracket opener at that position.
243+ */
244+ private static int countLongBracketLevel (String text , int index ) {
245+ int length = text .length ();
246+ if (index >= length || text .charAt (index ) != '[' ) {
247+ return -1 ;
248+ }
249+ int level = 0 ;
250+ int pos = index + 1 ;
251+ while (pos < length && text .charAt (pos ) == '=' ) {
252+ level ++;
253+ pos ++;
254+ }
255+ if (pos < length && text .charAt (pos ) == '[' ) {
256+ return level ;
257+ }
258+ return -1 ;
259+ }
260+
261+ /**
262+ * Skips past a long string starting with {@code [=..=[} at {@code index}.
263+ * The {@code level} is the number of {@code =} signs in the bracket.
264+ * Returns the index after the closing {@code ]=..=]}.
265+ */
266+ private static int skipLongString (String text , int index , int level ) {
267+ int length = text .length ();
268+ // Skip the opening bracket [=..=[ (1 + level + 1 chars)
269+ int pos = index + 1 + level + 1 ;
270+ String close = "]" + "=" .repeat (level ) + "]" ;
271+ int closeIdx = text .indexOf (close , pos );
272+ if (closeIdx < 0 ) {
273+ return length ;
274+ }
275+ return closeIdx + close .length ();
276+ }
277+
278+ /**
279+ * Skips a quoted string starting at {@code index} with quote character {@code quote}.
280+ * Handles backslash escapes. Returns the index after the closing quote.
281+ */
282+ private static int skipQuotedString (String text , int index , char quote ) {
283+ int length = text .length ();
284+ int pos = index + 1 ; // skip opening quote
285+ while (pos < length ) {
286+ char ch = text .charAt (pos );
287+ if (ch == '\\' ) {
288+ pos += 2 ; // skip escaped character
289+ } else if (ch == quote ) {
290+ return pos + 1 ;
291+ } else if (ch == '\n' ) {
292+ // Unfinished string literal — treat as ended
293+ return pos ;
294+ } else {
295+ pos ++;
296+ }
297+ }
298+ return pos ;
299+ }
300+
109301 private static int skipWhitespace (String text , int index ) {
110302 while (index < text .length () && Character .isWhitespace (text .charAt (index ))) {
111303 index ++;
0 commit comments