mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
* feat(kotlin): bind Spring @Value and ConfigurationProperties consumers Kotlin sources were skipping the Java-only config-binding attach path, so mixed JVM apps under-reported blast radius for Kotlin placeholders. Capture from the live AST, serialize on the existing side channel, and reuse the shared binder. (U1-U4) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(review): require exact Spring annotation FQNs and skip raw-string escapes Reject similarly named third-party imports and leave Kotlin triple-quoted bodies undecoded so capture stays fail-closed. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(kotlin): use only grammar-valid Kotlin string and class-name nodes The coverage shard failed the #1920 literal gate on invented string node types and a Java-style name field. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(kotlin): fail closed on non-literal prefixes and persist unresolved config markers Reject constant and boolean @ConfigurationProperties arguments, scope nested Value shadows to their owner, and rewrite drifted consumer files when a config key is deleted. Co-authored-by: Cursor <cursoragent@cursor.com> * test(kotlin): add config-consumer capture benchmark and fix JVM feature stamps The Kotlin capture path had no performance or behavior guard: a file-wide lexical shadow regression silently dropped two of every three facts. The new bench arm fingerprints @Value / @ConfigurationProperties facts from an explicit-import control against a wildcard-import corpus whose files each declare a sibling nested `Value` type, so parity between the arms is the regression gate, and CI runs it with scaling + widening budgets. Broadening spring.config-bindings to .kt also means Kotlin-only JVM repos now stamp the feature, which the two exact-map orchestration expectations still denied. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(kotlin): let an explicit import shadow the Spring wildcard import importedAs accepted a star import of the Spring package even when the same simple name was explicitly bound to another type, so a file importing com.example.Value alongside the Spring annotation package emitted a false @Value consumer fact. Kotlin resolves the explicit import first, so the wildcard branch now only applies when the name is otherwise unbound. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
164 lines
5.1 KiB
JavaScript
164 lines
5.1 KiB
JavaScript
/**
|
|
* Build-free throughput + identity bench for Kotlin Spring config-consumer
|
|
* capture (#2412).
|
|
*
|
|
* Arms (identical corpora except the import style):
|
|
* - exact: explicit `import ...annotation.Value` control, which resolves the
|
|
* annotation from `imports.exact` before any shadow check runs
|
|
* - wildcard: `import ...annotation.*` feature path, where every simple-name
|
|
* annotation pays the lexical local-type shadow walk. Each file also
|
|
* declares a sibling nested type named `Value` that must NOT suppress the
|
|
* Spring annotation — the file-wide-shadow regression fixed on this branch.
|
|
*
|
|
* Parsing is prepared outside the timer; the measured path is the capture
|
|
* function the Kotlin worker calls on its own AST.
|
|
*
|
|
* Usage:
|
|
* node --import tsx bench/spring-config-bindings/measure.mjs
|
|
* node --import tsx bench/spring-config-bindings/measure.mjs --check
|
|
*/
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import Parser from 'tree-sitter';
|
|
import { SupportedLanguages } from 'gitnexus-shared';
|
|
import { getLanguageGrammar } from '../../src/core/tree-sitter/parser-loader.ts';
|
|
import { captureKotlinSpringConfigConsumerFacts } from '../../src/core/ingestion/languages/kotlin/spring-config-bindings.ts';
|
|
import { fingerprintIds, minSample, runBaselineCheck } from '../lib/identity-guard.mjs';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const BASELINE_PATH = path.resolve(__dirname, 'baselines.json');
|
|
|
|
const SMALL = 250;
|
|
const LARGE = 800;
|
|
const REPS = 15;
|
|
const WARMUP = 5;
|
|
/** Two @Value properties plus one @ConfigurationProperties class per file. */
|
|
const FACTS_PER_FILE = 3;
|
|
|
|
function consumerSource(i, mode) {
|
|
const imports =
|
|
mode === 'wildcard'
|
|
? `import org.springframework.beans.factory.annotation.*
|
|
import org.springframework.boot.context.properties.*`
|
|
: `import org.springframework.beans.factory.annotation.Value
|
|
import org.springframework.boot.context.properties.ConfigurationProperties`;
|
|
|
|
return `package bench.config
|
|
${imports}
|
|
|
|
class Shadowing${i} {
|
|
class Value
|
|
}
|
|
|
|
@ConfigurationProperties(prefix = "svc.${i}")
|
|
class Props${i} {
|
|
var endpoint: String? = null
|
|
}
|
|
|
|
class Consumer${i} {
|
|
@Value("\\\${app.key${i}}")
|
|
var timeout: Int = 0
|
|
|
|
@Value("\\\${app.other${i}:5}")
|
|
var other: String? = null
|
|
|
|
fun decoy() {}
|
|
}
|
|
`;
|
|
}
|
|
|
|
/** Position-free fact identity, so both arms are directly comparable. */
|
|
function factId(fact) {
|
|
const consumer = fact.consumer;
|
|
return consumer.kind === 'value'
|
|
? `value|${consumer.fieldName}|${[...consumer.keys].sort().join(',')}`
|
|
: `configuration-properties|${consumer.className}|${consumer.prefix}`;
|
|
}
|
|
|
|
function prepare(mode, fileCount) {
|
|
const files = [];
|
|
const lang = getLanguageGrammar(SupportedLanguages.Kotlin);
|
|
for (let i = 0; i < fileCount; i++) {
|
|
const parser = new Parser();
|
|
parser.setLanguage(lang);
|
|
const filePath = `bench/${mode}/Consumer${i}.kt`;
|
|
files.push({ tree: parser.parse(consumerSource(i, mode)), filePath, parser });
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function runAll(files) {
|
|
const ids = [];
|
|
for (const f of files) {
|
|
for (const fact of captureKotlinSpringConfigConsumerFacts(f.tree.rootNode, f.filePath)) {
|
|
ids.push(factId(fact));
|
|
}
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
function measure(mode, fileCount) {
|
|
const files = prepare(mode, fileCount);
|
|
const { last, ms } = minSample(() => runAll(files), WARMUP, REPS);
|
|
return {
|
|
files: fileCount,
|
|
ms,
|
|
facts: last.length,
|
|
fingerprint: fingerprintIds(last),
|
|
};
|
|
}
|
|
|
|
function failIfNeeded(current, errors) {
|
|
if (errors.length === 0) return;
|
|
console.error(JSON.stringify({ report: current, errors }, null, 2));
|
|
process.exit(1);
|
|
}
|
|
|
|
function runFactCountCheck(current, expectedCounts) {
|
|
const errors = [];
|
|
for (const [arm, expected] of Object.entries(expectedCounts)) {
|
|
const actual = current[arm]?.facts;
|
|
if (actual !== expected) errors.push(`${arm}.facts ${String(actual)} != ${expected}`);
|
|
}
|
|
failIfNeeded(current, errors);
|
|
}
|
|
|
|
/**
|
|
* A wildcard import plus a sibling `Value` declaration must capture exactly the
|
|
* facts the explicit-import control captures.
|
|
*/
|
|
function runFingerprintParityCheck(current, leftArm, rightArm) {
|
|
const left = current[leftArm]?.fingerprint;
|
|
const right = current[rightArm]?.fingerprint;
|
|
failIfNeeded(
|
|
current,
|
|
left === right ? [] : [`${leftArm}.fingerprint ${left} != ${rightArm}.fingerprint ${right}`],
|
|
);
|
|
}
|
|
|
|
const report = {
|
|
exact_small: measure('exact', SMALL),
|
|
exact_large: measure('exact', LARGE),
|
|
wildcard_small: measure('wildcard', SMALL),
|
|
wildcard_large: measure('wildcard', LARGE),
|
|
};
|
|
report.scaling_ratio = Number(
|
|
(report.wildcard_large.ms / report.wildcard_small.ms / (LARGE / SMALL)).toFixed(3),
|
|
);
|
|
report.widening_overhead = Number(
|
|
(report.wildcard_large.ms / Math.max(report.exact_large.ms, 0.001)).toFixed(3),
|
|
);
|
|
report.fingerprint = report.wildcard_large.fingerprint;
|
|
|
|
runFactCountCheck(report, {
|
|
exact_large: LARGE * FACTS_PER_FILE,
|
|
wildcard_large: LARGE * FACTS_PER_FILE,
|
|
});
|
|
runFingerprintParityCheck(report, 'exact_large', 'wildcard_large');
|
|
|
|
if (!process.argv.includes('--check')) {
|
|
console.log(JSON.stringify(report, null, 2));
|
|
process.exit(0);
|
|
}
|
|
|
|
runBaselineCheck(report, BASELINE_PATH);
|