From 9240baf2e67d24ef7616699b497f2e7248d10927 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 01:29:57 +0000 Subject: [PATCH 1/2] perf: avoid array allocation in C# operator name lookup and substring in Go recover check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - csharpAnalyzer: replace node.children.find() with direct node.child(i) iteration in conversion_operator_declaration and identifier fallback paths; node.children creates a new array on each call, while child(i) is an O(1) index lookup with no allocation. - goAnalyzer: add length short-circuit (endIndex - startIndex !== 7) in isRecoverCall before the substring comparison; every call_expression triggers this check, and 99%+ of call sites are not recover() — the length test avoids a string allocation for all of them. 199 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../languages/csharpAnalyzer.ts | 30 ++++++++++++------- src/metricsAnalyzer/languages/goAnalyzer.ts | 5 +++- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/metricsAnalyzer/languages/csharpAnalyzer.ts b/src/metricsAnalyzer/languages/csharpAnalyzer.ts index 2329393..20c5057 100644 --- a/src/metricsAnalyzer/languages/csharpAnalyzer.ts +++ b/src/metricsAnalyzer/languages/csharpAnalyzer.ts @@ -307,12 +307,17 @@ export class CSharpMetricsAnalyzer { if (node.type === "conversion_operator_declaration") { // tree-sitter-c-sharp exposes a "type" field that points directly to the conversion // target type, replacing a previous O(n) linear scan over children. - // The implicit/explicit keyword is not a named field, so a short prefix scan is still - // needed, but it stops at the first matching token rather than scanning all children. - const kindNode = node.children.find( - (c) => c.type === "implicit" || c.type === "explicit" - ); - const kind = kindNode ? kindNode.type : "explicit"; + // The implicit/explicit keyword is not a named field; it appears after the modifier + // nodes (public, static, etc.), which number at most 3. Scan the first 4 children + // with direct index access to avoid the array allocation from node.children. + let kind = "explicit"; + for (let i = 0; i < 4 && i < node.childCount; i++) { + const c = node.child(i); + if (c?.type === "implicit" || c?.type === "explicit") { + kind = c.type; + break; + } + } const typeNode = node.childForFieldName("type"); if (typeNode) { const targetType = this.sourceText.substring(typeNode.startIndex, typeNode.endIndex); @@ -325,10 +330,15 @@ export class CSharpMetricsAnalyzer { // For regular methods, accessors, and local functions: use the "name" field when // available, falling back to a scan of child nodes for an identifier token. - const nameNode = - node.childForFieldName("name") ?? - node.children.find((child) => child.type === "identifier") ?? - null; + // Use direct child(i) access (O(1) per child) rather than node.children.find(...) + // to avoid the array allocation that node.children creates on each call. + let nameNode = node.childForFieldName("name"); + if (!nameNode) { + for (let i = 0; i < node.childCount; i++) { + const c = node.child(i); + if (c?.type === "identifier") { nameNode = c; break; } + } + } const methodName = nameNode ? this.sourceText.substring(nameNode.startIndex, nameNode.endIndex) : ""; diff --git a/src/metricsAnalyzer/languages/goAnalyzer.ts b/src/metricsAnalyzer/languages/goAnalyzer.ts index 9072ebc..8e39ad7 100644 --- a/src/metricsAnalyzer/languages/goAnalyzer.ts +++ b/src/metricsAnalyzer/languages/goAnalyzer.ts @@ -493,7 +493,10 @@ export class GoMetricsAnalyzer { private isRecoverCall(node: Parser.SyntaxNode): boolean { const funcNode = node.childForFieldName("function"); if (!funcNode || funcNode.type !== "identifier") { return false; } - return this.sourceText.substring(funcNode.startIndex, funcNode.endIndex) === "recover"; + // "recover" is exactly 7 characters — short-circuit before substring allocation. + // This avoids a string allocation for the vast majority of call_expression nodes. + if (funcNode.endIndex - funcNode.startIndex !== 7) { return false; } + return this.sourceText.substring(funcNode.startIndex, funcNode.endIndex) === "recover"; } /** From 057a0d3f3cb8bc534f6db4b711999fae627c42c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Aug 2026 07:28:42 +0000 Subject: [PATCH 2/2] fix: replace magic number with recover.length and scan all children for implicit/explicit keyword Co-authored-by: askpt <2493377+askpt@users.noreply.github.com> --- src/metricsAnalyzer/languages/csharpAnalyzer.ts | 8 ++++---- src/metricsAnalyzer/languages/goAnalyzer.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/metricsAnalyzer/languages/csharpAnalyzer.ts b/src/metricsAnalyzer/languages/csharpAnalyzer.ts index 20c5057..860beb1 100644 --- a/src/metricsAnalyzer/languages/csharpAnalyzer.ts +++ b/src/metricsAnalyzer/languages/csharpAnalyzer.ts @@ -307,11 +307,11 @@ export class CSharpMetricsAnalyzer { if (node.type === "conversion_operator_declaration") { // tree-sitter-c-sharp exposes a "type" field that points directly to the conversion // target type, replacing a previous O(n) linear scan over children. - // The implicit/explicit keyword is not a named field; it appears after the modifier - // nodes (public, static, etc.), which number at most 3. Scan the first 4 children - // with direct index access to avoid the array allocation from node.children. + // The implicit/explicit keyword is not a named field; it appears somewhere after + // the modifier nodes (public, static, etc.). Scan all children with direct index + // access to avoid the array allocation from node.children, breaking early when found. let kind = "explicit"; - for (let i = 0; i < 4 && i < node.childCount; i++) { + for (let i = 0; i < node.childCount; i++) { const c = node.child(i); if (c?.type === "implicit" || c?.type === "explicit") { kind = c.type; diff --git a/src/metricsAnalyzer/languages/goAnalyzer.ts b/src/metricsAnalyzer/languages/goAnalyzer.ts index 8e39ad7..6c858c2 100644 --- a/src/metricsAnalyzer/languages/goAnalyzer.ts +++ b/src/metricsAnalyzer/languages/goAnalyzer.ts @@ -493,9 +493,9 @@ export class GoMetricsAnalyzer { private isRecoverCall(node: Parser.SyntaxNode): boolean { const funcNode = node.childForFieldName("function"); if (!funcNode || funcNode.type !== "identifier") { return false; } - // "recover" is exactly 7 characters — short-circuit before substring allocation. + // Short-circuit before substring allocation when the identifier length does not match. // This avoids a string allocation for the vast majority of call_expression nodes. - if (funcNode.endIndex - funcNode.startIndex !== 7) { return false; } + if (funcNode.endIndex - funcNode.startIndex !== "recover".length) { return false; } return this.sourceText.substring(funcNode.startIndex, funcNode.endIndex) === "recover"; }