From e8fdc2e2abf9561d6629c5ec6add77c84ea2cfb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 04:42:39 +0100 Subject: [PATCH 01/24] chore(deps)(deps-dev): bump @babel/generator in /gitnexus (#2519) --- gitnexus/package-lock.json | 69 +++++++++++++++++++------------------- gitnexus/package.json | 2 +- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index b73eaf40c..a1d60c68f 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -50,7 +50,7 @@ "gitnexus": "dist/cli/index.js" }, "devDependencies": { - "@babel/generator": "^7.29.7", + "@babel/generator": "^8.0.0", "@babel/parser": "^8.0.0", "@babel/traverse": "^8.0.0", "@babel/types": "^7.29.7", @@ -107,36 +107,55 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", + "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", + "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" }, "engines": { - "node": ">=6.9.0" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/generator/node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "node_modules/@babel/generator/node_modules/@babel/helper-string-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/types": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" + "@babel/helper-string-parser": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4" }, "engines": { - "node": ">=6.0.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/helper-globals": { @@ -287,24 +306,6 @@ "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", - "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^8.0.0", - "@babel/types": "^8.0.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "@types/jsesc": "^2.5.0", - "jsesc": "^3.0.2" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, "node_modules/@babel/traverse/node_modules/@babel/helper-string-parser": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", diff --git a/gitnexus/package.json b/gitnexus/package.json index d1ae23b56..68de5143a 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -97,7 +97,7 @@ "onnxruntime-node": "^1.24.0" }, "devDependencies": { - "@babel/generator": "^7.29.7", + "@babel/generator": "^8.0.0", "@babel/parser": "^8.0.0", "@babel/traverse": "^8.0.0", "@babel/types": "^7.29.7", From 527ea5fc7a701012188f8edc12dc58efd2876803 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 05:26:54 +0100 Subject: [PATCH 02/24] chore(deps)(deps-dev): bump @babel/types in /gitnexus (#2518) --- gitnexus/package-lock.json | 206 +++++++++---------------------------- gitnexus/package.json | 2 +- 2 files changed, 48 insertions(+), 160 deletions(-) diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index a1d60c68f..a8708821a 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -53,7 +53,7 @@ "@babel/generator": "^8.0.0", "@babel/parser": "^8.0.0", "@babel/traverse": "^8.0.0", - "@babel/types": "^7.29.7", + "@babel/types": "^8.0.0", "@types/busboy": "^1.5.4", "@types/cli-progress": "^3.11.6", "@types/cors": "^2.8.17", @@ -96,16 +96,6 @@ "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/code-frame/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", - "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, "node_modules/@babel/generator": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", @@ -124,40 +114,6 @@ "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/generator/node_modules/@babel/helper-string-parser": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", - "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", - "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/generator/node_modules/@babel/types": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", - "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^8.0.0", - "@babel/helper-validator-identifier": "^8.0.4" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, "node_modules/@babel/helper-globals": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.0.tgz", @@ -169,23 +125,23 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/parser": { @@ -204,40 +160,6 @@ "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/parser/node_modules/@babel/helper-string-parser": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", - "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/parser/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", - "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/parser/node_modules/@babel/types": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", - "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^8.0.0", - "@babel/helper-validator-identifier": "^8.0.4" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, "node_modules/@babel/template": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz", @@ -253,40 +175,6 @@ "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/template/node_modules/@babel/helper-string-parser": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", - "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/template/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", - "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/template/node_modules/@babel/types": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", - "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^8.0.0", - "@babel/helper-validator-identifier": "^8.0.4" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, "node_modules/@babel/traverse": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.0.tgz", @@ -306,54 +194,20 @@ "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/helper-string-parser": { + "node_modules/@babel/types": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", - "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", - "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/types": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", - "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0.tgz", + "integrity": "sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^8.0.0", - "@babel/helper-validator-identifier": "^8.0.4" + "@babel/helper-validator-identifier": "^8.0.0" }, "engines": { "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -4124,6 +3978,26 @@ "source-map-js": "^1.2.1" } }, + "node_modules/magicast/node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/magicast/node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/magicast/node_modules/@babel/parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", @@ -4140,6 +4014,20 @@ "node": ">=6.0.0" } }, + "node_modules/magicast/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", diff --git a/gitnexus/package.json b/gitnexus/package.json index 68de5143a..fb60f0dbe 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -100,7 +100,7 @@ "@babel/generator": "^8.0.0", "@babel/parser": "^8.0.0", "@babel/traverse": "^8.0.0", - "@babel/types": "^7.29.7", + "@babel/types": "^8.0.0", "@types/busboy": "^1.5.4", "@types/cli-progress": "^3.11.6", "@types/cors": "^2.8.17", From dc993a6d43bbe086fec83ed41ec2d0a7273de78a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:40:51 +0100 Subject: [PATCH 03/24] chore(deps): bump github/codeql-action/analyze from 4.36.2 to 4.37.0 (#2506) * chore(deps): bump github/codeql-action/analyze from 4.36.2 to 4.37.0 Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.36.2 to 4.37.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...99df26d4f13ea111d4ec1a7dddef6063f76b97e9) --- updated-dependencies: - dependency-name: github/codeql-action/analyze dependency-version: 4.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * ci(codeql): keep action steps in lockstep Co-authored-by: azizur100389 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: azizur100389 --- .github/dependabot.yml | 4 ++++ .github/workflows/codeql.yml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c99b666eb..4a25e682d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,6 +16,10 @@ updates: labels: - dependencies - ci + groups: + codeql-action: + patterns: + - github/codeql-action/* # Keep pinned Docker base-image digests current for the root Dockerfiles. - package-ecosystem: docker diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 04d6d8dd2..a72adeed3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -48,7 +48,7 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: languages: ${{ matrix.language }} queries: security-and-quality @@ -73,6 +73,6 @@ jobs: - '**/test/**/fixtures/**' - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: category: '/language:${{ matrix.language }}' From 731ab6f51206e2e8191a7a7793de1e5852490988 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:20:48 +0100 Subject: [PATCH 04/24] chore(deps): bump marocchino/sticky-pull-request-comment (#2537) Bumps [marocchino/sticky-pull-request-comment](https://github.com/marocchino/sticky-pull-request-comment) from 3.0.4 to 3.0.5. - [Release notes](https://github.com/marocchino/sticky-pull-request-comment/releases) - [Commits](https://github.com/marocchino/sticky-pull-request-comment/compare/0ea0beb66eb9baf113663a64ec522f60e49231c0...5770ad5eb8f42dd2c4f34da00c94c5381e49af88) --- updated-dependencies: - dependency-name: marocchino/sticky-pull-request-comment dependency-version: 3.0.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-report.yml b/.github/workflows/ci-report.yml index b6d6b80e3..6a55254ca 100644 --- a/.github/workflows/ci-report.yml +++ b/.github/workflows/ci-report.yml @@ -437,7 +437,7 @@ jobs: - name: Comment on PR if: steps.meta.outputs.skip != 'true' - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v2 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v2 with: header: ci-report number: ${{ steps.meta.outputs.pr_number }} From 0656099332705ae7c011f22ff328d6de04d5e8dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:21:05 +0100 Subject: [PATCH 05/24] chore(deps): bump docker/metadata-action from 6.1.0 to 6.2.0 (#2536) Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 6.1.0 to 6.2.0. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9...dc802804100637a589fabce1cb79ff13a1411302) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: 6.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e07bf25ec..28ad56edc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -183,7 +183,7 @@ jobs: # `github.event_name` would still be "push", not "workflow_call". - name: Extract Docker metadata id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: # Dual-registry publish. metadata-action expands the same tag set # against every image ref listed here, and build-push-action pushes From e2e92549380df657824d42593fd0fad6937a74f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:21:25 +0100 Subject: [PATCH 06/24] chore(deps): bump github/codeql-action/upload-sarif (#2535) Bumps the codeql-action group with 1 update: [github/codeql-action/upload-sarif](https://github.com/github/codeql-action). Updates `github/codeql-action/upload-sarif` from 4.36.2 to 4.37.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...99df26d4f13ea111d4ec1a7dddef6063f76b97e9) --- updated-dependencies: - dependency-name: github/codeql-action/upload-sarif dependency-version: 4.37.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: codeql-action ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- .github/workflows/trivy.yml | 2 +- .github/workflows/workflow-lint.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index f48f5ff35..fe39a284a 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -53,6 +53,6 @@ jobs: retention-days: 5 - name: Upload to Security tab - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: sarif_file: results.sarif diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index e9000788c..60d233553 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -76,7 +76,7 @@ jobs: exit-code: '0' - name: Upload to Security tab - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: sarif_file: trivy-${{ matrix.image.name }}.sarif category: trivy-${{ matrix.image.name }} diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 302ba9d59..ba61226f7 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -76,7 +76,7 @@ jobs: continue-on-error: true - name: Upload SARIF - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: sarif_file: zizmor.sarif category: zizmor From 42c4d91cd48ce76d0e9c104de8ea2cd7c807c763 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:21:52 +0100 Subject: [PATCH 07/24] chore(deps)(deps-dev): bump vitest from 4.1.9 to 4.1.10 in /gitnexus-web (#2533) Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.1.9 to 4.1.10. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.10/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 4.1.10 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gitnexus-web/package-lock.json | 102 ++++++++++++++++----------------- gitnexus-web/package.json | 2 +- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/gitnexus-web/package-lock.json b/gitnexus-web/package-lock.json index f1093c919..28a8d43f7 100644 --- a/gitnexus-web/package-lock.json +++ b/gitnexus-web/package-lock.json @@ -64,7 +64,7 @@ "tree-sitter-wasms": "^0.1.13", "typescript": "^5.4.5", "vite": "^8.1.4", - "vitest": "^4.1.5", + "vitest": "^4.1.10", "wait-on": "^9.0.10" }, "engines": { @@ -2773,14 +2773,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", - "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.10", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -2794,8 +2794,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.9", - "vitest": "4.1.9" + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2804,16 +2804,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2822,13 +2822,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.9", + "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2859,9 +2859,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2872,13 +2872,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.10", "pathe": "^2.0.3" }, "funding": { @@ -2886,14 +2886,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2902,9 +2902,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", "funding": { @@ -2912,13 +2912,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", + "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -8350,19 +8350,19 @@ } }, "node_modules/vitest": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.9", - "@vitest/mocker": "4.1.9", - "@vitest/pretty-format": "4.1.9", - "@vitest/runner": "4.1.9", - "@vitest/snapshot": "4.1.9", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -8390,12 +8390,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.9", - "@vitest/browser-preview": "4.1.9", - "@vitest/browser-webdriverio": "4.1.9", - "@vitest/coverage-istanbul": "4.1.9", - "@vitest/coverage-v8": "4.1.9", - "@vitest/ui": "4.1.9", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/gitnexus-web/package.json b/gitnexus-web/package.json index d0f068a33..90bdc8388 100644 --- a/gitnexus-web/package.json +++ b/gitnexus-web/package.json @@ -74,7 +74,7 @@ "tree-sitter-wasms": "^0.1.13", "typescript": "^5.4.5", "vite": "^8.1.4", - "vitest": "^4.1.5", + "vitest": "^4.1.10", "wait-on": "^9.0.10" }, "overrides": { From 90b5098b24d87cadb861aa68753425d0df4068b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:22:18 +0100 Subject: [PATCH 08/24] chore(deps)(deps): bump @langchain/core in /gitnexus-web (#2530) Bumps [@langchain/core](https://github.com/langchain-ai/langchainjs) from 1.2.1 to 1.2.2. - [Release notes](https://github.com/langchain-ai/langchainjs/releases) - [Commits](https://github.com/langchain-ai/langchainjs/compare/@langchain/core@1.2.1...@langchain/core@1.2.2) --- updated-dependencies: - dependency-name: "@langchain/core" dependency-version: 1.2.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gitnexus-web/package-lock.json | 8 ++++---- gitnexus-web/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gitnexus-web/package-lock.json b/gitnexus-web/package-lock.json index 28a8d43f7..97fc47029 100644 --- a/gitnexus-web/package-lock.json +++ b/gitnexus-web/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@langchain/anthropic": "^1.3.29", - "@langchain/core": "^1.1.49", + "@langchain/core": "^1.2.2", "@langchain/google-genai": "^2.2.0", "@langchain/langgraph": "^1.4.7", "@langchain/ollama": "^1.2.7", @@ -1104,9 +1104,9 @@ } }, "node_modules/@langchain/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.2.1.tgz", - "integrity": "sha512-NNG/cC5FGuHDOAP56h0ddp8Rfk8p+othWzEK5RV9JIG6RvnF5vGa5r0AEGtKfQieed7s1kC42GuIzVOBvMBL/g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.2.2.tgz", + "integrity": "sha512-KfjEOT6sCg0vvItagfEtGpmrGoLMGfma4Affb5BGEqPmS2YR3AxW54pABSkhQlzCehTB+0BnLquAe1lGF4J9zQ==", "license": "MIT", "dependencies": { "@cfworker/json-schema": "^4.0.2", diff --git a/gitnexus-web/package.json b/gitnexus-web/package.json index 90bdc8388..0b86287ee 100644 --- a/gitnexus-web/package.json +++ b/gitnexus-web/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@langchain/anthropic": "^1.3.29", - "@langchain/core": "^1.1.49", + "@langchain/core": "^1.2.2", "@langchain/google-genai": "^2.2.0", "@langchain/langgraph": "^1.4.7", "@langchain/ollama": "^1.2.7", From 64e9ef84845c93b56d8283debf655f682a21565b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:22:37 +0100 Subject: [PATCH 09/24] chore(deps)(deps): bump @langchain/ollama in /gitnexus-web (#2527) Bumps [@langchain/ollama](https://github.com/langchain-ai/langchainjs) from 1.2.7 to 1.3.0. - [Release notes](https://github.com/langchain-ai/langchainjs/releases) - [Commits](https://github.com/langchain-ai/langchainjs/compare/langchain@1.2.7...@langchain/ollama@1.3.0) --- updated-dependencies: - dependency-name: "@langchain/ollama" dependency-version: 1.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gitnexus-web/package-lock.json | 8 ++++---- gitnexus-web/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gitnexus-web/package-lock.json b/gitnexus-web/package-lock.json index 97fc47029..f9ba12f8a 100644 --- a/gitnexus-web/package-lock.json +++ b/gitnexus-web/package-lock.json @@ -12,7 +12,7 @@ "@langchain/core": "^1.2.2", "@langchain/google-genai": "^2.2.0", "@langchain/langgraph": "^1.4.7", - "@langchain/ollama": "^1.2.7", + "@langchain/ollama": "^1.3.0", "@langchain/openai": "^1.5.3", "@sigma/edge-curve": "^3.1.0", "@tailwindcss/vite": "^4.3.2", @@ -1235,9 +1235,9 @@ } }, "node_modules/@langchain/ollama": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@langchain/ollama/-/ollama-1.2.7.tgz", - "integrity": "sha512-7Gu17q1dn4nKGB3ZYJyaaL8H/2k7LHvcL6V25S8cVDniTTSvd0fWzI5MzPJvbf9WPxBhvmf+rDDLAhOWljHEtQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@langchain/ollama/-/ollama-1.3.0.tgz", + "integrity": "sha512-uzLkZVTXN0SI5zAoJbJzL4y/QN7gbmJ1txBgFfqJ5M0Qf+Yu2Zu8q98L3jIrfM+akWMaNmRU4R4APhcfQmeIsw==", "license": "MIT", "dependencies": { "ollama": "^0.6.3" diff --git a/gitnexus-web/package.json b/gitnexus-web/package.json index 0b86287ee..3ccd8573b 100644 --- a/gitnexus-web/package.json +++ b/gitnexus-web/package.json @@ -22,7 +22,7 @@ "@langchain/core": "^1.2.2", "@langchain/google-genai": "^2.2.0", "@langchain/langgraph": "^1.4.7", - "@langchain/ollama": "^1.2.7", + "@langchain/ollama": "^1.3.0", "@langchain/openai": "^1.5.3", "@sigma/edge-curve": "^3.1.0", "@tailwindcss/vite": "^4.3.2", From 11c9d791e46abca0e506615957c4e16544009738 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:23:26 +0100 Subject: [PATCH 10/24] chore(deps): bump github/codeql-action/init from 4.36.2 to 4.37.0 (#2504) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.36.2 to 4.37.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...99df26d4f13ea111d4ec1a7dddef6063f76b97e9) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar From 0b777979d83c3608469aa315ba9094534c3f94a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:23:47 +0100 Subject: [PATCH 11/24] chore(deps)(deps-dev): bump @babel/types in /gitnexus (#2534) Bumps [@babel/types](https://github.com/babel/babel/tree/HEAD/packages/babel-types) from 8.0.0 to 8.0.4. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v8.0.4/packages/babel-types) --- updated-dependencies: - dependency-name: "@babel/types" dependency-version: 8.0.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gitnexus/package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index a8708821a..b11d4e572 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -195,14 +195,14 @@ } }, "node_modules/@babel/types": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0.tgz", - "integrity": "sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^8.0.0", - "@babel/helper-validator-identifier": "^8.0.0" + "@babel/helper-validator-identifier": "^8.0.4" }, "engines": { "node": "^22.18.0 || >=24.11.0" From 22905bb2a7d34e9d9dede22bc7d7fc31e3acd32b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:24:01 +0100 Subject: [PATCH 12/24] chore(deps)(deps-dev): bump @babel/traverse in /gitnexus (#2532) Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 8.0.0 to 8.0.4. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v8.0.4/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-version: 8.0.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gitnexus/package-lock.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index b11d4e572..9aa2ef8da 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -145,13 +145,13 @@ } }, "node_modules/@babel/parser": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0.tgz", - "integrity": "sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz", + "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^8.0.0" + "@babel/types": "^8.0.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -176,18 +176,18 @@ } }, "node_modules/@babel/traverse": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.0.tgz", - "integrity": "sha512-bxTj/W2VclGE6CctlfQOpxg8MPDzXArRqkOBePw8EHfebcjF7fETWSS3BriEECo+UiU/Yblq+xUtSImFu7cTbw==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.4.tgz", + "integrity": "sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^8.0.0", "@babel/generator": "^8.0.0", "@babel/helper-globals": "^8.0.0", - "@babel/parser": "^8.0.0", + "@babel/parser": "^8.0.4", "@babel/template": "^8.0.0", - "@babel/types": "^8.0.0", + "@babel/types": "^8.0.4", "obug": "^2.1.1" }, "engines": { From 06ba512046f6aea14937b121adf3005b1d3e0c22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:24:15 +0100 Subject: [PATCH 13/24] chore(deps)(deps-dev): bump @babel/parser in /gitnexus (#2531) Bumps [@babel/parser](https://github.com/babel/babel/tree/HEAD/packages/babel-parser) from 8.0.0 to 8.0.4. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v8.0.4/packages/babel-parser) --- updated-dependencies: - dependency-name: "@babel/parser" dependency-version: 8.0.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 4d85fe1f19b24d08af1fa06389518fc4a84d878b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:54:44 +0100 Subject: [PATCH 14/24] chore(deps)(deps-dev): bump @types/node in /gitnexus-web (#2528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.9.1 to 25.9.5. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.9.5 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar --- gitnexus-web/package-lock.json | 8 ++++---- gitnexus-web/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gitnexus-web/package-lock.json b/gitnexus-web/package-lock.json index f9ba12f8a..0800f4d65 100644 --- a/gitnexus-web/package-lock.json +++ b/gitnexus-web/package-lock.json @@ -53,7 +53,7 @@ "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/dompurify": "^3.2.0", - "@types/node": "^25.9.1", + "@types/node": "^25.9.5", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/react-syntax-highlighter": "^15.5.13", @@ -2523,9 +2523,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", "devOptional": true, "license": "MIT", "dependencies": { diff --git a/gitnexus-web/package.json b/gitnexus-web/package.json index 3ccd8573b..13fd86cd7 100644 --- a/gitnexus-web/package.json +++ b/gitnexus-web/package.json @@ -63,7 +63,7 @@ "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/dompurify": "^3.2.0", - "@types/node": "^25.9.1", + "@types/node": "^25.9.5", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/react-syntax-highlighter": "^15.5.13", From 3c36ab906bbf393ebbb3a7e3012e6454d4b39626 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:55:12 +0100 Subject: [PATCH 15/24] chore(deps)(deps): bump @langchain/anthropic in /gitnexus-web (#2526) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@langchain/anthropic](https://github.com/langchain-ai/langchainjs) from 1.3.29 to 1.5.1. - [Release notes](https://github.com/langchain-ai/langchainjs/releases) - [Commits](https://github.com/langchain-ai/langchainjs/commits/@langchain/anthropic@1.5.1) --- updated-dependencies: - dependency-name: "@langchain/anthropic" dependency-version: 1.5.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar --- gitnexus-web/package-lock.json | 43 ++++++++++++++++++++++++++-------- gitnexus-web/package.json | 2 +- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/gitnexus-web/package-lock.json b/gitnexus-web/package-lock.json index 0800f4d65..638123bf4 100644 --- a/gitnexus-web/package-lock.json +++ b/gitnexus-web/package-lock.json @@ -8,7 +8,7 @@ "name": "gitnexus-web", "version": "0.0.0", "dependencies": { - "@langchain/anthropic": "^1.3.29", + "@langchain/anthropic": "^1.5.1", "@langchain/core": "^1.2.2", "@langchain/google-genai": "^2.2.0", "@langchain/langgraph": "^1.4.7", @@ -98,12 +98,13 @@ } }, "node_modules/@anthropic-ai/sdk": { - "version": "0.91.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", - "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "version": "0.103.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.103.0.tgz", + "integrity": "sha512-1uG7RNgoHTUxzOXqSCODKt0UTVlxWiHk/2Tt2/uQJiPW7XzBeKVuJyd3Aw6T3LPyvZV/jDTnPLX7SaM70WLLjA==", "license": "MIT", "dependencies": { - "json-schema-to-ts": "^3.1.1" + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" }, "bin": { "anthropic-ai-sdk": "bin/cli" @@ -1088,19 +1089,19 @@ } }, "node_modules/@langchain/anthropic": { - "version": "1.3.29", - "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.3.29.tgz", - "integrity": "sha512-ep1qBIcV07bajsg3fDqMd39rYwoRLOEK/6lk+MCxlm1YB5SRoKKJAZANrblQ/4RYhZJnxf95c6BSQu8VoNbVAQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.5.1.tgz", + "integrity": "sha512-j92zCCd5BFH3rHMRzc2wBmSKDoVpinof1oh8aFiAz9TWbSOc4tGU4n6bqwy/wP0GH1uO96zZHLGCHBMPgrxTNw==", "license": "MIT", "dependencies": { - "@anthropic-ai/sdk": "^0.91.1", + "@anthropic-ai/sdk": "^0.103.0", "zod": "^3.25.76 || ^4" }, "engines": { "node": ">=20" }, "peerDependencies": { - "@langchain/core": "^1.1.45" + "@langchain/core": "^1.2.1" } }, "node_modules/@langchain/core": { @@ -1693,6 +1694,12 @@ "sigma": ">=3.0.0-beta.10" } }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -4343,6 +4350,12 @@ "node": ">=8.6.0" } }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fast-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", @@ -7773,6 +7786,16 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/std-env": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", diff --git a/gitnexus-web/package.json b/gitnexus-web/package.json index 13fd86cd7..093324dc2 100644 --- a/gitnexus-web/package.json +++ b/gitnexus-web/package.json @@ -18,7 +18,7 @@ "test:e2e:report": "playwright show-report" }, "dependencies": { - "@langchain/anthropic": "^1.3.29", + "@langchain/anthropic": "^1.5.1", "@langchain/core": "^1.2.2", "@langchain/google-genai": "^2.2.0", "@langchain/langgraph": "^1.4.7", From ed8ab1c24679c45a6463f5b325fee44b3742ecdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Fri, 17 Jul 2026 17:20:02 +0100 Subject: [PATCH 16/24] fix(scope-resolution): resolve callable reference flows (#2437) (#2522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(plans): add provider-hook value-refs plan (#2437) Co-Authored-By: Claude Fable 5 * docs(plans): deepen #2437 plan to USES + property-dispatch design Design revised after prior-art research (Kythe ref vs ref/call, Joern METHOD_REF, Feldthaus field-based call graphs, CodeQL impliedReceiverStep): registration sites emit reference-class USES, invocation is recovered by a field-based property-dispatch pass synthesizing CALLS at member-call sites. Co-Authored-By: Claude Fable 5 * feat(scope-resolution): model provider-hook value references (#2437) Functions referenced as object-literal property values (provider hooks like emitScopeCaptures: emitCppScopeCaptures) previously produced no edge at all, so impact/context reported a false-safe 0 upstream dependents. Two coordinated halves, per prior art (Kythe ref vs ref/call, Joern METHOD_REF, Feldthaus ICSE'13 field-based call graphs, CodeQL impliedReceiverStep): - Registration -> USES: new ReferenceKind 'value-ref'; TS/JS queries capture pair values and shorthand properties (with @reference.property-key); emitted as a reference-class USES edge, reason 'scope-resolution: value-ref'. Resolution is callable-gated so plain values emit nothing. - Dispatch -> CALLS: new shared pass emitPropertyDispatchCalls synthesizes CALLS (reason 'property-dispatch', confidence 0.7, per-key fan-out cap 32 calibrated on this repo's 16-provider hook tables) from member-call sites to every function registered under the same property key. Deviation from plan: the pass owns value-ref resolution entirely via the post-finalize findCallableBindingInScope walker — the shared registries only see pre-finalize local bindings, so imported hooks (the c-cpp.ts case) were unresolvable through lookupForSite; Reference.propertyKey passthrough dropped as unnecessary. SCHEMA_BUMP 13 -> 14: ParsedFile gains value-ref sites + propertyKey. Verified end-to-end: impact(emitCppScopeCaptures, upstream) now reports 8 impacted / HIGH with extractParsedFile (true dispatch caller) at d=1 via property-dispatch and the c-cpp.ts registration via USES. Co-Authored-By: Claude Fable 5 * test(scope-resolution): cover value-ref registration and property dispatch (#2437) Integration: same-file/cross-file/aliased/shorthand registrations emit USES; non-callable and destructuring values emit nothing; dispatch sites gain property-dispatch CALLS (incl. JS twins and per-language partitioning); fan-out-capped keys are dropped entirely; factory-call values unchanged. Unit: capture-shape pins for @reference.value-ref + @reference.property-key. Co-Authored-By: Claude Fable 5 * fix(scope-resolution): surface dropped property-dispatch keys in stats (#2437) Review finding: skippedKeys was returned but discarded — a hook table larger than the fan-out cap silently reopened the #2437 gap for those keys. Log dropped keys and fold value-ref USES + dispatch CALLS into referenceEdgesEmitted. Co-Authored-By: Claude Fable 5 * docs(plans): add callable reference-flow implementation plan * fix(scope-resolution): close property-dispatch review gaps * feat(scope-resolution): add callable flow facts * feat(scope-resolution): resolve callable value flow * feat(scope-resolution): resolve callable references across providers * fix: harden callable reference flow resolution * fix(scope-resolution): preserve callable binding semantics * docs(plans): add pr-2522-review-fixes plan Co-Authored-By: Claude Fable 5 * fix(storage): bump INCREMENTAL_SCHEMA_VERSION for callable-value-flow edges Callable-value-flow CALLS/USES edges (#2437) can connect two files whose content did not change, but the incremental write set only covers changed files — a top-up against a pre-v7 index would silently omit the new edges for every unchanged file pair, indefinitely. Force the one-time full re-analyze (review finding 1, #2522). Co-Authored-By: Claude Fable 5 * fix(storage): sanitize callable-flow sites per-site at load, log drops The load-time validator rejected the WHOLE ParsedFile when one site was malformed or over-bound, with no logging — and C++ legitimately emits empty-string parameterTypes entries ('' = unknown, the ReferenceSite.argumentTypes convention) for cv-only/ERROR-recovered types, so real repos fell into a permanent, silent warm-cache-miss reparse loop through the #1983-sensitive main-thread path (review finding 7, #2522). Now: '' entries are valid in type arrays; a malformed/over-bound site drops only itself (counted, warned once per load); only non-array garbage — evidence the serialization itself is untrustworthy — rejects the file. Deviation from plan §6 wording: validator-side tolerance replaces emit-side clamps — smaller diff, same asymmetry closed at the single chokepoint. Co-Authored-By: Claude Fable 5 * fix(scope-resolution): keep declarations in the union for reassigned callable cells The binding-lookup suppression for fact-constrained cells was wholesale: reassigning a declared function through its own name (greet = other; greet()) deferred the call to the solver, which then refused the lexical lookup that resolves the declaration — an unresolvable RHS yielded zero CALLS for a call that resolved pre-flow (review finding 8, #2522). Suppression now applies only to cells bound by FORMAL facts — its actual purpose (a parameter whose grammar emits no declaration binding must not adopt a same-named outer function). Copy/alias/store/load destinations keep their declaration as an inclusion seed (Andersen-style union). Co-Authored-By: Claude Fable 5 * fix(scope-resolution): count forfeited deferred sites in the budget-bailout warning On work-budget exhaustion the deferred invoke sites end the run with zero CALLS — free-call fallback and reference emission already skipped them — but the warning said 'ordinary graph emission remains untouched', which is false for exactly those sites. The warning context now carries the unresolved deferred-site count and the comment states the real cost (review finding: budget-bailout honesty, #2522). Co-Authored-By: Claude Fable 5 * feat(scope-resolution): surface dropped property-dispatch keys in stats and warn payload The over-cap warning carried only a count; the dropped key NAMES were discarded and RunScopeResolutionStats had no field, so the PR-body claim 'includes them in resolver statistics' was unimplemented (review finding, #2522; reviewer ask on the fan-out cap). The warn payload now names up to 20 dropped keys and the stats carry propertyDispatchSkippedKeys. Co-Authored-By: Claude Fable 5 * refactor(scope-resolution): drop producer-less ownerQualifiedName from formal sites No capture emitter anywhere produces @callable-flow.owner-qualified-name — the solver branch consuming it was unreachable in production, yet the field was typed, parsed, validated, and unit-tested with hand-built input (review finding 16, #2522; YAGNI). Re-add with a real producer if C++ qualified member declarators ever need it. Co-Authored-By: Claude Fable 5 * refactor(scope-resolution): drop dead callable-flow knobs CallableFlowPassingMode 'callable-object' had no producer and no consumer distinguishing it, and CallableFlowCaptureOptions.extractCallArguments had no language providing it (unlike its live sibling extractCallCallee) — review finding 17, #2522 (YAGNI). The invocation-kind 'callable-object' is a different, live concept and stays. Co-Authored-By: Claude Fable 5 * fix(ingestion): bind subscripted callable cells to the container, not the index terminalIdentifier iterates children in reverse, so tbl[i] = handler seeded the INDEX variable's cell (polluting a same-named formal) and tbl[i](7) looked up the callee under i in a different scope — no join, no CALLS edge for the classic function-pointer-array dispatch (review finding 12, #2522). Subscript nodes now recurse into their container field only, in both bindingIdentifier and terminalIdentifier, across the fielded grammars (C/C++/JS/TS/Python/Go/Java). Co-Authored-By: Claude Fable 5 * fix(ingestion): make cross-function file-scope callable bindings resolvable Two stacked gaps killed the canonical C callback-registration pattern (fp assigned in init(), called in run()) — the exact #2437 false-safe this PR exists to fix (review finding H1, #2522): 1. isVisibleValueBinding only consulted assignment regions and formals, so a call in a function OTHER than the assigning one emitted no invoke fact. A declared callable-typed binding is now a value binding wherever its declaration is visible (visibleCallableSignature). 2. The C scope query had no @declaration.variable pattern for function- pointer declarators — void (*fp)(int); created no scope-tree binding, so the seed (init) and invoke (run) cells canonicalized to different keys and never joined. Both bare and initialized forms now bind. Co-Authored-By: Claude Fable 5 * fix(c): detect variadic parameters via the named variadic_parameter node tree-sitter-c materializes '...' as a named variadic_parameter node; the anonymous-token checks never matched, so variadic function-pointer signatures were emitted with a wrong fixed arity and no '...' sentinel (review finding, #2522). C++ is unaffected ('...' stays an anonymous token there); the token checks remain for such grammars. Co-Authored-By: Claude Fable 5 * fix(ingestion): emit invoke facts for field-stored callable member calls The C ops-vtable pattern (o->run = handler; o->run(1)) captured the store but never the call — the member path in emitCallFacts bailed for languages without protocol methods, and the value-binding index recorded the member store under the OBJECT's name ('o'), not the member's ('run') (review finding 11/M3, #2522). Member destinations now also record their terminal member name, and a member call whose name-cell has a visible store emits an indirect invoke — gated on the store so plain accessor calls (map.get) stay inert. Co-Authored-By: Claude Fable 5 * fix(cpp): disambiguate (obj->*ptr)() ERROR recovery by token order tree-sitter-cpp groups the recovered '->*' two ways depending on error-recovery cost (identifier lengths): [identifier, ERROR '->*m'] or [ERROR 'obj->*', identifier]. The recovery assumed the first shape, so the second silently swapped receiver/member and dropped the call site — the committed test passed only by name luck (review finding H2, #2522). The identifier's position relative to '->*' inside the ERROR now decides roles. Co-Authored-By: Claude Fable 5 * fix(cpp): class members are never file-local in hasFileLocalCallableLinkage The name-keyed file-local set is populated from every static declaration, so an in-class 'static void make();' (external linkage — in-class static means no-instance) and any member sharing a name with a static free function were over-marked, refusing legitimate cross-file declaration/definition joins (review finding 13/M2, #2522). Method and Constructor defs now bypass the name-set, per the hook's own linkage-only contract. Deviation from plan step 13: the regression is a unit-level contract pin rather than an end-to-end join test — C++ merges out-of-line member definitions onto the member node by qualified identity, so the graph shape cannot discriminate the join refusal for members. Co-Authored-By: Claude Fable 5 * fix(cpp): classify parameter passing mode from the declarator chain only A whole-subtree scan for reference_declarator inverted copy vs alias: void reg(void (*cb)(int& out)) marked the by-value pointer cb as 'reference' because of the NESTED parameter's int&, making the solver back-propagate formal targets into every caller's argument cell — alias semantics for a copy (review finding 14/M5, #2522). The chain walk never descends into nested parameter lists; a reference anywhere ON the chain (int& x, void (*&cb)(int)) still aliases. Co-Authored-By: Claude Fable 5 * fix(ruby): bare identifiers are calls, not callable references Ruby parses a receiver-less zero-arg method call identically to a variable read, so 'action = process' — which CALLS process and stores its return — seeded action with the callable and minted a wrong CALLS edge from any dispatch through it, confirmed end-to-end (review finding 15/HIGH, #2522). New provider knob bareNamesAreCalls: a bare name that is not a provably local value binding and not an explicit reference form (method(:x), lambda/proc) emits no flow fact, on both the assignment and argument paths. Co-Authored-By: Claude Fable 5 * fix(go): pair multi-value := positionally instead of cross-wiring The shared field fallback took the FIRST LHS identifier and the LAST RHS identifier of Go's expression_list pair, cross-wiring 'a, b := f, g' and synthesizing a garbage comma-joined qualified name — the real relationships were silently dropped (review finding 16, #2522). extractAssignment may now return multiple pairs; Go pairs list entries positionally and emits nothing for a length mismatch (multi-return call RHS). Co-Authored-By: Claude Fable 5 * fix(java): drop get/test from callableProtocolMethods 'get' and 'test' collide with ubiquitous non-functional-interface APIs (Map/List/Optional/Future.get), so every ordinary container access emitted a spurious callable-object invoke fact — high-volume misleading graph facts with a cross-wiring risk on receiver-name reuse (review finding 17, #2522). Supplier.get/Predicate.test dispatch is deliberately traded away until the check can gate on the receiver's declared type. Co-Authored-By: Claude Fable 5 * fix(rust): pin the qualified-name no-degrade guard as a hard invariant Rust's scoped_identifier callable-reference capture over-includes unit enum variants and associated constants (Shape::Square seeds as if callable); they stay edge-free only because resolveSeedCandidates refuses to degrade an unresolved qualified name to a simple-name lookup (review finding 18, #2522). Capture-side type filtering would false-negative on tuple-variant constructors, so the guard IS the contract: documented as a hard invariant (Go's mis-shaped multi-value forms also rely on it) and pinned end-to-end. Co-Authored-By: Claude Fable 5 * fix(php): remove nonexistent optional_parameter node type tree-sitter-php has no 'optional_parameter' — defaults ride on simple_parameter — so the entry was dead weight the #1920 literal gate does not cover for capture-option Sets (review finding 19, #2522). Co-Authored-By: Claude Fable 5 * fix(cobol): detect procedure pointers on fixed-format sources Two stacked defects made the feature a no-op on classic sequence-numbered fixed format (review finding 20/H3, #2522): 1. parseDataItemClauses' USAGE alternation knew POINTER but not PROCEDURE-POINTER/FUNCTION-POINTER, so the dataItems filter was dead. 2. The raw-line fallback scanned UNCLEANED text, where the sequence number satisfied the leading digits and the LEVEL NUMBER got captured as the pointer name. It now scans preprocessed lines and requires a letter- initial name (COBOL data names must contain a letter). 161 COBOL preprocessor/copy-expander tests stay green; free-format matrix case unchanged. Co-Authored-By: Claude Fable 5 * fix(cobol): skip comment lines in SET seed/copy scans A commented-out SET (indicator-column '*'/'/' or free-format '*>') produced a live seed and a false CALLS edge from dead code (review finding 21/M1, #2522). The scan now skips indicator-column comment lines and strips inline '*>' tails before matching. Co-Authored-By: Claude Fable 5 * docs(architecture): document callable-flow-only mode and skipped-key reporting The Callable-value flow section omitted scopeResolutionEdgeMode: 'callable-flow-only' — a real emit-pipeline branch that suppresses all ordinary emission for standalone providers (review finding 22, #2522) — and predated the skipped-key names/stats surfacing. Co-Authored-By: Claude Fable 5 * docs(scope-resolution): correct value-ref resolution attribution and stale pdg-gating comments The value-ref contract comment claimed MethodRegistry resolution — the mechanism is the post-finalize findCallableBindingInScope walker owned by emitPropertyDispatchCalls (resolveReferenceSites skips these sites). Three 'only under --pdg' calleeIdSink comments were falsified by the #2437 gating change (callee-id-sink.ts's header was updated; these copies were missed). Review finding 23, #2522. Co-Authored-By: Claude Fable 5 * test(ingestion): direct unit coverage for synthesizeCallableFlowCaptures The 1,100-line shared synthesizer had no test naming it — only downstream consumers were covered (review finding 24, #2522). Pins seed/invoke/ formal/argument emission, subscript container binding, store-gated member invokes, produced-value guards, and the bareNamesAreCalls knob over a minimal options object so assertions target the synthesizer's own semantics. Co-Authored-By: Claude Fable 5 * test(resolvers): deepen shallow-language coverage; fix Kotlin/Swift reassignment gaps it exposed Adds the COBOL SET x TO y copy-branch scenario and conditional-assignment scenarios for Kotlin, C#, Swift, and Dart (10 languages previously had one generic case each — review finding 25, #2522). The new scenarios exposed two real capture gaps, fixed here: - tree-sitter-kotlin's 'assignment' node is fieldless, so nested reassignments (chosen = ::target inside a block) produced no flow facts; Kotlin's extractAssignment now decomposes it positionally. - tree-sitter-swift fields its assignment as target:/result:, neither in the shared fallback's field lists; both added. Co-Authored-By: Claude Fable 5 * test(infra): literal-validation gate for callable-capture option Sets The #1920 gate validates query literals and exported configs but not the module-private *_CALLABLE_CAPTURE_OPTIONS Sets consumed by the shared synthesizer — a typo'd node type silently captures nothing (PHP shipped a dead 'optional_parameter'; review finding 26, #2522). Every NodeTypes Set literal is now validated against its language's grammar; name-carrying sets (callableProtocolMethods, memberPointerOperators) are deliberately outside the contract. Co-Authored-By: Claude Fable 5 * test(storage): centralize corrupt-fixture casts into makeStoreEntry The callable-flow store tests scattered 'as unknown as' double-casts per fixture (review finding 27, #2522; standing no-as-any rule). One typed helper now owns the single controlled escape hatch for building malformed serialization-boundary payloads. Co-Authored-By: Claude Fable 5 * chore(bench): refresh capture fingerprints after review fixes python-scope: the committed baseline (8d5c3699) never matched this branch's code — CI's benchmarks arm was red on the PR head (review finding 2/HIGH, #2522); regenerated (a99e69ab), scaling 1.04 in budget. scope-capture: ruby/cpp/swift/java/kotlin drifted from the review-fix commits (bare-name suppression, passing modes + ->* recovery, assignment fields, protocol narrowing, positional assignment); all 14 languages re-verified PASS with ratios <= 1.18 against the 1.5 budget. Co-Authored-By: Claude Fable 5 * chore(docs): untrack docs/plans working documents docs/ is gitignored (local working docs); the plan files were force-added past the ignore. Untracked from the index only — they stay on disk. Co-Authored-By: Claude Fable 5 * test(golden): regenerate captures goldens after callable-flow review fixes The per-language digest guards (csharp/go/php/python/ruby/rust/swift) locked the pre-fix capture output; the review-fix series intentionally changed it — store-gated member invokes, subscript container binding, Ruby bare-name suppression, Swift assignment fields, positional pairing. Regenerated with UPDATE_GOLDEN=1; clean verification run 59/59; all other parity/golden guards (pipeline-graph, spring-route, python parity) pass untouched at 33/33. Co-Authored-By: Claude Fable 5 * fix(ingestion): prototypes are callees, not callable value cells The cross-function visibility fix indexed EVERY signature-bearing declaration as a value binding — including plain function/method prototypes (void f(int);). Every call to a declared function then became an indirect invoke, and with emitCanonicalInvokeReference (C/C++) minted a free-call reference that resolved through the registry, bypassing the precise passes' two-phase/ambiguity/subobject suppression — eight phantom CALLS edges in the cpp resolver suite on CI. Only declarations whose binding identifier sits under a pointer/ parenthesized declarator (callable-typed variables like void (*fp)(int);) create value cells now. cpp resolver suite 331/331; callable-value-flow + C/C++ suites 181/181 (the cross-function fp regression still passes); cpp fingerprint rebaselined, both bench gates PASS. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- ARCHITECTURE.md | 18 +- ...-11-001-fix-claude-skill-discovery-plan.md | 207 --- ...plan-fix-2432-napi-abort-cpp-extraction.md | 228 --- gitnexus-shared/src/index.ts | 16 + .../scope-resolution/callable-flow-site.ts | 142 ++ .../src/scope-resolution/parsed-file.ts | 7 + .../src/scope-resolution/reference-site.ts | 19 + gitnexus-shared/src/scope-resolution/types.ts | 10 +- .../python-scope/baseline-fingerprint.txt | 2 +- gitnexus/bench/scope-capture/baselines.json | 81 +- .../ingestion/cobol/cobol-preprocessor.ts | 2 +- .../src/core/ingestion/emit-references.ts | 4 + .../core/ingestion/languages/c/captures.ts | 67 + .../src/core/ingestion/languages/c/query.ts | 17 + .../ingestion/languages/c/scope-resolver.ts | 6 + .../ingestion/languages/cobol/captures.ts | 134 ++ .../languages/cobol/scope-resolver.ts | 9 + .../core/ingestion/languages/cpp/captures.ts | 231 +++ .../ingestion/languages/cpp/scope-resolver.ts | 14 + .../ingestion/languages/csharp/captures.ts | 24 + .../core/ingestion/languages/dart/captures.ts | 40 + .../core/ingestion/languages/go/captures.ts | 31 + .../core/ingestion/languages/java/captures.ts | 21 + .../languages/javascript/captures.ts | 17 + .../ingestion/languages/javascript/query.ts | 14 + .../ingestion/languages/kotlin/captures.ts | 44 + .../core/ingestion/languages/php/captures.ts | 46 + .../ingestion/languages/python/captures.ts | 20 + .../core/ingestion/languages/ruby/captures.ts | 42 + .../core/ingestion/languages/rust/captures.ts | 14 + .../ingestion/languages/swift/captures.ts | 16 + .../languages/typescript/captures.ts | 22 + .../ingestion/languages/typescript/query.ts | 20 + .../ingestion/pipeline-phases/parse-impl.ts | 3 +- .../src/core/ingestion/resolve-references.ts | 11 + .../src/core/ingestion/scope-extractor.ts | 364 +++- .../contract/scope-resolver.ts | 31 + .../graph-bridge/callee-id-sink.ts | 18 +- .../scope-resolution/graph-bridge/edges.ts | 10 +- .../scope-resolution/graph-bridge/ids.ts | 1 + .../graph-bridge/node-lookup.ts | 3 + .../graph-bridge/references-to-edges.ts | 11 +- .../passes/callable-value-flow.ts | 1527 +++++++++++++++++ .../passes/free-call-fallback.ts | 12 +- .../passes/property-dispatch.ts | 148 ++ .../passes/receiver-bound-calls.ts | 7 +- .../scope-resolution/pipeline/phase.ts | 35 +- .../scope-resolution/pipeline/run.ts | 280 ++- .../ingestion/utils/callable-flow-captures.ts | 1245 ++++++++++++++ gitnexus/src/storage/parse-cache.ts | 2 +- gitnexus/src/storage/parsedfile-store.ts | 183 +- gitnexus/src/storage/repo-manager.ts | 7 +- .../expected-captures.json | 200 +-- .../go-captures-golden/expected-captures.json | 120 +- .../expected-captures.json | 252 +-- .../expected-captures.json | 432 ++--- .../expected-captures.json | 80 +- .../expected-captures.json | 128 +- .../expected-captures.json | 68 +- ...lable-capture-options-literal-gate.test.ts | 74 + .../integration/cfg/worker-roundtrip.test.ts | 43 + .../resolvers/callable-value-flow.test.ts | 1437 ++++++++++++++++ .../resolvers/typescript-value-refs.test.ts | 359 ++++ .../unit/call-summary-schema-version.test.ts | 10 +- gitnexus/test/unit/parsedfile-store.test.ts | 144 +- .../test/unit/pdg-callee-id-capture.test.ts | 14 + .../scope-resolution/c/c-captures.test.ts | 12 + .../callable-flow-captures.test.ts | 117 ++ .../callable-value-flow-worklist.test.ts | 202 +++ .../scope-resolution/cpp/cpp-captures.test.ts | 33 + .../cpp/cpp-file-local-linkage.test.ts | 56 + .../java/java-captures.test.ts | 29 + .../javascript/javascript-captures.test.ts | 12 + .../scope-resolution/scope-extractor.test.ts | 185 ++ .../typescript/typescript-captures.test.ts | 39 + 75 files changed, 8264 insertions(+), 1265 deletions(-) delete mode 100644 docs/plans/2026-07-11-001-fix-claude-skill-discovery-plan.md delete mode 100644 docs/plans/2026-07-11-gitnexus-plan-fix-2432-napi-abort-cpp-extraction.md create mode 100644 gitnexus-shared/src/scope-resolution/callable-flow-site.ts create mode 100644 gitnexus/src/core/ingestion/scope-resolution/passes/callable-value-flow.ts create mode 100644 gitnexus/src/core/ingestion/scope-resolution/passes/property-dispatch.ts create mode 100644 gitnexus/src/core/ingestion/utils/callable-flow-captures.ts create mode 100644 gitnexus/test/integration/callable-capture-options-literal-gate.test.ts create mode 100644 gitnexus/test/integration/resolvers/callable-value-flow.test.ts create mode 100644 gitnexus/test/integration/resolvers/typescript-value-refs.test.ts create mode 100644 gitnexus/test/unit/scope-resolution/callable-flow-captures.test.ts create mode 100644 gitnexus/test/unit/scope-resolution/callable-value-flow-worklist.test.ts create mode 100644 gitnexus/test/unit/scope-resolution/cpp/cpp-file-local-linkage.test.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4390cae8c..fae270bf3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -201,7 +201,9 @@ Language-agnostic scope-resolution resolver. This is the resolution path for eve ReferenceIndex │ emitReceiverBoundCalls ── FIRST │ emitFreeCallFallback ── THEN - │ emitReferencesViaLookup ── LAST (uses handledSites) + │ emitReferencesViaLookup ── uses handledSites + deferred-site skip set + │ emitPropertyDispatchCalls ── registration USES + conservative CALLS + │ emitCallableValueFlow ── assigned/passed callable invocation CALLS │ emitImportEdges ▼ KnowledgeGraph (IMPORTS / CALLS / ACCESSES / INHERITS / USES) @@ -210,6 +212,18 @@ Language-agnostic scope-resolution resolver. This is the resolution path for eve Orchestrator: `runScopeResolution(input, provider)` in `scope-resolution/pipeline/run.ts`. Pipeline phase: `scopeResolutionPhase` in `scope-resolution/pipeline/phase.ts` — iterates the registered `SCOPE_RESOLVERS` over the worker-serialized `ParsedFile`s. (Per-language `emitScopeCaptures` hooks may reuse a cached Tree via the orchestrator's `treeCache`, but in worker-pool runs that cache is empty — Trees can't cross MessageChannels — so they consume the pre-extracted `ParsedFile` instead; § Performance notes.) +### Callable-value flow + +First-class callable values use a language-neutral inclusion analysis in `passes/callable-value-flow.ts`. Providers recognize their own syntax and emit JSON-safe `CallableFlowSite` facts (`seed`, `copy`, `alias`, `address`, `load`, `store`, `formal`, `argument`, and `invoke`) into `ParsedFile`; shared ingestion never branches on a language name. These always-on facts cross workers and the durable parse store, whose schema is bumped whenever their semantic shape changes. + +The emit stage defers only invocation sites proven to reference a flow cell. Ordinary receiver/free/reference passes still resolve direct callees first and record exact callee IDs by file/line/column. Property dispatch then runs before callable flow because a property-dispatched wrapper call can seed actual-to-formal propagation. The callable solver consumes those direct targets, propagates callable sets through lexical cells and formals, and emits `CALLS` at the real indirect invocation site with reason `callable-value-flow` (confidence 0.8 for a singleton, 0.7 for a bounded multi-target set). + +The solver is flow-insensitive but bounded: dependency-indexed work items rerun only when a cell they read changes; target/address sets cap at 32; a hostile fact graph has a finite work budget; overflow or budget exhaustion emits no partial `CALLS` and produces a structured warning. Lexical shadowing is function/block aware, invocation/constructor results are not reinterpreted as callable designators, and overload selection uses provider-supplied signature metadata. C/C++ additionally associate visible prototypes with unique definitions so actual-to-formal flow crosses translation units; the provider-owned `hasFileLocalCallableLinkage` hook prevents `static` declarations or definitions from leaking across files. C++ member-function pointers preserve parameter/cv shape, keep non-virtual targets exact, and expand virtual targets through `MethodDispatchIndex`/MRO. + +Property-key dispatch remains a separate conservative fallback. Its per-key fan-out cap is 32; capped keys synthesize no partial calls and are reported at warning level with language, skipped-key count, dropped key names (bounded), and cap; the count also travels in `RunScopeResolutionStats.propertyDispatchSkippedKeys`. + +Standalone (regex-based) providers such as COBOL participate via `ScopeResolver.scopeResolutionEdgeMode: 'callable-flow-only'`: `runScopeResolution` runs for them, but every ordinary emission path — heritage, interface implementations, receiver-bound, free-call fallback, reference/import edges, post-resolution hooks — is gated off, so their legacy phase (e.g. `cobolPhase`) remains the sole owner of structural edges and the callable solver's `CALLS` are purely additive. A callable-flow-only provider whose files emitted no callable facts exits early, before finalize, keeping the opt-in proportional to source scanning. + ### Optional CFG/PDG emission (`--pdg`, #2081–#2086) On a `--pdg` run the parse worker builds a per-function control-flow graph from the tree-sitter AST (`LanguageProvider.cfgVisitor`; TypeScript/JavaScript today) and serializes it onto `ParsedFile.cfgSideChannel` as plain data. Scope-resolution then emits the program-dependence layers from that side-channel **inside Phase 4 of `runScopeResolution`, while the disk-backed ParsedFile store is still live** — the only window where the worker-built CFGs are loaded (the store is cleared right after the phase returns). A standalone post-`mro` phase would read an empty store, so the emit deliberately lives in-phase, mirroring the `applyCaptureSideChannel` pattern. The opt-in is off by default (graph byte-identical), folded into the parse-cache key (a pdg-off warm cache is never reused on a `--pdg` run), and each layer is bounded by a per-function edge cap that logs any dropped edges. All layers are `BasicBlock → BasicBlock` edges in the single `CodeRelation` table, keyed by `type`; there is **no** `Function → BasicBlock` edge — the symbol↔block join is reconstructed from the BasicBlock id prefix + line span. The layers build on each other: @@ -242,6 +256,7 @@ Single interface a language implements to plug into the pipeline. Contract fully | `collapseMemberCallsByCallerTarget?` | One CALLS edge per (caller, target) instead of per-site — default off | | `populateNamespaceSiblings?` | Cross-file implicit visibility (compiler-implicit namespace sharing) — default off; ctx carries `treeCache` | | `hoistTypeBindingsToModule?` | Walk up to Module scope when looking up a method's return-type typeBinding — default off; enable only when bindings are stored at module level | +| `hasFileLocalCallableLinkage?` | Precise internal-linkage predicate used only when joining callable declarations/prototypes to cross-file definitions; C/C++ use it for `static` free functions | ### Per-language registration @@ -273,6 +288,7 @@ CI auto-discovers the set via `tsx`. No workflow edit required. - **Cross-phase Tree cache**: the orchestrator's `treeCache` (`RunScopeResolutionInput.treeCache`) lets a scope-resolution per-language hook (`emitScopeCaptures`) reuse a tree instead of re-parsing. Workers leave it empty — Trees can't cross MessageChannels — so in normal (worker-pool) runs scope-resolution does NOT rely on it: workers serialize each file's `ParsedFile` (+ capture side-channel) and stream them in, so scope-resolution consumes the pre-extracted artifact rather than re-parsing on the main thread (§ Chunked parse-and-resolve). `PROF_SCOPE_RESOLUTION=1` emits hit/miss counters and a worker-engaged warning. - **Typed relationship iteration**: heritage + MRO walk only the EXTENDS / IMPLEMENTS / HAS_METHOD edges via `iterRelationshipsByType`, not the full relationship map. - **Workspace-resolution-index**: O(1) `findOwnedMember` / `findExportedDef` / `classScopeByDefId` built once per run. +- **Callable-value worklist**: dependency-indexed inclusion propagation is linear in a reverse-ordered copy-chain fixture; target/address sets cap at 32 and the whole worklist has a finite budget with no partial edge emission on exhaustion. - **SCC-ordered cross-file return-type propagation** (PR #1050): `propagateImportedReturnTypes` walks `indexes.sccs` in reverse-topological order (leaves first), so multi-hop alias chains like `models.User → service.user → app.user` collapse to the terminal class in a single linear pass. Within each importer, the source module's `typeBindings` is chain-followed BEFORE mirroring (so we mirror terminal types, not intermediate refs), and the importer's own `typeBindings` is chain-followed AFTER mirroring (so local `const x = importedFn()` resolves before downstream importers run). Cyclic SCCs reach a partial fixpoint within a single pass without iterating to convergence — see the `ts-circular` cross-file-binding fixture which only asserts pipeline-no-throw. PROF output (`PROF_SCOPE_RESOLUTION=1`) splits `finalize` from `propagate` so quadratic regressions in the chain-follow surface independently. --- diff --git a/docs/plans/2026-07-11-001-fix-claude-skill-discovery-plan.md b/docs/plans/2026-07-11-001-fix-claude-skill-discovery-plan.md deleted file mode 100644 index 364ada8c3..000000000 --- a/docs/plans/2026-07-11-001-fix-claude-skill-discovery-plan.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: Claude Skill Discovery Paths - Plan -type: fix -date: 2026-07-11 -artifact_contract: ce-unified-plan/v1 -artifact_readiness: implementation-ready -product_contract_source: ce-plan-bootstrap -execution: code ---- - -# Claude Skill Discovery Paths - Plan - -## Goal Capsule - -- **Objective:** Make every Claude Code skill written by `gitnexus analyze` discoverable from the project skill root while preserving skip flags, repeat-run stability, and unrelated user skills. -- **Authority:** GitHub issue #2433 and Claude Code's documented project-skill layout are the behavioral contract; repository guardrails and existing CLI conventions govern implementation. -- **Execution profile:** Standard, test-first bug fix in `gitnexus/`; no dependency, schema, or public MCP changes. -- **Stop conditions:** Stop if the fix requires deleting unrecognized user-owned skill directories, changes `--skip-skills` semantics, or impact analysis reports HIGH/CRITICAL risk without maintainer approval. -- **Tail ownership:** LFG owns simplification, review, commits, PR creation, and CI follow-through after the implementation units pass verification. - ---- - -## Product Contract - -### Summary - -Install standard and repo-generated Claude Code skills as direct children of `.claude/skills/`, update all generated references and CLI messages to those paths, and migrate known legacy GitNexus outputs without touching unrelated project skills. - -### Problem Frame - -`gitnexus analyze` currently writes standard skills below `.claude/skills/gitnexus/` and community skills below `.claude/skills/generated/`. -Claude Code treats `.claude/skills//SKILL.md` as the project-skill shape; nested `.claude/skills/` directories elsewhere in a monorepo are separate discovery roots, not grouping directories inside a skill root. -The current installer therefore reports success and writes managed instructions that point to files, but the skills are not registered for invocation. - -### Requirements - -**Standard skills** - -- R1. Each bundled `gitnexus-*` standard skill is written to `.claude/skills//SKILL.md`. -- R2. Generated AGENTS.md and CLAUDE.md routing rows reference the same direct standard-skill paths. - -**Community skills** - -- R3. Each `--skills` community skill is written directly below `.claude/skills/` with a GitNexus-owned name that cannot collide with the six standard skills or ordinary unprefixed project skills. -- R4. Community skill frontmatter, returned metadata, console output, and generated routing rows use one consistent discoverable name and path. - -**Migration and compatibility** - -- R5. A repeat analyze removes or replaces only legacy directories GitNexus can identify as its own output and preserves unrelated `.claude/skills/` entries. -- R6. `--skip-skills` continues to suppress only the six standard skills, while `--skills` community generation remains independent; `--index-only` continues to suppress all context-file injection. -- R7. CLI help and localized help text describe the corrected paths without changing flag behavior. -- R8. This repository's checked-in copies of the six standard skills and its managed AGENTS.md/CLAUDE.md routing rows use the corrected direct layout when the fix lands. - -### Acceptance Examples - -- AE1. Given a clean repository, a normal analyze creates `.claude/skills/gitnexus-exploring/SKILL.md`, does not create `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md`, and emits the direct path in AGENTS.md and CLAUDE.md. -- AE2. Given `analyze --skills`, each generated community skill has a direct, namespaced directory below `.claude/skills/`, and the context-file table points to that exact file. -- AE3. Given existing unrelated project skills plus legacy GitNexus grouping directories, rerunning analyze preserves the unrelated skills, produces the direct GitNexus skills, and leaves no managed reference pointing at a legacy grouped path. -- AE4. Given `--skip-skills`, no standard `gitnexus-*` skill is installed or referenced, while generated community skill behavior remains available when `--skills` is also requested. - -### Success Criteria - -- All standard and generated skill files use Claude Code's documented direct-child layout. -- Generated context, return messages, help text, and tests contain no active references to `.claude/skills/gitnexus/` or `.claude/skills/generated/`. -- The canonical repository no longer ships the six standard skills or managed routing rows in the broken grouped layout. -- Repeated runs are deterministic and do not delete unrelated user skills. - -### Scope Boundaries - -- **In scope:** project-local Claude Code skill installation performed by `analyze`, repo-generated community skills, managed context paths, the repository's checked-in copies of the six standard skills, CLI/help copy, migration of known legacy outputs, and regression coverage. -- **Out of scope:** global `gitnexus setup` targets, plugin skill layouts, changing the six bundled skill bodies, or changing Claude Code itself. -- **Deferred to follow-up work:** relocating this repository's three extra hand-maintained nested `.claude/skills/gitnexus/` skills that are not installed by `analyze`; those are workspace configuration rather than the issue's six standard installer outputs. - -### Sources - -- GitHub issue #2433: `https://github.com/abhigyanpatwari/GitNexus/issues/2433` -- Claude Code skills documentation: `https://code.claude.com/docs/en/slash-commands` -- Related path-contract regressions: GitHub issues #1098 and #1381. - ---- - -## Planning Contract - -### Key Technical Decisions - -- KTD1. Treat `.claude/skills/` as the installation root and make each skill directory its direct child. This matches the official project-skill contract and avoids relying on recursive discovery that Claude Code does not document. -- KTD2. Keep the six standard names unchanged because they are already `gitnexus-*` namespaced. This preserves their intended invocation names while correcting only the filesystem layout. -- KTD3. Reserve a separate GitNexus-owned prefix for generated community skill names before writing them flat. This prevents a community such as `Cli` from overwriting `gitnexus-cli` and prevents common labels such as `auth` from replacing user skills. -- KTD4. Replace grouped-directory cleanup with ownership-bounded cleanup. Remove known standard legacy children and generated legacy output, or direct generated directories carrying the reserved prefix, but never recursively clear `.claude/skills/` itself. -- KTD5. Keep generation and documentation derived from the same `GeneratedSkillInfo.name` value so disk paths, frontmatter names, managed routing rows, and repeat-run cleanup cannot drift. - -### Assumptions - -- Issue #2433's request to check community skills includes fixing them in this PR rather than filing a separate follow-up. -- Legacy `.claude/skills/generated/` is GitNexus-owned because current code already deletes and recreates it on every `--skills` run; unknown siblings under `.claude/skills/` remain user-owned. -- Standard legacy cleanup is limited to the six bundled names under `.claude/skills/gitnexus/`; unknown children in that grouping directory are preserved. -- The exact generated-skill prefix may be refined during implementation, but it must be stable, GitNexus-owned, direct-child compatible, and non-conflicting with standard skill names. - -### Existing Patterns to Follow - -- `gitnexus/src/cli/setup.ts` installs globally scoped Claude skills directly under the target skill root and provides a path-contract precedent. -- `gitnexus/src/cli/ai-context.ts` already centralizes standard skill definitions, context table generation, skip semantics, and best-effort filesystem handling. -- `gitnexus/src/cli/skill-gen.ts` already owns community-name normalization, deterministic collision suffixes, output cleanup, frontmatter rendering, and returned path metadata. -- `gitnexus/test/unit/ai-context.test.ts` uses temporary repositories to prove file layout and skip-mode behavior. -- `gitnexus/test/unit/skill-gen.test.ts` and `gitnexus/test/integration/skills-e2e.test.ts` cover generated skill metadata, file contents, idempotency, and end-to-end context references. - -### System-Wide Impact - -The change affects the user-visible filesystem contract of `gitnexus analyze`, generated AGENTS.md/CLAUDE.md content, CLI help output, and the invocation names of previously inert community skills. -It does not alter indexing, graph storage, MCP APIs, global setup targets, or runtime analysis behavior. - -### Risks and Mitigations - -- **Accidental user-skill deletion:** Scope cleanup to known standard names, the prior generated output directory, and the new reserved prefix; add preservation tests with unrelated directories. -- **Standard/community collision:** Use distinct namespaces and assert representative `Cli`/common-label cases. -- **Path drift across surfaces:** Derive context rows from returned generated names and assert exact disk-to-doc parity. -- **Skip-mode regression:** Retain focused tests for normal, `--skip-skills`, `--skills`, and `--index-only` combinations. - ---- - -## Implementation Units - -### U1. Flatten standard skill installation and managed references - -- **Goal:** Install the six bundled skills as direct project skills and migrate only their known legacy copies. -- **Requirements:** R1, R2, R5, R6; AE1, AE3, AE4. -- **Dependencies:** None. -- **Files:** `gitnexus/src/cli/ai-context.ts`, `gitnexus/test/unit/ai-context.test.ts`. -- **Approach:** Change the standard install root and routing-table templates together; preserve `skipSkills` behavior and result reporting; add bounded cleanup for the six known legacy child directories while preserving unknown siblings and unrelated direct skills. -- **Execution note:** Start with failing temporary-repository assertions for the direct path, absence of the legacy path, preservation of unrelated skills, and repeated-run behavior. -- **Patterns to follow:** Existing `installSkills`, `generateGitNexusContent`, and temporary-directory tests in `ai-context.test.ts`. -- **Test scenarios:** - - Covers AE1. A default run writes all six direct skill files and emits the same direct paths in both context files. - - Covers AE3. A run with an unrelated direct skill and an unknown legacy-group child preserves both while replacing known legacy standard children. - - Covers AE4. `skipSkills` writes no standard direct skill, emits no standard routing row, and reports the corrected skipped location. - - A second default run produces the same six skills without duplicate directories or context rows. -- **Verification:** Focused AI-context tests prove the filesystem, managed-document, migration, and skip contracts. - -### U2. Flatten and namespace generated community skills - -- **Goal:** Make `--skills` outputs discoverable without colliding with standard or user-authored skills. -- **Requirements:** R3, R4, R5, R6; AE2, AE3, AE4. -- **Dependencies:** U1 establishes the shared direct-root convention. -- **Files:** `gitnexus/src/cli/skill-gen.ts`, `gitnexus/test/unit/skill-gen.test.ts`, `gitnexus/test/integration/skills-e2e.test.ts`, `gitnexus/test/unit/analyze-no-stats-bridge.test.ts`, `gitnexus/test/unit/analyze-gitnexusrc.test.ts`. -- **Approach:** Generate reserved, deterministic community names; write each directory directly under `.claude/skills/`; clean only legacy generated output and stale directories in the reserved namespace; return and render the direct path consistently; update mocked path fixtures that model the output contract. -- **Execution note:** Characterize existing name normalization and idempotency first, then add red tests for a community label that would collide with a standard or common user skill. -- **Patterns to follow:** `toKebabName`, `renderSkillMarkdown`, and existing repeat-run tests. -- **Test scenarios:** - - Covers AE2. A representative community produces a direct namespaced directory whose basename equals frontmatter `name` and returned metadata `name`. - - A `Cli` community does not overwrite the standard `gitnexus-cli` skill. - - A pre-existing unrelated `.claude/skills/auth/SKILL.md` survives generation of an Auth community. - - Covers AE3. A repeat run removes stale GitNexus-generated community directories and the legacy `generated/` output while preserving unrelated direct skills. - - The end-to-end `analyze --skills` fixture finds generated files at direct paths and context tables point to those exact paths on first and second runs. -- **Verification:** Unit and integration tests prove collision resistance, ownership-bounded cleanup, path/frontmatter parity, and deterministic regeneration. - -### U3. Align CLI help and path-contract assertions - -- **Goal:** Remove stale user-facing descriptions of grouped skill directories and lock the corrected contract into CLI coverage. -- **Requirements:** R7 and the active-reference portion of R2/R4. -- **Dependencies:** U1 and U2 determine the final standard and generated naming conventions. -- **Files:** `gitnexus/src/cli/index.ts`, `gitnexus/src/cli/i18n/zh-CN.ts`, `gitnexus/test/unit/skip-git-cli.test.ts`, `gitnexus/test/unit/ai-context.test.ts`, `gitnexus/test/integration/skills-e2e.test.ts`. -- **Approach:** Update English and Chinese help copy and strengthen existing help/context assertions so legacy grouped paths fail tests if reintroduced. -- **Patterns to follow:** Existing Commander option descriptions, `help.option.analyze.*` translation keys, and `skip-git-cli.test.ts` help assertions. -- **Test scenarios:** - - `gitnexus analyze --help` names the direct standard location and the reserved direct community naming convention. - - Generated AGENTS.md and CLAUDE.md contain no active `.claude/skills/gitnexus/` or `.claude/skills/generated/` routing entries. - - Chinese help retains the same flag semantics while naming corrected locations. -- **Verification:** Focused CLI/help tests and repository search confirm stale active path copy is gone from changed runtime and test surfaces. - -### U4. Align the repository's checked-in standard skills - -- **Goal:** Ensure the canonical GitNexus checkout demonstrates the same discoverable layout the corrected analyzer produces. -- **Requirements:** R8 and the repository-facing portion of R2. -- **Dependencies:** U1 establishes the standard direct paths. -- **Files:** `.claude/skills/gitnexus-exploring/SKILL.md`, `.claude/skills/gitnexus-debugging/SKILL.md`, `.claude/skills/gitnexus-impact-analysis/SKILL.md`, `.claude/skills/gitnexus-refactoring/SKILL.md`, `.claude/skills/gitnexus-guide/SKILL.md`, `.claude/skills/gitnexus-cli/SKILL.md`, `AGENTS.md`, `CLAUDE.md`. -- **Approach:** Relocate exactly the six analyzer-installed standard skill directories from the grouped path to direct children and update only their managed routing rows; preserve the extra hand-maintained nested skills unchanged. -- **Patterns to follow:** The direct paths produced by U1 and the existing GitNexus-managed block markers in AGENTS.md and CLAUDE.md. -- **Test scenarios:** Test expectation: none -- this unit relocates checked-in skill assets without changing their bodies; repository search and the focused path-contract tests cover their discoverability contract. -- **Verification:** Each of the six direct files exists with unchanged content, the six legacy grouped copies are absent, the three extra nested skill directories remain, and both managed tables point to the direct files. - ---- - -## Verification Contract - -| Gate | Command | Proves | -|---|---|---| -| Focused standard installer | `cd gitnexus && npx vitest run test/unit/ai-context.test.ts` | Direct standard paths, managed rows, migration safety, skip flags | -| Focused community generator | `cd gitnexus && npx vitest run test/unit/skill-gen.test.ts` | Namespacing, collision handling, cleanup, metadata/frontmatter parity | -| CLI help | `cd gitnexus && npx vitest run test/unit/skip-git-cli.test.ts` | User-facing flag path contract | -| Community end to end | `cd gitnexus && npx vitest run test/integration/skills-e2e.test.ts` | Real analyze output and repeat-run references across fixtures | -| CLI/Core regression | `cd gitnexus && npm test` | Full package behavior | -| Type safety | `cd gitnexus && npx tsc --noEmit` | TypeScript contract integrity | -| Change scope | GitNexus `detect_changes` before each commit | Only expected CLI skill-generation symbols and flows are affected | - ---- - -## Definition of Done - -- U1-U3 requirements and test scenarios pass. -- U4's checked-in relocation and managed-row verification pass. -- Standard and community skills are direct children of `.claude/skills/` and discoverable by documented Claude Code rules. -- No runtime or generated-document surface points to the two legacy grouping layouts. -- Unrelated user-authored skills and unknown legacy-group children are preserved by regression tests. -- `--skip-skills`, `--skills`, and `--index-only` retain their documented independence. -- Full `gitnexus` tests and typecheck pass, or any environment-only exception is documented with focused proof. -- GitNexus change detection reports only the expected CLI generation and test scope. -- Abandoned experimental code and temporary artifacts from implementation are absent from the final diff. diff --git a/docs/plans/2026-07-11-gitnexus-plan-fix-2432-napi-abort-cpp-extraction.md b/docs/plans/2026-07-11-gitnexus-plan-fix-2432-napi-abort-cpp-extraction.md deleted file mode 100644 index 702d4c3d3..000000000 --- a/docs/plans/2026-07-11-gitnexus-plan-fix-2432-napi-abort-cpp-extraction.md +++ /dev/null @@ -1,228 +0,0 @@ -# GitNexus Engineering Plan - -> Task: Fix #2432 — `analyze` aborts with `Napi::Error` SIGABRT on triton-lang/triton: pathological C++ capture extraction triggers worker timeouts, then worker termination lands mid-native-call. -> Evidence verified at commit 737a8cdb; GitNexus index refreshed this session (`node .gitnexus/run.cjs analyze --index-only --pdg`, 230,253 nodes / 487,362 edges). Deepened same session: assumption A1 empirically refuted (see §5a/§12); §6-C redesigned accordingly. (Note: the MCP context resource still displays a stale banner after refresh — tools serve the refreshed data; PDG queries succeed. Cosmetic cache issue, recorded in §12.) - -## 1. Objective - -`gitnexus analyze` on triton (repro: `GITNEXUS_MAX_FILE_SIZE=5120 … analyze --worker-timeout 60`) must complete without SIGABRT. Two stacked defects, both fixed: - -1. **Perf (trigger):** C++ scope-capture extraction is O(calls × args × treeSize) per file — `lib/Dialect/TritonInstrument/IR/FunctionBuilder.cpp` (194 KB, parses in 46 ms) burns **151 s** in the worker; `hip_prof_str.h` (`.h` → cpp provider) burns 116 s. Measured via `--cpu-prof` on the real dist worker `[verified]`. -2. **Crash (abort):** terminating a worker thread that is inside an N-API call — whether via pool shutdown, breaker trip, **or plain process exit** — makes the pending `Napi::Error` escape as an uncaught C++ exception → `std::terminate` → SIGABRT kills the whole CLI (workers are `worker_threads`, shared process). Reproduced 2/2 on origin/main, exit 134 `[verified]`; process-exit variant reproduced directly `[verified]` (§5a). - -Acceptance criteria: triton repro completes (exit 0); `FunctionBuilder.cpp` extraction drops from ~151 s to sub-second; no worker-pool or cpp-resolver test regressions. - -## 2. Current Behaviour - -Per-file worker flow (`parse-worker.ts` `processFileGroup`): parse → `query.matches` → `extractParsedFile` → provider `emitScopeCaptures` (`emitCppScopeCaptures` for `.cpp`/`.h` — `c-cpp.ts:435` maps both) `[verified]`. - -For every call-expression capture, `inferCppCallArgTypeClasses` (`cpp/captures.ts:929`, driven from `:325`) classifies each identifier argument via: - -- `lookupDeclaredTypeClassForIdentifier` (`:1133`) — linear scan of the enclosing scope's children per identifier `[verified]`; -- `lookupFunctionParameterTypeClass` (`:1182`) + `findEnclosingFunctionParameter` (`:1200`) — walk up + param scan per identifier `[verified]`; -- **`isKnownEnumName` (`:1248`) — walks to the AST root and full-tree DFS for `enum_specifier`, per identifier argument** `[verified]`. CPU profile: 87.7 s of 149.9 s inside it; self-time dominated by tree-sitter N-API accessors (`child`/`childCount`/`type`/`unmarshalNode`) `[verified]`. - -Crash path: idle-timeout/give-up paths all pass `'retire'` → `retireWorkerAfterTimeout` (`worker-pool.ts:1188`) defers terminate until the worker posts `sub-batch-done`/`result`/`error` (the #1848 fix) `[verified]`. But: - -- `parse-impl.ts:1123-1125` `finally { await workerPool?.terminate() }` → pool `terminate` (`worker-pool.ts:2025` awaits) → `terminateTrackedWorkers` (`:971-980`) — terminates every live AND retired worker unconditionally `[verified]`. -- `tripBreaker` (`:1303`) fire-and-forgets the same (`void terminateTrackedWorkers`, `:1312`) on breaker trip — including live workers that may be mid-native-parse `[verified]`. -- These are the ONLY two callers `[verified]` (grep; matches the graph's d=1). -- **Existing test `worker-pool-timeout-retire.test.ts:97` asserts the crash-causing contract**: a never-safe retired mock worker gets `terminateCalls === 1` after `pool.terminate()` (`:114-118`); `:155` asserts the same for breaker trips `[verified]`. Both flip intentionally under this fix. -- Run evidence: process died 12 s after the last retire log with no error-path output; the 2-file mini corpus (retired workers finish before shutdown) exits 0 `[verified]`. - -## 3. Relevant Architecture - -- Shared ingestion pipeline is language-agnostic (AGENTS.md): the fix stays inside the cpp language module (`languages/cpp/captures.ts`) and the generic worker pool; no `LanguageProvider` interface change. -- Precedent for exactly this bug class: Go scope-capture re-walk fix (#1848/#1915), Python (#1918), C++ ADL once-built index (#1990) — `languages/c/captures.ts:39-42` documents the pattern `[verified]`. -- The C provider has its own thin `emitCScopeCaptures` (164 lines, no arg-type-class inference) — not affected `[verified]`. -- Workers cluster (76 symbols, 65% cohesion) is self-contained; depth-3 upstream impact of the shutdown change stays entirely inside it `[graph]`. -- `parse-worker.ts:1362-1369` documents the group-catch trap: a throw escaping per-file processing makes the language-group catch drop every remaining file — any new bail path must be caught per-file, never thrown outward `[verified]`. - -## 4. GitNexus Findings - -- `impact {target: emitCppScopeCaptures, direction: upstream, maxDepth: 2}` → 0 dependents, LOW `[graph]`. **Graph/source discrepancy:** the real consumer is the provider-hook indirection (`c-cpp.ts:495 emitScopeCaptures: emitCppScopeCaptures` → `scope-extractor-bridge.ts:41 extractParsedFile`) which the call graph doesn't model. Source wins; internal signature changes are still safe (all hot functions are file-private). -- `impact {target: terminateTrackedWorkers, direction: upstream}` at depth 2 → d=1: `tripBreaker`, `terminate`; deepened at `maxDepth: 3, summaryOnly` → 13 symbols total (d1:2, d2:7, d3:4), risk LOW, all in the Workers module. Key output: `"direct": 2` — both d=1 dependents modified deliberately in §6-C and source-confirmed `[verified]`. -- Related tests located and read: `test/unit/worker-pool-timeout-retire.test.ts` (mock `TimeoutThenHealthyWorker` harness with `terminateCalls`/`unrefCalls` counters and a `delayed-safe-return` mode — supports the new scenarios without factory changes `[verified]`), `test/integration/resolvers/cpp.test.ts` + `c.test.ts` (golden equivalence gate), `test/integration/cpp-adl-benchmark.test.ts` (GITNEXUS_BENCH-gated; template for the new benchmark) `[verified]`. - -## 5. Statement-Level PDG Findings - -- `pdg_query {mode: controls, target: isKnownEnumName}` (28 edges): the DFS body (`:1253-1262`) is control-dependent only on the trivial `typeName === ''` guard (`:1249`, guard:true) and its own loop conditions — **no memoization or early-exit gate exists**; the full-tree walk runs unconditionally on every call `[graph]`, consistent with source `[verified]`. -- `pdg_query {mode: controls, target: terminateTrackedWorkers}` → 0 edges: straight-line, unconditional termination of both worker lists `[graph]`. The crash fix is precisely "add the missing control dependency" (safe-point gate). -- Performance-mode scan: the hot loop's N-API fan-out (`cur.child(i)` per node per DFS per identifier) is the marshalling hotspot (`unmarshalNode` 15.5 s incl.) `[verified via profile]`. -- Ordering constraint: `lookupDeclaredTypeClassForIdentifier` returns the **first** matching `declaration` in scope-child order, with no position filtering relative to the identifier — the replacement index must preserve first-declaration-wins and must NOT introduce use-before-decl filtering `[verified]`. - -## 5a. Deepen finding — A1 refuted empirically - -Driver test (`exit-with-busy-worker.mjs`, kept in scratchpad): main thread `process.exit(0)` five seconds into the real dist worker's extraction of `FunctionBuilder.cpp`, worker `unref()`d → **process aborts: `terminate called after throwing an instance of 'Napi::Error'`, exit 134** `[verified]`. Node tears down worker environments on process exit through the same terminate path. Consequence: "skip terminating unsafe workers and let the process exit" merely relocates the abort. The shutdown design must instead guarantee workers reach a JS safe point in bounded time before the process exits — this promotes the previously-deferred cooperative extraction deadline into scope (§6-D). - -## 6. Proposed Changes - -**A. Perf root-cause — per-file lookup index in `gitnexus/src/core/ingestion/languages/cpp/captures.ts`.** -Introduce a lazily-built, per-invocation index object created at the top of `emitCppScopeCaptures` and threaded through `inferCppCallArgTypes` / `inferCppCallArgTypeClasses` → the lookup helpers (all file-private; no exported API change): - -- `enumNames: Set` — built by ONE root DFS on first `isKnownEnumName` query (lazy: files with no identifier args pay nothing). `isKnownEnumName` becomes a Set lookup. Behavior-identical: current code matches any `enum_specifier` name anywhere in the translation unit. -- `scopeDecls: Map>` — per-scope declaration map built on first lookup in that scope by one pass over `scope` children, first-declaration-wins (skip existing keys). Replaces the per-identifier linear scans in `lookupDeclaredTypeClassForIdentifier` / `lookupDeclaredTypeForIdentifier` (`:1090-1131`). -- `fnParams: Map>` — same treatment for `findEnclosingFunctionParameter`. - -Complexity: O(treeSize + identifiers) per file. Expected: 151 s → sub-second (parse itself is 46 ms). `classifyCppParameterType` / `normalizeCppTypeText` stay per-hit (cheap; memoizing them changes nothing observable). - -**B. New benchmark test — `gitnexus/test/integration/cpp-captures-typeclass-benchmark.test.ts`** modeled exactly on `cpp-adl-benchmark.test.ts` (`describe.skipIf(!GITNEXUS_BENCH)`): synthetic C++ file scaling call-sites × enums, asserts sub-quadratic scaling of the capture-emit phase. - -**C. Crash fix — safe-point-gated shutdown with bounded drain, `gitnexus/src/core/ingestion/workers/worker-pool.ts`.** (Redesigned after §5a.) - -- **C1 (gate):** extend `RetiredWorkerRecord` with `safeToTerminate`, set exactly where `terminateWhenBackInJs` fires today (`onRetiredMessage` for `sub-batch-done`/`result`/`error`, and `messageerror`). `terminateTrackedWorkers` terminates retired records only when safe; unsafe records keep their armed at-safe-point terminate listener. -- **C2 (bounded drain):** pool `terminate()` awaits unsafe retired records' safe-point terminate up to a cap (`GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS`, default ≈ 30 s — comfortably above D's per-file deadline so the drain converges for the known class). On cap expiry: log a clear diagnostic naming the wedged worker + in-flight file and proceed (residual abort risk at process exit remains for truly-wedged native code, now rare and diagnosed). The breaker path (`tripBreaker:1312`) stays fire-and-forget — it must never block the dispatch rejection; its unsafe records drain when the pipeline's `finally` runs pool `terminate()`. -- **C3 (breaker-path live workers):** on breaker trip, live workers in `busySlots` (`:1154` `[verified]`) are routed through `retireWorkerAfterTimeout` instead of direct `terminate()` — same mid-native abort risk, same cure. Idle live workers terminate directly (parked in the JS event loop; safe). The normal post-parse `terminate()` still direct-terminates live workers — all idle by construction (jobs drained). - -**D. Cooperative extraction deadline (promoted from deferred Q2 by §5a) — `cpp/captures.ts`.** -Bound per-file wall time inside `emitCppScopeCaptures`'s match loop: check `Date.now()` against a soft budget (`GITNEXUS_CPP_CAPTURE_BUDGET_MS`, default ≈ 20 s; post-A one iteration is microseconds, so check granularity of every N=64 matches is ample). On breach: **return** partial captures accumulated so far + `reportWarning` naming the file — never throw (the group-catch trap, §3). This guarantees cpp extraction returns to JS in bounded time, which is what makes C2's drain converge and process exit safe. Generic all-language budget remains a deferred follow-up (§12). - -## 7. Implementation Sequence - -1. **cpp captures index (A).** Build the index type + lazy constructors; convert `isKnownEnumName`, `lookupDeclaredType{Class}ForIdentifier`, `lookupFunctionParameterType{Class}`, `findEnclosingFunctionParameter`; thread from `emitCppScopeCaptures`. Gate: `npx vitest run test/integration/resolvers/cpp.test.ts test/integration/resolvers/c.test.ts` passes unchanged. -2. **Benchmark (B).** Add the GITNEXUS_BENCH-gated benchmark; record before/after in the PR body (before: 151 s / 116 s from this plan). -3. **Extraction deadline (D).** Budget check + partial-return + warning; unit test with a tiny budget forcing the bail (assert warning emitted, remaining files in group still processed). -4. **Worker-pool shutdown safety (C1–C3).** Gate + drain + breaker routing. Update `worker-pool-timeout-retire.test.ts:97` and `:155` (both currently assert the buggy contract) and add: (i) `pool.terminate()` with a never-safe retired worker + tiny drain cap → resolves after cap, `terminateCalls === 0`, diagnostic logged; (ii) retired worker signals safe during drain → terminated, `terminate()` resolves promptly; (iii) breaker trip with busy live worker → retired, not direct-terminated. -5. **End-to-end validation.** Rebuild (`npm run build`); re-run the triton repro → exits 0, `FunctionBuilder.cpp` indexed (not quarantined); mini 2-file corpus completes in seconds; re-run the §5a exit-with-busy-worker driver against the built worker with D's budget lowered → clean exit. - -Steps 1–2 alone de-trigger #2432; 3–4 close the abort class. Each step leaves the tree green. - -## 8. Test Strategy - -- **Update:** `worker-pool-timeout-retire.test.ts:97` + `:155` — expectations flip to the new contract (unsafe ⇒ not terminated at shutdown; terminated at safe point). The mock harness supports this as-is `[verified]`. -- **Add:** benchmark (§6-B); three shutdown cases (§7-4); deadline-bail unit test (§7-3). -- **Regression:** resolver goldens `cpp.test.ts`/`c.test.ts` unchanged (equivalence gate); full `npm run test:unit`; `npm run test:integration` (carries its build via `pretest:integration`). -- **Edge cases:** file with enums but no calls (lazy index never built); duplicate declaration in one scope (first-wins preserved); use-before-decl in scope (still resolved — no position filter); anonymous enums (name-less `enum_specifier` excluded, same as today); breaker trip with mixed busy/idle live workers; drain cap = 0 (immediate proceed); deadline breach mid-file (partial captures kept, group continues). -- **Failure paths:** shutdown never hangs (drain is capped); deadline bail is a warning, never a group-dropping throw (§3 trap). -- **Verification commands (verified to exist):** `npm run build`, `npm run test:unit`, `npm run test:integration`, `GITNEXUS_BENCH=1 npx vitest run test/integration/cpp-captures-typeclass-benchmark.test.ts` — all from `gitnexus/`. - -## 9. Risk and Impact Analysis - -- **d=1 dependents of `terminateTrackedWorkers`** — `tripBreaker` (`:1312`), `terminate` (`:2025`): both modified deliberately; no other callers `[verified]`. Depth-3 radius stays pool-internal (13 symbols, Workers module) `[graph]`. -- **Behavioral-equivalence risk (A):** first-declaration-wins + position-free matching must be preserved (§5). Mitigation: resolver goldens + explicit edge cases. -- **Node identity:** key maps by `SyntaxNode.id` (stable within a tree); wrapper object identity is NOT usable (wrappers are recreated per access — a `WeakMap` would silently fail). -- **Drain-cap tuning (C2 vs D):** drain cap must exceed D's budget or the drain can expire while a worker is legitimately finishing its bailed file — defaults 30 s vs 20 s encode that; both env-tunable, relation asserted in a unit test comment. -- **Residual abort window:** a worker wedged in native code longer than the drain cap still aborts at process exit — now requires non-cpp pathological input (D bounds cpp) and is logged with the culprit file before it can happen. Accepted; full elimination needs child-process workers (out of scope, §12). -- **`emitCppScopeCaptures` consumers:** provider hook only; signature unchanged (D's budget read from env inside the module) — zero external surface. -- **Deadline false positives (D):** 20 s default is ~3 orders of magnitude above post-A extraction cost of the worst observed file; breach ⇒ degraded coverage for that file (warning), never a failed run. -- **Coverage change:** triton's `FunctionBuilder.cpp` was previously quarantined; post-fix it indexes — strictly an improvement. -- **Concurrency:** the new index and deadline state are function-scoped per invocation (per file, per worker thread) — no shared state, no `clearCaches()` interaction. - -## 10. Files Expected to Change - -| File | Symbols | Reason | -|---|---|---| -| `gitnexus/src/core/ingestion/languages/cpp/captures.ts` | `emitCppScopeCaptures`, `inferCppCallArgTypes`, `inferCppCallArgTypeClasses`, `lookupDeclaredType{Class}ForIdentifier`, `lookupFunctionParameterType{Class}`, `findEnclosingFunctionParameter`, `isKnownEnumName` (+ index type, + deadline) | A: O(n²)→O(n) index; D: bounded extraction | -| `gitnexus/src/core/ingestion/workers/worker-pool.ts` | `RetiredWorkerRecord`, `retireWorkerAfterTimeout`, `terminateTrackedWorkers`, `terminate`, `tripBreaker` | C1–C3: safe-point gate + bounded drain + breaker routing | -| `gitnexus/test/unit/worker-pool-timeout-retire.test.ts` | `:97`, `:155` + 3 new cases | New shutdown contract | -| `gitnexus/test/integration/cpp-captures-typeclass-benchmark.test.ts` | new | Scaling regression gate | -| `gitnexus/test/unit/` (new file) | cpp capture deadline-bail | D coverage | - -## 11. Reusable Implementation Context - -```yaml -implementation_context: - task_summary: > - Fix #2432 (SIGABRT on triton analyze): (A) replace per-identifier full-tree/ - per-scope AST re-walks in cpp capture extraction with a lazily-built per-file - index; (C) gate worker terminate on a JS-safe-point flag with a bounded - shutdown drain and breaker-path retire routing; (D) bound cpp capture - extraction wall-time per file (partial-return + warning, never throw). - acceptance_criteria: - - "Triton repro (avoid[4] artifacts) exits 0, no Napi::Error abort" - - "FunctionBuilder.cpp capture extraction sub-second (was 151s)" - - "resolvers/cpp.test.ts + worker-pool suites green" - - "exit-with-busy-worker driver (avoid[4]) exits cleanly against built worker" - primary_symbols: - - { symbol: isKnownEnumName, file: gitnexus/src/core/ingestion/languages/cpp/captures.ts, lines: "1248-1265", role: "full-tree DFS per identifier — replace with per-file enum-name Set" } - - { symbol: lookupDeclaredTypeClassForIdentifier, file: gitnexus/src/core/ingestion/languages/cpp/captures.ts, lines: "1133-1172", role: "per-identifier scope scan — replace with per-scope decl map; preserve first-wins, position-free" } - - { symbol: lookupFunctionParameterTypeClass, file: gitnexus/src/core/ingestion/languages/cpp/captures.ts, lines: "1182-1224", role: "per-identifier param walk — memoize per function node.id" } - - { symbol: inferCppCallArgTypeClasses, file: gitnexus/src/core/ingestion/languages/cpp/captures.ts, lines: "929-1010", role: "per-call driver — threads the index down; call sites at 311/325" } - - { symbol: emitCppScopeCaptures, file: gitnexus/src/core/ingestion/languages/cpp/captures.ts, lines: "15-", role: "per-file entry — owns index lifetime + D deadline checks in its match loop" } - - { symbol: terminateTrackedWorkers, file: gitnexus/src/core/ingestion/workers/worker-pool.ts, lines: "971-980", role: "add safeToTerminate gate (C1); callers: tripBreaker :1312 (void), terminate :2025 (await) — the only two" } - - { symbol: retireWorkerAfterTimeout, file: gitnexus/src/core/ingestion/workers/worker-pool.ts, lines: "1188-1245", role: "set safeToTerminate where terminateWhenBackInJs fires; unref already at :1240" } - - { symbol: tripBreaker, file: gitnexus/src/core/ingestion/workers/worker-pool.ts, lines: "1303-1315", role: "C3: retire busySlots members instead of direct terminate; stays fire-and-forget" } - - { symbol: "pool terminate", file: gitnexus/src/core/ingestion/workers/worker-pool.ts, lines: "~2010-2027", role: "C2: bounded drain of unsafe records before/instead of force terminate" } - related_symbols: - - { symbol: extractParsedFile, relationship: "CALLS emitScopeCaptures via provider hook", relevance: "graph-invisible consumer; signature unchanged" } - - { symbol: "parse-impl.ts:1124 finally", relationship: CALLS, relevance: "the shutdown trigger; C2 drain runs under this await" } - - { symbol: "c-cpp.ts:435 extensions", relationship: config, relevance: ".h routes to cpp provider — hip_prof_str.h covered by A+D" } - - { symbol: busySlots, relationship: "state read by C3", relevance: "worker-pool.ts:1154; add/delete sites verified at :1631/:1646/:1676/:1687/:1720/:1814" } - execution_path: - - "worker: parse file → query.matches → extractParsedFile → emitCppScopeCaptures" - - "per call capture: inferCppCallArgTypeClasses → per identifier: scope scan + full-tree enum DFS (hot)" - - "worker exceeds idle timeout → retire (no terminate) → parse ends → parse-impl finally → pool.terminate → terminateTrackedWorkers → terminate mid-N-API → SIGABRT" - - "ALSO: process exit with native-busy unref'd worker → same abort (verified) — why C2+D exist" - pdg_constraints: - - description: "isKnownEnumName full-tree DFS gated only by typeName!=='' (guard, line 1249); no memo gate exists" - affected_statements: ["gitnexus/src/core/ingestion/languages/cpp/captures.ts:1253-1262"] - implementation_consequence: "Set lookup is behavior-identical; keep the empty/'unknown' early-out" - - description: "terminateTrackedWorkers is straight-line (0 CDG edges) — terminates unconditionally" - affected_statements: ["gitnexus/src/core/ingestion/workers/worker-pool.ts:975-977"] - implementation_consequence: "add safeToTerminate control dependency; drain bounded, never unbounded await" - - description: "lookupDeclaredTypeClassForIdentifier: first-declaration-wins, position-free scope match" - affected_statements: ["gitnexus/src/core/ingestion/languages/cpp/captures.ts:1148-1170"] - implementation_consequence: "build per-scope map in child order, skip existing keys, no use-before-decl filtering" - architectural_patterns: - - { pattern: "once-built per-file index over repeated AST walks", example_location: "gitnexus/src/core/ingestion/languages/c/captures.ts:39-42 (comment citing go #1848 / python #1918); ADL index #1990", usage_guidance: "thread an index object; key node maps by SyntaxNode.id, never object identity" } - - { pattern: "GITNEXUS_BENCH-gated scaling benchmark", example_location: "gitnexus/test/integration/cpp-adl-benchmark.test.ts", usage_guidance: "copy harness shape incl. skipIf + table output" } - - { pattern: "mock-Worker retire harness", example_location: "gitnexus/test/unit/worker-pool-timeout-retire.test.ts:12-64", usage_guidance: "TimeoutThenHealthyWorker: terminateCalls/unrefCalls counters + 'delayed-safe-return' mode cover all new cases; no factory change needed" } - - { pattern: "per-file bail must not throw", example_location: "gitnexus/src/core/ingestion/workers/parse-worker.ts:1362-1369 (CFG isolation comment)", usage_guidance: "D returns partial captures + reportWarning; a throw drops the whole language group" } - files_to_modify: - - { file: gitnexus/src/core/ingestion/languages/cpp/captures.ts, symbols: [see primary], intended_change: "A index + D deadline" } - - { file: gitnexus/src/core/ingestion/workers/worker-pool.ts, symbols: [see primary], intended_change: "C1 gate, C2 drain, C3 breaker routing" } - - { file: gitnexus/test/unit/worker-pool-timeout-retire.test.ts, symbols: [], intended_change: "flip :97/:155 + 3 new cases" } - - { file: gitnexus/test/integration/cpp-captures-typeclass-benchmark.test.ts, symbols: [], intended_change: "new benchmark" } - tests: - - file: gitnexus/test/unit/worker-pool-timeout-retire.test.ts - scenarios: - - "never-safe retired worker + tiny drain cap → pool.terminate() resolves after cap, terminateCalls === 0, diagnostic logged" - - "retired worker signals safe during drain → terminated, terminate() resolves promptly" - - "breaker trip with busy live worker → routed through retire, not direct terminate" - - "UPDATED :97/:155 — unsafe workers not terminated at shutdown (was: terminated)" - - file: gitnexus/test/integration/cpp-captures-typeclass-benchmark.test.ts - scenarios: ["N call-sites × M enums synthetic file → capture emit scales sub-quadratically"] - - file: "gitnexus/test/unit/ (new: cpp capture deadline test)" - scenarios: ["GITNEXUS_CPP_CAPTURE_BUDGET_MS=1 on a many-call file → partial captures returned, warning emitted, no throw"] - - file: gitnexus/test/integration/resolvers/cpp.test.ts - scenarios: ["existing golden behavior unchanged (equivalence gate — run, don't modify)"] - verification_commands: - - "cd gitnexus && npm run build" - - "cd gitnexus && npm run test:unit" - - "cd gitnexus && npm run test:integration" - - "cd gitnexus && GITNEXUS_BENCH=1 npx vitest run test/integration/cpp-captures-typeclass-benchmark.test.ts" - risks: - - "equivalence break in decl ordering (first-wins) → resolver goldens catch" - - "drain cap must exceed D budget (30s > 20s) or drains expire on legitimately-bailing workers" - - "SyntaxNode object identity is NOT stable — key by node.id" - - "residual abort: non-cpp native wedge longer than drain cap still aborts at exit — logged, accepted (child-process workers out of scope)" - assumptions: - - "D's env-read (GITNEXUS_CPP_CAPTURE_BUDGET_MS) is visible in worker threads — CHECK: workers inherit process.env by default; confirm no env filtering in spawnWorker (worker-pool.ts:909-925 sets only workerData/resourceLimits — none seen)" - open_questions: - - "Q2 (narrowed): generic all-language extraction budget — deferred follow-up issue after cpp-only D lands" - avoid: - - "Do not repeat full repository discovery — symbols and line ranges verified at 737a8cdb" - - "Do not change LanguageProvider or emitScopeCaptures signatures — provider hook consumers are graph-invisible" - - "Do not add position/use-before-decl filtering to scope lookups — changes resolution behavior" - - "Repro artifacts in scratchpad: repro-2432-wt (worktree), triton/, mini-2432/, repro-run{1,2}.log, profiles/CPU.*.cpuprofile, profile-worker.mjs, prof-top.mjs, exit-with-busy-worker.mjs — reuse for §7-5, do not re-derive" - - "Do not let a D bail throw out of emitCppScopeCaptures — the language-group catch drops all remaining files (parse-worker.ts:1362-1369)" - - "Do not edit CHANGELOG (release-time owned)" -``` - -## 12. Assumptions and Open Questions - -- **A1 — RESOLVED (refuted):** process exit with a native-busy unref'd worker DOES abort (§5a, empirical). Design consequence absorbed into §6-C2/§6-D. -- **A2 — RESOLVED:** mock harness supports all new shutdown cases without factory changes (test file read in full). -- **A3 (new, minor):** worker threads see `process.env` for D's budget knob — spawn options set only `workerData`/`resourceLimits`, so default env inheritance applies; executor re-verifies in one line. -- **Q2 (narrowed):** generic per-language extraction budget — file as follow-up issue once cpp-only D proves the shape. -- **Deferred:** `lookupDeclaredTypeForIdentifier` (`:1090`) gets the same index for consistency (cheap, in A) though not hot (0.4 s incl.). -- **Graph/source discrepancies recorded:** (i) provider-hook edges invisible to `impact`; (ii) MCP `context` resource staleness banner not refreshed after `--index-only --pdg` while tools serve fresh data — both worth separate GitNexus issues, not this fix. - -## 13. Definition of Done - -1. `GITNEXUS_HOME= GITNEXUS_LBUG_EXTENSION_INSTALL=never GITNEXUS_MAX_FILE_SIZE=5120 node gitnexus/dist/cli/index.js analyze --worker-timeout 60` on triton-lang/triton exits 0 with no `Napi::Error`/SIGABRT, and `lib/Dialect/TritonInstrument/IR/FunctionBuilder.cpp` appears in the index (not quarantined). -2. Mini 2-file corpus (FunctionBuilder.cpp + hip_prof_str.h) analyzes in seconds (was 318.9 s). -3. The §5a exit-with-busy-worker driver, run against the rebuilt worker, exits cleanly. -4. Updated + new worker-pool unit tests green (including flipped `:97`/`:155` contract); resolver goldens (`cpp.test.ts`, `c.test.ts`) green unchanged; `npm run test:unit` and `npm run test:integration` green in `gitnexus/`. -5. Benchmark demonstrates sub-quadratic capture-emit scaling behind `GITNEXUS_BENCH=1`; deadline-bail test proves partial-return-not-throw. -6. No `LanguageProvider`/public API signature changes; no CHANGELOG edits. diff --git a/gitnexus-shared/src/index.ts b/gitnexus-shared/src/index.ts index 1b1d9c9a9..860558016 100644 --- a/gitnexus-shared/src/index.ts +++ b/gitnexus-shared/src/index.ts @@ -84,6 +84,22 @@ export type { ResolveTypeRefContext } from './scope-resolution/resolve-type-ref. // ScopeExtractor output contracts (RFC §3.2 Phase 1; Ring 2 PKG #919) export type { ParsedFile } from './scope-resolution/parsed-file.js'; export type { ReferenceSite, ReferenceKind, CallForm } from './scope-resolution/reference-site.js'; +export type { + CallableFlowOperand, + CallableFlowExpectedSignature, + CallableFlowPassingMode, + CallableFlowInvocationKind, + CallableFlowSeedSite, + CallableFlowCopySite, + CallableFlowAliasSite, + CallableFlowAddressSite, + CallableFlowStoreSite, + CallableFlowLoadSite, + CallableFlowFormalSite, + CallableFlowArgumentSite, + CallableFlowInvokeSite, + CallableFlowSite, +} from './scope-resolution/callable-flow-site.js'; // Method-dispatch materialized view over HeritageMap (RFC §3.1; Ring 2 SHARED #914) export { buildMethodDispatchIndex } from './scope-resolution/method-dispatch-index.js'; diff --git a/gitnexus-shared/src/scope-resolution/callable-flow-site.ts b/gitnexus-shared/src/scope-resolution/callable-flow-site.ts new file mode 100644 index 000000000..4ecd5fced --- /dev/null +++ b/gitnexus-shared/src/scope-resolution/callable-flow-site.ts @@ -0,0 +1,142 @@ +/** + * Always-on callable-value-flow facts produced during scope extraction. + * + * These records deliberately contain no parser nodes or provider-private + * objects: they cross the worker, disk-cache, and ParsedFile-store boundaries. + * Language providers recognize syntax and emit `@callable-flow.*` captures; + * the central extractor attaches lexical scope ids and materializes this + * language-neutral representation. + */ + +import type { ParameterTypeClass } from './symbol-definition.js'; +import type { Range, ScopeId } from './types.js'; + +/** A lexical binding or abstract indirection cell used by a flow fact. */ +export interface CallableFlowOperand { + /** Provider-normalized lexical binding name. */ + readonly name: string; + readonly inScope: ScopeId; + readonly atRange: Range; + /** Number of dereference operators applied at this occurrence. */ + readonly indirection: number; + /** Whether this occurrence explicitly takes the binding's address. */ + readonly addressOf: boolean; + /** + * Provider-normalized expression role. `binding` is the conservative + * default; the other variants prove the occurrence denotes a callable + * designator rather than a value produced by an expression. + */ + readonly expressionKind?: + | 'binding' + | 'callable-designator' + | 'bound-member' + | 'anonymous-callable'; + /** Qualified spelling retained for receiver/member-aware lookup. */ + readonly qualifiedName?: string; +} + +/** Callable shape used to disambiguate overload sets when syntax supplies it. */ +export interface CallableFlowExpectedSignature { + readonly parameterCount?: number; + readonly parameterTypes?: readonly string[]; + readonly parameterTypeClasses?: readonly ParameterTypeClass[]; + /** C++ member-function cv qualifier when the declarator supplies it. */ + readonly isConst?: boolean; +} + +// 'pointer' is currently behaviorally identical to 'value' in the solver +// (only 'reference' back-propagates); it stays in the union as C/C++ +// provenance. A 'callable-object' passing mode had no producer and was +// dropped (#2522 review) — the invocation-kind 'callable-object' below is a +// different, live concept. +export type CallableFlowPassingMode = 'value' | 'reference' | 'pointer'; + +export interface CallableFlowSeedSite { + readonly kind: 'seed'; + readonly destination: CallableFlowOperand; + /** Simple callable name used for lexical lookup. */ + readonly targetName: string; + /** Optional provider-normalized qualified spelling (for example `Base.method`). */ + readonly targetQualifiedName?: string; + readonly targetRange: Range; + readonly expectedSignature?: CallableFlowExpectedSignature; +} + +export interface CallableFlowCopySite { + readonly kind: 'copy'; + readonly source: CallableFlowOperand; + readonly destination: CallableFlowOperand; +} + +/** True identity (for example a C++ reference), modelled bidirectionally. */ +export interface CallableFlowAliasSite { + readonly kind: 'alias'; + readonly source: CallableFlowOperand; + readonly destination: CallableFlowOperand; +} + +/** `destination` receives the abstract address of `source`. */ +export interface CallableFlowAddressSite { + readonly kind: 'address'; + readonly source: CallableFlowOperand; + readonly destination: CallableFlowOperand; +} + +/** Targets in `source` flow into every abstract cell reached by `pointer`. */ +export interface CallableFlowStoreSite { + readonly kind: 'store'; + readonly source: CallableFlowOperand; + readonly pointer: CallableFlowOperand; +} + +/** Targets in every abstract cell reached by `pointer` flow into `destination`. */ +export interface CallableFlowLoadSite { + readonly kind: 'load'; + readonly pointer: CallableFlowOperand; + readonly destination: CallableFlowOperand; +} + +/** Maps a callable definition's parameter position to its lexical binding. */ +export interface CallableFlowFormalSite { + readonly kind: 'formal'; + readonly ownerName: string; + readonly ownerRange: Range; + readonly parameterIndex: number; + readonly binding: CallableFlowOperand; + readonly passingMode: CallableFlowPassingMode; + readonly expectedSignature?: CallableFlowExpectedSignature; +} + +/** Maps one real call-site argument position to its source operand. */ +export interface CallableFlowArgumentSite { + readonly kind: 'argument'; + readonly callSite: Range; + readonly parameterIndex: number; + readonly source: CallableFlowOperand; + /** Direct callee spelling used when an earlier resolver already emitted the edge. */ + readonly directCalleeName?: string; +} + +export type CallableFlowInvocationKind = 'indirect' | 'member-pointer' | 'callable-object'; + +/** A syntactically indirect invocation owned by callable-value-flow. */ +export interface CallableFlowInvokeSite { + readonly kind: 'invoke'; + readonly callSite: Range; + readonly inScope: ScopeId; + readonly callee: CallableFlowOperand; + readonly receiver?: CallableFlowOperand; + readonly invocationKind: CallableFlowInvocationKind; + readonly arity?: number; +} + +export type CallableFlowSite = + | CallableFlowSeedSite + | CallableFlowCopySite + | CallableFlowAliasSite + | CallableFlowAddressSite + | CallableFlowStoreSite + | CallableFlowLoadSite + | CallableFlowFormalSite + | CallableFlowArgumentSite + | CallableFlowInvokeSite; diff --git a/gitnexus-shared/src/scope-resolution/parsed-file.ts b/gitnexus-shared/src/scope-resolution/parsed-file.ts index 01b70f206..db0833967 100644 --- a/gitnexus-shared/src/scope-resolution/parsed-file.ts +++ b/gitnexus-shared/src/scope-resolution/parsed-file.ts @@ -55,6 +55,7 @@ import type { Scope, ScopeId } from './types.js'; import type { ParsedImport } from './types.js'; import type { SymbolDefinition } from './symbol-definition.js'; import type { ReferenceSite } from './reference-site.js'; +import type { CallableFlowSite } from './callable-flow-site.js'; export interface ParsedFile { readonly filePath: string; @@ -74,6 +75,12 @@ export interface ParsedFile { */ readonly localDefs: readonly SymbolDefinition[]; readonly referenceSites: readonly ReferenceSite[]; + /** + * Compact, always-on facts for calls through assigned/passed callable + * values. Optional so hand-built fixtures and providers with no relevant + * syntax remain source-compatible; consumers normalize absence to `[]`. + */ + readonly callableFlowSites?: readonly CallableFlowSite[]; /** * Opaque, language-private serialization of capture-time side-channel * state that a provider's `emitScopeCaptures` populates into module-level diff --git a/gitnexus-shared/src/scope-resolution/reference-site.ts b/gitnexus-shared/src/scope-resolution/reference-site.ts index b80ed8394..5ced6e176 100644 --- a/gitnexus-shared/src/scope-resolution/reference-site.ts +++ b/gitnexus-shared/src/scope-resolution/reference-site.ts @@ -38,6 +38,17 @@ export type ReferenceKind = | 'type-reference' | 'inherits' | 'import-use' + // An identifier in object-literal property-value position + // (`{ emitScopeCaptures: emitCppScopeCaptures }`, shorthand `{ hook }`). + // Resolution is owned entirely by the post-finalize property-dispatch pass + // (`emitPropertyDispatchCalls` via the callable-gated finalized-bindings + // walker `findCallableBindingInScope`; `resolveReferenceSites` skips these + // sites), so a non-function value never produces a reference. Emitted as a `USES` + // reference edge — NOT `CALLS` (a registration is not an invocation; + // Kythe `ref` / Joern `METHOD_REF` precedent). The invocation side is + // recovered separately by the property-dispatch pass, which uses + // `propertyKey` to synthesize CALLS at member-call sites (#2437). + | 'value-ref' // A macro invocation (`log!(...)` / `vec![...]`). Resolved against // `Macro`-labeled definitions ONLY (see `MacroRegistry`) so a macro // never aliases a same-named free function — macros and functions are @@ -89,6 +100,14 @@ export interface ReferenceSite { readonly explicitReceiver?: { readonly name: string }; /** Argument count at the call site; used by `provider.arityCompatibility`. */ readonly arity?: number; + /** + * Object-literal key under which a `value-ref` site registers its value + * (`{ emitScopeCaptures: emitHook }` → `'emitScopeCaptures'`; shorthand + * `{ emitHook }` → `'emitHook'`). Consumed by the property-dispatch pass + * to connect member-call sites (`x.emitScopeCaptures()`) to registered + * functions (#2437). Only set for `kind === 'value-ref'`. + */ + readonly propertyKey?: string; /** * Inferred argument types at the call site, one per argument. An * empty-string entry means "unknown" — consumers narrowing overload diff --git a/gitnexus-shared/src/scope-resolution/types.ts b/gitnexus-shared/src/scope-resolution/types.ts index 6012694cf..91bc1f22c 100644 --- a/gitnexus-shared/src/scope-resolution/types.ts +++ b/gitnexus-shared/src/scope-resolution/types.ts @@ -453,7 +453,15 @@ export interface Reference { readonly toDef: DefId; /** Location of the reference in source. */ readonly atRange: Range; - readonly kind: 'call' | 'read' | 'write' | 'type-reference' | 'inherits' | 'import-use' | 'macro'; + readonly kind: + | 'call' + | 'read' + | 'write' + | 'type-reference' + | 'inherits' + | 'import-use' + | 'value-ref' + | 'macro'; readonly confidence: number; readonly evidence: readonly ResolutionEvidence[]; } diff --git a/gitnexus/bench/python-scope/baseline-fingerprint.txt b/gitnexus/bench/python-scope/baseline-fingerprint.txt index 13a0b5ee3..3853b2f93 100644 --- a/gitnexus/bench/python-scope/baseline-fingerprint.txt +++ b/gitnexus/bench/python-scope/baseline-fingerprint.txt @@ -1 +1 @@ -c03f87cd8cd1ee716dea93cceb27109ee5b4584bc9fe426eea3f18fd6f9854cc +a99e69ab2dfb897ed771c6a8e29c5b32843a7f734db701e0699afc07c090e4d5 diff --git a/gitnexus/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json index 3a16d856a..15e8b2f88 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -1,89 +1,128 @@ { "_comment": "Per-language baselines for bench/scope-capture/measure.mjs --check. fingerprint = order-independent sha256 over the lang-resolution/-* fixture corpus + a 20-entity synthetic source (correctness gate; re-baseline intentionally on a legitimate capture change). scaling_budget = max allowed (t800/t250)/(800/250); ~1.0 is linear, ~3.2 is quadratic. The synthetic source is now HERITAGE-BEARING for every language (each Entity extends/implements/embeds/uses-trait/conforms-to a shared base) so the #1951 @reference.inherits synth is gated at scale, not just the base capture loop. All languages thread the tree-sitter captured node instead of re-deriving it with findNodeAtRange(tree.rootNode,...) per match, so all are linear (go #1915, python #1918, ruby/php/rust/csharp #1951, java #1956).", "go": { - "fingerprint": "09ecd94911b830f52fa8807560abcbd79f163d02a2072870c1a59297e9a326e1", + "fingerprint": "57b3c55135af8d2af33b9a7c4bf89796a7bee5b5822b402a2dea91af7232cf4a", "scaling_budget": 1.5, + "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 3d4e32e7490c830516126e28931827949baa3594cb521f7a3d8dcfed95b6018a -> 57b3c55135af8d2af33b9a7c4bf89796a7bee5b5822b402a2dea91af7232cf4a; scaling 1.058 < 1.5.", + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: provider-owned callable assignment/copy/formal/argument/invoke facts with invocation/constructor-result suppression. Prior 09ecd94911b830f52fa8807560abcbd79f163d02a2072870c1a59297e9a326e1 -> 3d4e32e7490c830516126e28931827949baa3594cb521f7a3d8dcfed95b6018a; scaling 1.039 < 1.5.", "_rebaselined": "#1976: F33 generic composite literal constructor inference adds generic_type captures in composite_literal patterns; fingerprint drift expected." }, "cobol": { - "fingerprint": "68ee0e95eb9f86f2d92ca35f730f4c2d4d83abc1b5241ae767ff3437780ec8d1", + "fingerprint": "d45bb091b0893d0de4fae2486b31ba21719c9377bf35a0908fd3a36fa1c3bf4e", "scaling_budget": 1.5, + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: COBOL procedure-pointer callable flow facts; multi-topic extraction now consumes each grouped scope/declaration match once instead of requiring a duplicate declaration-only match. Prior 68ee0e95eb9f86f2d92ca35f730f4c2d4d83abc1b5241ae767ff3437780ec8d1 -> d45bb091b0893d0de4fae2486b31ba21719c9377bf35a0908fd3a36fa1c3bf4e; scaling 0.853 < 1.5.", "_note": "Updated for F17-F23 fixes (P2: TIMES guard, ADD GIVING, SQL AS alias). See PR #1959." }, "c": { - "fingerprint": "12a196b2d6249c8d86a931b12ecebc2a0cdf8d6f47683acdd0d8e9d8bc7657f5", + "fingerprint": "3418cded9f7072152f68992f0a426f43ae7d9d553579a47075fc0cab185848a5", "scaling_budget": 1.5, + "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 57fee292147ae6d2db7062da1e07d17122cf355207c8967fa85fd2ec9ca398a4 -> 3418cded9f7072152f68992f0a426f43ae7d9d553579a47075fc0cab185848a5; scaling 1.073 < 1.5.", + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: C function-pointer signatures plus direct-callee argument metadata and invocation-result suppression. Prior 75bcdbbf006bf9bd263c0f5857461b118f39b164e9f821cb0651ad0ec46ef6ae -> 57fee292147ae6d2db7062da1e07d17122cf355207c8967fa85fd2ec9ca398a4; scaling 1.035 < 1.5.", + "_rebaselined_callable_flow": "Callable-value-flow facts for C function pointers, copies, pointer-to-pointer cells, arguments, and indirect invokes. Prior 12a196b2d6249c8d86a931b12ecebc2a0cdf8d6f47683acdd0d8e9d8bc7657f5 -> 75bcdbbf006bf9bd263c0f5857461b118f39b164e9f821cb0651ad0ec46ef6ae; measured scaling ratio 0.980 < 1.5.", "_added": "#1956: c added to the scope-capture bench (was UNBENCHED). C has no inheritance \u2014 flat scale source. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in c/captures.ts (threaded c.node, byte-identical over c-* fixtures); scaling 3.475 -> 0.96.", "_note": "#1983: + c-static-linkage-worker fixture (caller.c/lib.c/lib.h/local.c \u2014 worker-path static-linkage side-channel test). Pure fixture-corpus drift: no c/captures.ts or query change branch-vs-main, existing fixtures' captures byte-identical (c-captures.test.ts 45/45), scaling stays linear (~0.97). The baseline was missed when the fixture landed; regenerated here. fingerprint 0de009b->39f3a83.", "_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression)." }, "cpp": { - "fingerprint": "6ab657c8f9bfe988a3759098c2cffdcc0443def75ff263f1282b82c21d96e931", + "fingerprint": "a70625bb0a9ef74e760d9d79cc5557485d0f0d3fb935e8a22a0c9556c65b5bb1", "scaling_budget": 1.5, + "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature/cv metadata. Prior dde874d2c30bda9f634f9799281a66de800cad9f76cf65e7c31839e2ae9da9ff -> 57860dd2a8d4b06c6d2dd0d854c08b781faee3da8f2b6c42ba0c68a9f70e5ccb; scaling 1.090 < 1.5.", + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: C++ overload-aware function/reference/member-pointer flow facts with invocation/constructor-result suppression. Prior 3a503a1513e7eede3f7a223dcce0896c06d15bdfa920445224c9025848c0d710 -> dde874d2c30bda9f634f9799281a66de800cad9f76cf65e7c31839e2ae9da9ff; scaling 1.034 < 1.5.", + "_rebaselined_callable_flow": "Callable-value-flow facts for C++ function pointers/references, reference aliases, contextual arity, arguments, and member-pointer syntax. Prior 6ab657c8f9bfe988a3759098c2cffdcc0443def75ff263f1282b82c21d96e931 -> 3a503a1513e7eede3f7a223dcce0896c06d15bdfa920445224c9025848c0d710; measured scaling ratio 1.069 < 1.5.", "_note_1909_p3_followup": "#1909 P3 follow-up: cpp-variadic-dependent-resolution covers a comment between a pack-expanded base and `...`; review follow-up restores a separate plain `B...` fixture path too. Capture ranges intentionally drift while scaling remains linear (1.116 < 1.5).", "_note_1899_followup": "#1899 follow-up: braced-init metadata now carries element count, intentionally changing C++ capture output; CI benchmark scaling remains linear (1.129 < 1.5).", "_added": "#1956: cpp added to the scope-capture bench (was UNBENCHED). Heritage-bearing scale source (: public Base, public Mixin) drives emitCppInheritanceCaptures at scale. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in cpp/captures.ts (~12 sites, threaded c.node, byte-identical over 263 cpp-* fixtures); scaling 2.30 -> 1.12.", "_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression). #2094: deleted C++ declarations retain @declaration.is-deleted metadata; deleted operator and pointer-return shapes plus the expanded deleted-overload fixture are included. Intended capture drift; scaling remains linear (1.139 < 1.5).", - "_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift \u2014 no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures \u2014 pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture \u2014 pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae. #2077 review follow-up: cpp-member-lattice adds cross-file, qualified-base, nested-template, inherited-using, this-receiver, and non-virtual-override regressions; fixture_count 274->275. Capture scaling remains linear (1.134 < 1.5). #1899: braced-init call arguments emit a conservative parameter-type capture; fixture_count 277, scaling remains linear (1.141 < 1.5)." + "_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift \u2014 no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures \u2014 pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture \u2014 pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae. #2077 review follow-up: cpp-member-lattice adds cross-file, qualified-base, nested-template, inherited-using, this-receiver, and non-virtual-override regressions; fixture_count 274->275. Capture scaling remains linear (1.134 < 1.5). #1899: braced-init call arguments emit a conservative parameter-type capture; fixture_count 277, scaling remains linear (1.141 < 1.5).", + "_rebaselined_2522_review_fixes": "PR #2522 review fixes: outermost-chain passing modes; ->* ERROR-recovery role order; member-store visibility. Prior 57860dd2a8d4b06c6d2dd0d854c08b781faee3da8f2b6c42ba0c68a9f70e5ccb -> f29bc3f7b1622954d6f6b7647bc9cf6c7a2629ffcc0fe00ac7918e4925876b65; scaling ratio re-verified within budget.", + "_rebaselined_2522_prototype_value_cells": "Plain function/method prototypes no longer index as callable value cells (only pointer/parenthesized variable declarators do) \u2014 removes the spurious indirect-invoke facts that leaked phantom CALLS past two-phase suppression. Prior f29bc3f7b1622954d6f6b7647bc9cf6c7a2629ffcc0fe00ac7918e4925876b65 -> a70625bb0a9ef74e760d9d79cc5557485d0f0d3fb935e8a22a0c9556c65b5bb1; scaling re-verified within budget." }, "csharp": { "_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged. | #1924 F16: record primary-constructor base bindings now exclude constructor arguments; capture fingerprint changes, scaling remains linear. | #2036 review follow-up: csharp-record-base now exercises primary-constructor base dispatch end to end; +2 capture groups, scaling remains linear.", - "fingerprint": "2bb5bc8c19cb8eb08c9590545ad8a1968a7152951f7e12746e2d7901d542fed9", + "fingerprint": "75cf380209fa7d1a8a3ec873be1a9424b4e5173be0b08234c2291e8521a9b3c1", "scaling_budget": 1.5, + "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior f31544530924748f9aa37d11cec570bc10c3ddf9d9b237e6df7a17623fd2bb3a -> 75cf380209fa7d1a8a3ec873be1a9424b4e5173be0b08234c2291e8521a9b3c1; scaling 1.061 < 1.5.", + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: C# method-group/delegate callable flow facts with invocation-result suppression. Prior 2bb5bc8c19cb8eb08c9590545ad8a1968a7152951f7e12746e2d7901d542fed9 -> f31544530924748f9aa37d11cec570bc10c3ddf9d9b237e6df7a17623fd2bb3a; scaling 1.115 < 1.5.", "_note": "#2046: F35 qualified-constructor captures now emit @reference.qualified-name + a simple-name @reference.name on `new Ns.Foo()`/`new A.B.Foo()`; namespace_declaration/file_scoped_namespace_declaration now emit @declaration.namespace name captures (feeding the non-destructive namespacePrefix sidecar for `new B.Foo()` same-tail disambiguation). + csharp-interface-only-base and csharp-namespace-qualified-ctor fixtures. Pure capture-additive + fixture-corpus drift; scaling stays linear (~1.11)." }, "rust": { - "fingerprint": "ac610bbe97666bf285923479dd7b43a2fe4c5354aae8df1bcbafdc04fb220f82", + "fingerprint": "df369c5a5f8de7753fc8bab8b4108ef5081750974ea5085ba9a867675ac9eb29", "scaling_budget": 1.5, + "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 65e5bca66bb1ca117949409e8fb5c80ee69d6f1b5318908eaaecf08da0482e5c -> df369c5a5f8de7753fc8bab8b4108ef5081750974ea5085ba9a867675ac9eb29; scaling 1.065 < 1.5.", + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Rust fn-value callable flow facts with invocation/constructor-result suppression. Prior ac610bbe97666bf285923479dd7b43a2fe4c5354aae8df1bcbafdc04fb220f82 -> 65e5bca66bb1ca117949409e8fb5c80ee69d6f1b5318908eaaecf08da0482e5c; scaling 1.024 < 1.5.", "_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04). #1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls) \u2014 legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target; rust scope-extractor captures byte-identical. | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.", "_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED \u2014 @declaration.macro/@reference.macro + MacroRegistry \u2192 USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126). #1992: + rust-nested-tail-collision-generic and rust-generic-impl-same-method-name (F3) fixtures \u2014 pure fixture-corpus drift, no scope-extractor change; fixture_count 127->129, fingerprint 56ffc1c0->b00aea0f." }, "php": { - "fingerprint": "31c9e3f3cb7094a2bf9021cf9db859036e002f8b44605cd993b470fc600e97cb", + "fingerprint": "4a688fa5a7016546f7f3c6d44de023608ae80c5b0e3670c16f6e61b3632608fd", "scaling_budget": 1.5, + "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior df7b1565f9115d66b1ae32e4a408d651afb2521b14e5ca615f3be426c29af618 -> 4a688fa5a7016546f7f3c6d44de023608ae80c5b0e3670c16f6e61b3632608fd; scaling 1.078 < 1.5.", + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: PHP first-class callable and variable-invocation flow facts with invocation-result suppression. Prior 31c9e3f3cb7094a2bf9021cf9db859036e002f8b44605cd993b470fc600e97cb -> df7b1565f9115d66b1ae32e4a408d651afb2521b14e5ca615f3be426c29af618; scaling 1.074 < 1.5.", "_rebaselined": "#1956: heritage-bearing scale source (class extends Base + use trait); both forms gated at scale; linear (~1.04). | #2481/#2482: PHP imports carry a symbol-kind capture so function/constant imports resolve by declaring file; capture shape changes, scaling remains linear (~1.04).", "_note": "PR #1931: F53 import multi-clause, F54 enum_case, F55 anonymous_class \u2014 fixture count 138\u2192140, fingerprint drift expected." }, "ruby": { - "fingerprint": "b5ea93bb3d0469c3821a8c70f5d5991c6f326e41097c119ad691154301dcc753", + "fingerprint": "070e4e11502442998ddf4048c2981cf1b2b735a87362ff854c5d14d71f98f4e2", "scaling_budget": 1.5, + "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior cff273ae6cb7232c977d9241581834a2a2fa8bcf6369f7bd8f2471cd4419a6ef -> bf50ec6a53c8c91680dc6feac63a8956e78b1059249232dc25a0cfed25f31236; scaling 1.103 < 1.5.", + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Ruby Method/Proc callable flow facts with invocation/constructor-result suppression. Prior b5ea93bb3d0469c3821a8c70f5d5991c6f326e41097c119ad691154301dcc753 -> cff273ae6cb7232c977d9241581834a2a2fa8bcf6369f7bd8f2471cd4419a6ef; scaling 1.086 < 1.5.", "_rebaselined": "#1956 synth-widening: + ruby-qualified-base fixture; synth now reduces a scope_resolution superclass (class C < Mod::Super) to its trailing constant (matching the #1940 legacy leg), at parity. Linear (~1.03). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.", - "_note": "F62: + scope_resolution class/module declaration captures \u2014 fixture count 78\u219281, fingerprint drift expected. #1975: + ruby-tail-collision fixture (Foo::Bar vs Baz::Bar stay distinct nodes) \u2014 pure fixture-corpus drift, scope-extractor captures unchanged; 81\u219282. #1991: + ruby-nested-mixin-tail-collision fixture (85\u219286). Recomputed on the #942 merge (fixture-comment rewording shifts capture byte-positions, capture LOGIC unchanged): bf6b13a -> b5ea93bb." + "_note": "F62: + scope_resolution class/module declaration captures \u2014 fixture count 78\u219281, fingerprint drift expected. #1975: + ruby-tail-collision fixture (Foo::Bar vs Baz::Bar stay distinct nodes) \u2014 pure fixture-corpus drift, scope-extractor captures unchanged; 81\u219282. #1991: + ruby-nested-mixin-tail-collision fixture (85\u219286). Recomputed on the #942 merge (fixture-comment rewording shifts capture byte-positions, capture LOGIC unchanged): bf6b13a -> b5ea93bb.", + "_rebaselined_2522_review_fixes": "PR #2522 review fixes: bare identifiers are calls, not callable references (bareNamesAreCalls). Prior bf50ec6a53c8c91680dc6feac63a8956e78b1059249232dc25a0cfed25f31236 -> 070e4e11502442998ddf4048c2981cf1b2b735a87362ff854c5d14d71f98f4e2; scaling ratio re-verified within budget." }, "swift": { - "fingerprint": "180ac68e780bdf6f9089d53f51cbb9a66aed3e7774631cc3fcbaae5020213998", + "fingerprint": "115c5da807e36bb12fdeba28e44f2b6484ef322ff26c19fa0f191febaf774248", "scaling_budget": 1.5, - "_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression)." + "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 5f923c6604d825d12b249f31c155b0f4d13a8379d532e5dde64a0f9b15cf4725 -> 7687ee2466e16020a12440a03fbda53e63aa05f94b4481f6133c09867a0d560d; scaling 1.042 < 1.5.", + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Swift function-value callable flow facts with invocation-result suppression. Prior 180ac68e780bdf6f9089d53f51cbb9a66aed3e7774631cc3fcbaae5020213998 -> 5f923c6604d825d12b249f31c155b0f4d13a8379d532e5dde64a0f9b15cf4725; scaling 1.043 < 1.5.", + "_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression).", + "_rebaselined_2522_review_fixes": "PR #2522 review fixes: assignment target:/result: fields join the shared fallback. Prior 7687ee2466e16020a12440a03fbda53e63aa05f94b4481f6133c09867a0d560d -> 115c5da807e36bb12fdeba28e44f2b6484ef322ff26c19fa0f191febaf774248; scaling ratio re-verified within budget." }, "dart": { - "fingerprint": "94bf2c26e1ba96f4211634aa572c0a989b503e717e75dfc5df04f66c417de80f", + "fingerprint": "66a46d5ff09f3d11b2771db0f48596fe7057e95c5bc8f56241fdb911137298c3", "scaling_budget": 1.5, + "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 29ce2bfe70b246b1c9d5e99c0ec11e850c22e9672737592207242b7f4cc824b8 -> 66a46d5ff09f3d11b2771db0f48596fe7057e95c5bc8f56241fdb911137298c3; scaling 1.054 < 1.5.", + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Dart function-tearoff and selector callable flow facts, split signature/body lexical ownership, and invocation-result suppression. Prior 94bf2c26e1ba96f4211634aa572c0a989b503e717e75dfc5df04f66c417de80f -> 29ce2bfe70b246b1c9d5e99c0ec11e850c22e9672737592207242b7f4cc824b8; scaling 0.906 < 1.5.", "_added": "#939: dart added to the scope-capture bench with the registry-primary migration. Heritage-bearing scale source (Entity extends Base implements Marker) gates the @reference.inherits synth + the postfix-chain reference walk at scale. emitDartScopeCaptures threads tree-sitter captured nodes (no findNodeAtRange root-walk), so it is linear (~1.0).", "_rebaselined": "#1919 review CF3 fix: extended kotlin-local-property-owner (init/accessor destructuring) + new dart-accessor-owner fixture (getter/setter ownership). Fingerprint-only corpus drift; scaling ~1.0." }, "java": { - "fingerprint": "062d754764aaa8a6772fb90875c710502a63e3e7a300e633942381ed914faada", + "fingerprint": "f3b4f4b6610e07c3ac90deb1c53d3572b6ad55a36e5d7134984876d30031ff67", "scaling_budget": 1.5, + "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata; same-name lexical regions use an O(ancestor-depth) ID-set lookup. Prior d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a -> 004a3592998dca1193bd1429a8284513725de7764f2a3eceedaaa984cfd763b4; scaling 0.992 < 1.5.", + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Java method-reference/SAM callable flow facts with invocation-result suppression. Prior 062d754764aaa8a6772fb90875c710502a63e3e7a300e633942381ed914faada -> d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a; scaling 1.074 < 1.5.", "_rebaselined": "#2357 (supersedes #2353): + java-cast-receiver, java-this-field-chain, java-this-dispatch fixtures (cast-wrapped receivers, this.field chains incl. initializer contexts, bare-this dispatch pinning). Drift is purely fixture-additive: with the three new dirs parked, the fingerprint reproduces the prior baseline byte-identically \u2014 no emit/capture change. #1956 synth-widening: + java-iface-extends fixture; synthesizeJavaInheritanceReferences now ALSO walks interface_declaration extends_interfaces (interface IA extends IB, IC), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.", - "_note": "#1928 / #2045: F35 adds qualified + qualified-generic constructor query captures (`new pkg.Foo()`, `new a.b.Foo()`, `new pkg.Box()`); F38 synthesizes `@reference.call.constructor` on `super(...)`/`this(...)` explicit_constructor_invocation nodes; F41 generic-aware stripQualifier in interpret (type-binding normalization). + java-qualified-constructor and java-explicit-constructor fixtures. Pure capture-additive + fixture-corpus drift; scaling stays linear (~1.06)." + "_note": "#1928 / #2045: F35 adds qualified + qualified-generic constructor query captures (`new pkg.Foo()`, `new a.b.Foo()`, `new pkg.Box()`); F38 synthesizes `@reference.call.constructor` on `super(...)`/`this(...)` explicit_constructor_invocation nodes; F41 generic-aware stripQualifier in interpret (type-binding normalization). + java-qualified-constructor and java-explicit-constructor fixtures. Pure capture-additive + fixture-corpus drift; scaling stays linear (~1.06).", + "_rebaselined_2522_review_fixes": "PR #2522 review fixes: get/test dropped from callableProtocolMethods. Prior 004a3592998dca1193bd1429a8284513725de7764f2a3eceedaaa984cfd763b4 -> f3b4f4b6610e07c3ac90deb1c53d3572b6ad55a36e5d7134984876d30031ff67; scaling ratio re-verified within budget." }, "typescript": { - "fingerprint": "3f44a4a6892698df2d145c8ff2812c3b318807648983c88aca28fbd694f172f9", + "fingerprint": "e05446620c5b80b7aae291cfdf32f693580fada2ae687124769b04a0c03bfe63", "scaling_budget": 1.5, + "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 27f937bfb47d4bded316ea3c785ff659c8cd88a5761d928f113477a08c802c78 -> e05446620c5b80b7aae291cfdf32f693580fada2ae687124769b04a0c03bfe63; scaling 0.983 < 1.5.", + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: lexical callable bindings, direct-callee argument metadata, and invocation-result suppression. Prior db5933cc6760234ed7d495123410feba6de243646d583f20d43032b9459f81fd -> 27f937bfb47d4bded316ea3c785ff659c8cd88a5761d928f113477a08c802c78; scaling 0.975 < 1.5.", + "_rebaselined_callable_flow": "Callable assignment/copy/formal/argument/invoke facts (also consumed by Vue script blocks). Prior 25de86fd3377132c4e35d3d98f4f94a58e0cfeb7c22948a8ea3be4e793be74fd -> db5933cc6760234ed7d495123410feba6de243646d583f20d43032b9459f81fd; measured scaling ratio 0.951 < 1.5.", "_rebaselined": "#1962: F44 (class scope@), F85 (enum member declarations), F87 (optional_parameter type annotations) add new captures \u2014 fingerprint drift expected.", - "_note": "#1968: F44, F85, F87 \u2014 fingerprint drift expected." + "_note": "#1968: F44, F85, F87 \u2014 fingerprint drift expected.", + "_rebaselined_2522": "#2522 intentional @reference.value-ref/property-key capture additions. GitHub Actions run 29553361660 job 87800394279: prior 3f44a4a6892698df2d145c8ff2812c3b318807648983c88aca28fbd694f172f9 -> 25de86fd3377132c4e35d3d98f4f94a58e0cfeb7c22948a8ea3be4e793be74fd; scaling ratio 0.987 < 1.5." }, "javascript": { - "fingerprint": "d72f03c6c502235d2d4b74d66baa5c7d361f040d7a1b72e84acad61210d05ae8", + "fingerprint": "479927409bbdd9852a36172c8260aa56df260e99129a7a9c20a0d1903dd5538b", "scaling_budget": 1.5, + "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior b59fe8135b6a31a12bc3f872b224054b16592588153ae3661d03958d787c76f3 -> 479927409bbdd9852a36172c8260aa56df260e99129a7a9c20a0d1903dd5538b; scaling 1.050 < 1.5.", + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: lexical callable bindings, direct-callee argument metadata, and invocation-result suppression. Prior 917a9cd975ba035bdad71fdb70cd72eeddec58c25797e5a1addfa6172808a55c -> b59fe8135b6a31a12bc3f872b224054b16592588153ae3661d03958d787c76f3; scaling 1.093 < 1.5.", + "_rebaselined_callable_flow": "Callable assignment/copy/formal/argument/invoke facts (also consumed by Vue script blocks). Prior 5567dd47e7ba29821a518c4a9852adc3b774e25ef3e7a6e2b3ecb7b59ddab73c -> 917a9cd975ba035bdad71fdb70cd72eeddec58c25797e5a1addfa6172808a55c; measured scaling ratio 1.126 < 1.5.", "_added": "#1951: bench coverage added (was ungated); scale source heritage-bearing (extends Base); js/kotlin O(n^2) findNodeAtRange-per-match fixed to threaded captured node, now linear.", - "_rebaselined": "#1956 synth-widening: + javascript-qualified-base fixture; synthesizeJsInheritanceReferences now handles a member_expression base (class S extends ns.Base -> Base), matching the #1940 legacy leg + the TS terminalTsTypeNameNode property_identifier case, at parity. Linear (~1.05). | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged." + "_rebaselined": "#1956 synth-widening: + javascript-qualified-base fixture; synthesizeJsInheritanceReferences now handles a member_expression base (class S extends ns.Base -> Base), matching the #1940 legacy leg + the TS terminalTsTypeNameNode property_identifier case, at parity. Linear (~1.05). | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.", + "_rebaselined_2522": "#2522 intentional @reference.value-ref/property-key capture additions. GitHub Actions run 29553361660 job 87800394279: prior d72f03c6c502235d2d4b74d66baa5c7d361f040d7a1b72e84acad61210d05ae8 -> 5567dd47e7ba29821a518c4a9852adc3b774e25ef3e7a6e2b3ecb7b59ddab73c; scaling ratio 1.031 < 1.5." }, "kotlin": { - "fingerprint": "4900431791f2b9280009deb2b82659c26ead8aa6fb8731190a7c505dec5a9041", + "fingerprint": "4b31f46cfb004ba769a96feeb06ae4ef109c77410f54e7aaab4a688df599b112", "scaling_budget": 1.5, + "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior bddba25d5a88152bbbee8d70e82c944b5302accb4b625df782adb1d4f7a7ac12 -> e856951c2a779163d555dadc8e1bf59304a86caed78ac1f450d9caa2b50f63d1; scaling 1.090 < 1.5.", + "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Kotlin callable-reference flow facts with invocation-result suppression. Prior 4900431791f2b9280009deb2b82659c26ead8aa6fb8731190a7c505dec5a9041 -> bddba25d5a88152bbbee8d70e82c944b5302accb4b625df782adb1d4f7a7ac12; scaling 0.880 < 1.5.", "_added": "#1951: bench coverage added (was ungated); scale source heritage-bearing (: Base()); js/kotlin O(n^2) findNodeAtRange-per-match fixed to threaded captured node, now linear.", "_rebaselined": "#1919 review CF3 fix: extended kotlin-local-property-owner (init/accessor destructuring) + new dart-accessor-owner fixture (getter/setter ownership). Fingerprint-only corpus drift; scaling ~1.0.", - "_rebaselined_2271": "PR #2271: re-vendored tree-sitter-kotlin 0.3.8 -> unreleased fwcd main c8ac3d26 for `fun interface` support + new kotlin-fun-interface fixture in the corpus. Drift is both corpus-additive (the fixture) and grammar-driven (the new grammar parses `fun interface` as a class_declaration, not an ERROR node). Baselined to the NEW grammar's fingerprint, so this --check passes only once the regenerated prebuilds land \u2014 until then CI loads the committed 0.3.8 binary and the bench is red, same as the kotlin fun-interface integration tests. scaling ~0.83 (linear)." + "_rebaselined_2271": "PR #2271: re-vendored tree-sitter-kotlin 0.3.8 -> unreleased fwcd main c8ac3d26 for `fun interface` support + new kotlin-fun-interface fixture in the corpus. Drift is both corpus-additive (the fixture) and grammar-driven (the new grammar parses `fun interface` as a class_declaration, not an ERROR node). Baselined to the NEW grammar's fingerprint, so this --check passes only once the regenerated prebuilds land \u2014 until then CI loads the committed 0.3.8 binary and the bench is red, same as the kotlin fun-interface integration tests. scaling ~0.83 (linear).", + "_rebaselined_2522_review_fixes": "PR #2522 review fixes: fieldless assignment nodes decomposed positionally. Prior e856951c2a779163d555dadc8e1bf59304a86caed78ac1f450d9caa2b50f63d1 -> 4b31f46cfb004ba769a96feeb06ae4ef109c77410f54e7aaab4a688df599b112; scaling ratio re-verified within budget." } } diff --git a/gitnexus/src/core/ingestion/cobol/cobol-preprocessor.ts b/gitnexus/src/core/ingestion/cobol/cobol-preprocessor.ts index 4e28b6d2a..68282e97e 100644 --- a/gitnexus/src/core/ingestion/cobol/cobol-preprocessor.ts +++ b/gitnexus/src/core/ingestion/cobol/cobol-preprocessor.ts @@ -607,7 +607,7 @@ function parseDataItemClauses(rest: string): { // USAGE [IS] — including non-standard COMP-6, COMP-X etc. const usageMatch = text.match( - /\bUSAGE\s+(?:IS\s+)?(COMP(?:UTATIONAL)?(?:-[0-9X])?|BINARY|PACKED-DECIMAL|DISPLAY|INDEX|POINTER|NATIONAL)\b/i, + /\bUSAGE\s+(?:IS\s+)?(PROCEDURE-POINTER|FUNCTION-POINTER|COMP(?:UTATIONAL)?(?:-[0-9X])?|BINARY|PACKED-DECIMAL|DISPLAY|INDEX|POINTER|NATIONAL)\b/i, ); if (usageMatch) { result.usage = usageMatch[1].toUpperCase(); diff --git a/gitnexus/src/core/ingestion/emit-references.ts b/gitnexus/src/core/ingestion/emit-references.ts index f03819c02..1cece4f76 100644 --- a/gitnexus/src/core/ingestion/emit-references.ts +++ b/gitnexus/src/core/ingestion/emit-references.ts @@ -289,6 +289,10 @@ function mapKindToType(kind: Reference['kind']): RelationshipType { case 'type-reference': case 'import-use': case 'macro': + // value-ref: function registered as an object-literal property value — + // a reference, not an invocation; CALLS are synthesized separately by + // the property-dispatch pass (#2437). + case 'value-ref': return 'USES'; } } diff --git a/gitnexus/src/core/ingestion/languages/c/captures.ts b/gitnexus/src/core/ingestion/languages/c/captures.ts index 85dfe6118..825301935 100644 --- a/gitnexus/src/core/ingestion/languages/c/captures.ts +++ b/gitnexus/src/core/ingestion/languages/c/captures.ts @@ -11,6 +11,26 @@ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; import { splitCInclude } from './import-decomposer.js'; import { computeCDeclarationArity, computeCCallArity } from './arity-metadata.js'; import { markStaticName } from './static-linkage.js'; +import { + synthesizeCallableFlowCaptures, + type CallableCaptureSignature, +} from '../../utils/callable-flow-captures.js'; + +const C_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set(['function_definition']), + callNodeTypes: new Set(['call_expression']), + parameterListNodeTypes: new Set(['parameter_list', 'argument_list']), + parameterNodeTypes: new Set(['parameter_declaration']), + bindingNodeTypes: new Set(['init_declarator']), + assignmentNodeTypes: new Set(['assignment_expression']), + identifierNodeTypes: new Set(['identifier', 'field_identifier', 'type_identifier']), + callableSignatureDeclarationNodeTypes: new Set(['declaration', 'parameter_declaration']), + emitCanonicalInvokeReference: true, + parameterPassingMode: (parameter: SyntaxNode) => + containsNodeType(parameter, 'pointer_declarator') ? ('pointer' as const) : ('value' as const), + expectedSignature: (container: SyntaxNode, destination: SyntaxNode) => + functionDeclaratorSignature(destination) ?? functionDeclaratorSignature(container), +} as const; export function emitCScopeCaptures( sourceText: string, @@ -146,9 +166,56 @@ export function emitCScopeCaptures( out.push(grouped); } + out.push(...synthesizeCallableFlowCaptures(tree.rootNode, C_CALLABLE_CAPTURE_OPTIONS)); return out; } +function functionDeclaratorSignature(node: SyntaxNode): CallableCaptureSignature | undefined { + const declarator = findDescendantOfType(node, 'function_declarator'); + const parameters = declarator?.childForFieldName('parameters'); + if (parameters === null || parameters === undefined) return undefined; + const parameterNodes = parameters.namedChildren.filter( + (child): child is SyntaxNode => child !== null && child.type === 'parameter_declaration', + ); + // tree-sitter-c materializes `...` as a NAMED `variadic_parameter` node — + // the anonymous-token checks never matched, so variadic signatures were + // emitted with a wrong fixed arity (#2522 review). Keep the token checks + // for grammar variants that expose `...` as an anonymous literal. + const hasEllipsis = parameters.children.some( + (child) => + child.type === 'variadic_parameter' || + child.type === '...' || + (!child.isNamed && child.text === '...'), + ); + const isVoidOnly = + parameterNodes.length === 1 && + parameterNodes[0]!.namedChildCount === 1 && + parameterNodes[0]!.firstNamedChild?.text === 'void'; + if (isVoidOnly) return { parameterCount: 0, parameterTypes: [] }; + const parameterTypes = parameterNodes.map( + (parameter) => parameter.childForFieldName('type')?.text ?? 'unknown', + ); + if (hasEllipsis) parameterTypes.push('...'); + return { + ...(hasEllipsis ? {} : { parameterCount: parameterNodes.length }), + parameterTypes, + }; +} + +function containsNodeType(root: SyntaxNode, type: string): boolean { + return findDescendantOfType(root, type) !== null; +} + +function findDescendantOfType(root: SyntaxNode, type: string): SyntaxNode | null { + const stack: SyntaxNode[] = [root]; + while (stack.length > 0) { + const node = stack.pop()!; + if (node.type === type) return node; + for (const child of node.namedChildren) if (child !== null) stack.push(child); + } + return null; +} + /** * Check if a C function_definition or declaration has `static` storage class. * Walks direct children for a `storage_class_specifier` node with text `static`. diff --git a/gitnexus/src/core/ingestion/languages/c/query.ts b/gitnexus/src/core/ingestion/languages/c/query.ts index 065603b1a..c83ab6a6f 100644 --- a/gitnexus/src/core/ingestion/languages/c/query.ts +++ b/gitnexus/src/core/ingestion/languages/c/query.ts @@ -118,6 +118,23 @@ const C_SCOPE_QUERY = ` declarator: (pointer_declarator declarator: (identifier) @declaration.name)) @declaration.variable +;; Declarations — function-pointer variables (\`void (*fp)(int);\`), with and +;; without initializer. Without these the file-scope pointer has no scope-tree +;; binding, so callable-flow cells written in one function and read in another +;; canonicalize to different keys and never join (#2522 review, H1). +(declaration + declarator: (function_declarator + declarator: (parenthesized_declarator + (pointer_declarator + declarator: (identifier) @declaration.name)))) @declaration.variable + +(declaration + declarator: (init_declarator + declarator: (function_declarator + declarator: (parenthesized_declarator + (pointer_declarator + declarator: (identifier) @declaration.name))))) @declaration.variable + ;; Declarations — macro definitions (preproc_def name: (identifier) @declaration.name) @declaration.macro diff --git a/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts index cb4a6424f..af31ec572 100644 --- a/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts @@ -126,4 +126,10 @@ export const cScopeResolver: ScopeResolver = { const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? ''; return isStaticName(def.filePath, simple); }, + // Precise linkage-only variant used to associate a visible prototype with + // its implementation during callable-value actual-to-formal propagation. + hasFileLocalCallableLinkage: (def: SymbolDefinition) => { + const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? ''; + return isStaticName(def.filePath, simple); + }, }; diff --git a/gitnexus/src/core/ingestion/languages/cobol/captures.ts b/gitnexus/src/core/ingestion/languages/cobol/captures.ts index 3f9c2620f..6eee60bf3 100644 --- a/gitnexus/src/core/ingestion/languages/cobol/captures.ts +++ b/gitnexus/src/core/ingestion/languages/cobol/captures.ts @@ -60,6 +60,23 @@ export function emitCobolScopeCaptures( const extracted = extractCobolSymbolsWithRegex(cleaned, _filePath); const out: CaptureMatch[] = []; + const procedurePointers = new Set( + extracted.dataItems + .filter((item) => /(?:PROCEDURE|FUNCTION)-POINTER/.test(item.usage?.toUpperCase() ?? '')) + .map((item) => item.name.toUpperCase()), + ); + // Fallback for declarations the clause parser missed. Scan CLEANED lines — + // on raw fixed-format sources the sequence number (cols 1-6) satisfied + // `\d+` and the LEVEL NUMBER got captured as the name, so the set never + // contained the real pointer name and the whole feature no-op'd (#2522 + // review, H3). COBOL data names must contain a letter, so `[A-Z]` first + // rejects level numbers. + for (const line of cleaned.split(/\r?\n/)) { + const declaration = line.match( + /^\s*\d{1,2}\s+([A-Z][A-Z0-9-]*).*\b(?:PROCEDURE|FUNCTION)-POINTER\b/i, + ); + if (declaration !== null) procedurePointers.add(declaration[1]!.toUpperCase()); + } // ── 1. PROGRAM-ID → @scope.module ─────────────────────────────────── // The primary program name (first PROGRAM-ID encountered) @@ -278,6 +295,123 @@ export function emitCobolScopeCaptures( if (m !== null) out.push(m); } + // ── 9. Procedure-pointer value flow ──────────────────────────────── + // ISO COBOL exposes callable values through USAGE PROCEDURE-POINTER, + // SET ... TO ENTRY, and dynamic CALL data-items. The regex provider emits + // the same normalized facts as AST-backed providers so shared ingestion + // remains language-agnostic. + for (const program of extracted.programs) { + for (let index = 0; index < (program.procedureUsing?.length ?? 0); index++) { + const parameter = program.procedureUsing![index]!; + const ownerRange = rangeOf(program.startLine, 0, program.endLine, 0); + out.push({ + '@callable-flow.formal': capture('@callable-flow.formal', ownerRange, program.name), + '@callable-flow.owner': capture('@callable-flow.owner', ownerRange, program.name), + '@callable-flow.binding': capture( + '@callable-flow.binding', + rangeOf(program.startLine, 0, program.startLine, parameter.length), + parameter, + ), + '@callable-flow.parameter-index': capture( + '@callable-flow.parameter-index', + ownerRange, + String(index), + ), + '@callable-flow.passing-mode': capture( + '@callable-flow.passing-mode', + ownerRange, + 'reference', + ), + }); + } + } + + for (let index = 0; index < lines.length; index++) { + const line = lines[index]!; + // Comment lines must not seed flows — a commented-out SET produced a + // live seed and a false CALLS edge from dead code (#2522 review, M1). + // Fixed format marks comments with '*'/'/' in indicator column 7; free + // format uses '*>' (also valid inline, so strip the tail). + if (line[6] === '*' || line[6] === '/') continue; + const code = line.split('*>')[0]!; + const lineNumber = index + 1; + const lineRange = rangeOf(lineNumber, 0, lineNumber, endColFrom(line)); + const entry = code.match( + /\bSET\s+([A-Z0-9][A-Z0-9-]*)\s+TO\s+ENTRY\s+(?:"([^"]+)"|'([^']+)')/i, + ); + if (entry !== null) { + const destination = entry[1]!; + const target = entry[2] ?? entry[3]!; + if (procedurePointers.has(destination.toUpperCase())) { + out.push({ + '@callable-flow.seed': capture('@callable-flow.seed', lineRange, line), + '@callable-flow.destination': capture( + '@callable-flow.destination', + lineRange, + destination, + ), + '@callable-flow.target': capture('@callable-flow.target', lineRange, target), + '@callable-flow.target-name': capture('@callable-flow.target-name', lineRange, target), + }); + } + continue; + } + const copy = code.match(/\bSET\s+([A-Z0-9][A-Z0-9-]*)\s+TO\s+([A-Z0-9][A-Z0-9-]*)\b/i); + if ( + copy !== null && + procedurePointers.has(copy[1]!.toUpperCase()) && + procedurePointers.has(copy[2]!.toUpperCase()) + ) { + out.push({ + '@callable-flow.copy': capture('@callable-flow.copy', lineRange, line), + '@callable-flow.destination': capture('@callable-flow.destination', lineRange, copy[1]!), + '@callable-flow.source': capture('@callable-flow.source', lineRange, copy[2]!), + }); + } + } + + for (const call of extracted.calls) { + const line = lines[call.line - 1] ?? ''; + const lineRange = rangeOf(call.line, 0, call.line, endColFrom(line)); + for (let index = 0; index < (call.parameters?.length ?? 0); index++) { + const parameter = call.parameters![index]!; + out.push({ + '@callable-flow.argument': capture('@callable-flow.argument', lineRange, line), + '@callable-flow.source': capture('@callable-flow.source', lineRange, parameter), + '@callable-flow.parameter-index': capture( + '@callable-flow.parameter-index', + lineRange, + String(index), + ), + ...(!call.isQuoted && procedurePointers.has(call.target.toUpperCase()) + ? {} + : { + '@callable-flow.direct-callee-name': capture( + '@callable-flow.direct-callee-name', + lineRange, + call.target, + ), + }), + }); + } + if (!call.isQuoted && procedurePointers.has(call.target.toUpperCase())) { + out.push({ + '@callable-flow.invoke': capture('@callable-flow.invoke', lineRange, line), + '@callable-flow.callee': capture('@callable-flow.callee', lineRange, call.target), + '@callable-flow.invocation-kind': capture( + '@callable-flow.invocation-kind', + lineRange, + 'indirect', + ), + '@callable-flow.arity': capture( + '@callable-flow.arity', + lineRange, + String(call.parameters?.length ?? 0), + ), + }); + } + } + return out; } diff --git a/gitnexus/src/core/ingestion/languages/cobol/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/cobol/scope-resolver.ts index 799cc518e..9e528ce16 100644 --- a/gitnexus/src/core/ingestion/languages/cobol/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/cobol/scope-resolver.ts @@ -58,6 +58,15 @@ const cobolScopeResolver: ScopeResolver = { return 'incompatible'; }, + // PROGRAM-ID declarations bridge to legacy Module graph nodes. COBOL's + // procedure-pointer ENTRY values therefore target Module defs, while every + // AST-backed provider keeps the shared callable-label default. + isCallableValueTarget: (def) => def.type === 'Module', + + // Structural COBOL CALLS/IMPORTS remain owned by the established regex + // processor; this resolver contributes only procedure-pointer CALLS. + scopeResolutionEdgeMode: 'callable-flow-only', + // No inheritance in COBOL — empty MRO map. buildMro: () => new Map(), diff --git a/gitnexus/src/core/ingestion/languages/cpp/captures.ts b/gitnexus/src/core/ingestion/languages/cpp/captures.ts index f55efb48c..069459470 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/captures.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/captures.ts @@ -14,6 +14,7 @@ import { classifyCppParameterType, computeCppDeclarationArity, computeCppCallArity, + normalizeCppParamType, } from './arity-metadata.js'; import { markCppAnonymousNamespaceRange, markFileLocal } from './file-local-linkage.js'; import { markCppDependentBase, markCppDependentPackBase } from './two-phase-lookup.js'; @@ -23,6 +24,111 @@ import { extractCppTemplateConstraints } from './constraint-extractor.js'; import { captureCppMemberLookupFacts } from './member-lookup.js'; import { CPP_BRACED_INIT_TYPE_PREFIX } from './conversion-rank.js'; import { logger } from '../../../logger.js'; +import { + synthesizeCallableFlowCaptures, + type CallableCaptureSignature, +} from '../../utils/callable-flow-captures.js'; + +const CPP_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set(['function_definition', 'lambda_expression']), + callNodeTypes: new Set(['call_expression']), + parameterListNodeTypes: new Set(['parameter_list', 'argument_list']), + parameterNodeTypes: new Set(['parameter_declaration', 'optional_parameter_declaration']), + bindingNodeTypes: new Set(['init_declarator']), + assignmentNodeTypes: new Set(['assignment_expression']), + identifierNodeTypes: new Set([ + 'identifier', + 'field_identifier', + 'type_identifier', + 'namespace_identifier', + ]), + callableSignatureDeclarationNodeTypes: new Set([ + 'declaration', + 'field_declaration', + 'parameter_declaration', + 'optional_parameter_declaration', + ]), + callableReferenceNodeTypes: new Set(['qualified_identifier']), + emitCanonicalInvokeReference: true, + memberPointerOperators: new Set(['.*', '->*']), + memberPointerParts: (node: SyntaxNode) => cppRecoveredMemberPointerParts(node), + parameterPassingMode: (parameter: SyntaxNode) => cppOutermostPassingMode(parameter), + isTrueReferenceBinding: (_container: SyntaxNode, destination: SyntaxNode) => + cppContainsNodeType(destination, 'reference_declarator'), + expectedSignature: (container: SyntaxNode, destination: SyntaxNode) => + cppFunctionDeclaratorSignature(destination) ?? cppFunctionDeclaratorSignature(container), + normalizeQualifiedName: (raw: string) => raw.replaceAll('::', '.'), +} as const; + +/** + * Passing mode from the parameter's OUTERMOST declarator chain only. A deep + * subtree scan inverted copy vs alias: `void reg(void (*cb)(int& out))` has a + * `reference_declarator` inside the nested parameter list, but `cb` itself is + * a by-value pointer copy — classifying it 'reference' made the solver + * back-propagate formal targets into every caller's argument cell (#2522 + * review, M5). The chain walk never descends into nested parameter lists. + */ +function cppOutermostPassingMode(parameter: SyntaxNode): 'reference' | 'pointer' | 'value' { + let sawReference = false; + let sawPointer = false; + let node: SyntaxNode | null = parameter.childForFieldName('declarator') ?? null; + const visited = new Set(); + while (node !== null && !visited.has(node.id)) { + visited.add(node.id); + if (node.type === 'reference_declarator' || node.type === 'abstract_reference_declarator') { + // A reference ANYWHERE on the chain aliases the caller's storage — + // `void (*&cb)(int)` is a reference to pointer, i.e. an alias. + sawReference = true; + } else if (node.type === 'pointer_declarator' || node.type === 'abstract_pointer_declarator') { + sawPointer = true; + } + const next: SyntaxNode | null = node.childForFieldName('declarator'); + if (next !== null) { + node = next; + continue; + } + if (node.type === 'parenthesized_declarator') { + node = + node.namedChildren.find( + (child): child is SyntaxNode => child !== null && child.type.includes('declarator'), + ) ?? null; + continue; + } + break; + } + return sawReference ? 'reference' : sawPointer ? 'pointer' : 'value'; +} + +function cppRecoveredMemberPointerParts( + node: SyntaxNode, +): { receiver: SyntaxNode; member: SyntaxNode; operator: string } | undefined { + if (!node.type.includes('parenthesized')) return undefined; + const recovered = node.namedChildren.find( + (child): child is SyntaxNode => child !== null && child.type === 'ERROR', + ); + if (recovered === undefined) return undefined; + const operator = recovered.children.find((child) => child !== null && child.text === '->*'); + const sibling = node.namedChildren.find( + (child): child is SyntaxNode => child !== null && child.id !== recovered.id, + ); + const errIdentifier = recovered.namedChildren.find( + (child): child is SyntaxNode => child !== null, + ); + if (operator === undefined || sibling === undefined || errIdentifier === undefined) { + return undefined; + } + // tree-sitter's error recovery groups `(obj->*ptr)` two ways depending on + // recovery cost (identifier lengths!): either `[identifier, ERROR "->*m"]` + // or `[ERROR "obj->*", identifier]`. Disambiguate by token order INSIDE the + // ERROR: an identifier BEFORE `->*` is the receiver (the sibling is the + // pointer/member); an identifier AFTER `->*` is the member (the sibling is + // the receiver). Assuming one fixed grouping swapped the roles and silently + // dropped the call site (#2522 review, H2). + if (errIdentifier.startIndex < operator.startIndex) { + return { receiver: errIdentifier, member: sibling, operator: operator.text }; + } + return { receiver: sibling, member: errIdentifier, operator: operator.text }; +} /** * Per-file wall-clock budget for the capture-emit loop (#2432). A worker @@ -519,9 +625,134 @@ export function emitCppScopeCaptures( detectCppDependentBases(tree.rootNode, filePath); captureCppMemberLookupFacts(tree.rootNode, filePath); + out.push(...synthesizeCallableFlowCaptures(tree.rootNode, CPP_CALLABLE_CAPTURE_OPTIONS)); return out; } +function cppFunctionDeclaratorSignature(node: SyntaxNode): CallableCaptureSignature | undefined { + const declarator = cppFindDescendantOfType(node, 'function_declarator'); + const parameters = declarator?.childForFieldName('parameters'); + if (parameters === null || parameters === undefined) { + return recoverCppMemberPointerSignature(node.text); + } + const parameterNodes = parameters.namedChildren.filter( + (child): child is SyntaxNode => child !== null && child.type.includes('parameter_declaration'), + ); + const hasEllipsis = + parameters.children.some( + (child) => child.type === '...' || (!child.isNamed && child.text === '...'), + ) || parameterNodes.some((parameter) => parameter.type === 'variadic_parameter_declaration'); + const isVoidOnly = + parameterNodes.length === 1 && + parameterNodes[0]!.namedChildCount === 1 && + parameterNodes[0]!.firstNamedChild?.text === 'void'; + if (isVoidOnly) { + return { parameterCount: 0, parameterTypes: [], parameterTypeClasses: [] }; + } + const parameterTypes: string[] = []; + const parameterTypeClasses: ParameterTypeClass[] = []; + for (const parameter of parameterNodes) { + if (parameter.type === 'variadic_parameter_declaration') { + parameterTypes.push('...'); + parameterTypeClasses.push({ + base: '...', + cv: 'unknown', + indirection: 'unknown', + pointerDepth: 0, + }); + continue; + } + const rawType = parameter.childForFieldName('type')?.text ?? 'unknown'; + const declaratorText = parameter.childForFieldName('declarator')?.text; + parameterTypes.push(normalizeCppParamType(rawType)); + parameterTypeClasses.push(classifyCppParameterType(rawType, declaratorText, parameter.text)); + } + if (hasEllipsis && !parameterTypes.includes('...')) { + parameterTypes.push('...'); + parameterTypeClasses.push({ + base: '...', + cv: 'unknown', + indirection: 'unknown', + pointerDepth: 0, + }); + } + return { + ...(hasEllipsis ? {} : { parameterCount: parameterNodes.length }), + parameterTypes, + parameterTypeClasses, + isConst: /\)\s*const(?:\s|$)/.test(declarator.text), + }; +} + +/** + * tree-sitter-cpp parses a non-const pointer-to-member variable such as + * `void (Base::*member)()` as nested call expressions (while the cv-qualified + * twin is a declaration). Recover the callable shape from the declarator text + * so `const`/non-`const` overload sets remain distinguishable. + */ +function recoverCppMemberPointerSignature(text: string): CallableCaptureSignature | undefined { + const match = text.match(/\(\s*[^()]*::\s*\*\s*[A-Za-z_]\w*\s*\)\s*\(([^()]*)\)\s*(const\b)?/); + if (match === null) return undefined; + const rawParameters = match[1]!.trim(); + const parameterCount = + rawParameters === '' || rawParameters === 'void' + ? 0 + : splitTopLevelCppParameters(rawParameters).length; + return { parameterCount, isConst: match[2] !== undefined }; +} + +function splitTopLevelCppParameters(text: string): string[] { + const out: string[] = []; + let start = 0; + let angleDepth = 0; + let parenDepth = 0; + let bracketDepth = 0; + for (let index = 0; index < text.length; index++) { + switch (text[index]) { + case '<': + angleDepth++; + break; + case '>': + angleDepth = Math.max(0, angleDepth - 1); + break; + case '(': + parenDepth++; + break; + case ')': + parenDepth = Math.max(0, parenDepth - 1); + break; + case '[': + bracketDepth++; + break; + case ']': + bracketDepth = Math.max(0, bracketDepth - 1); + break; + case ',': + if (angleDepth === 0 && parenDepth === 0 && bracketDepth === 0) { + out.push(text.slice(start, index).trim()); + start = index + 1; + } + break; + } + } + out.push(text.slice(start).trim()); + return out.filter((parameter) => parameter.length > 0); +} + +function cppContainsNodeType(root: SyntaxNode, type: string): boolean { + return cppFindDescendantOfType(root, type) !== null; +} + +function cppFindDescendantOfType(root: SyntaxNode, type: string): SyntaxNode | null { + const stack: SyntaxNode[] = [root]; + while (stack.length > 0) { + const node = stack.pop()!; + if (node.type === type) return node; + for (const child of node.namedChildren) if (child !== null) stack.push(child); + } + return null; +} + function extractCppDeclarationReturnType(fnNode: SyntaxNode): string | undefined { const typeNode = fnNode.childForFieldName('type'); if (typeNode === null) return undefined; diff --git a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts index 5ff5a1f8f..a1b891b32 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts @@ -288,6 +288,20 @@ export const cppScopeResolver: ScopeResolver = { return false; }, + // Keep declaration/definition identity narrower than the global free-call + // visibility gate above: namespace and class ownership do not imply + // internal linkage, while a namespace-scope `static` function does. + hasFileLocalCallableLinkage: (def: SymbolDefinition) => { + // Class members have EXTERNAL linkage even when declared `static` — + // inside a class, `static` means "no instance", not internal linkage. + // The name-keyed set below is populated from every `static` declaration, + // so without this gate a header-declared static member refused its + // cross-file definition join (#2522 review, M2). + if (def.type === 'Method' || def.type === 'Constructor') return false; + const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? ''; + return isFileLocal(def.filePath, simple); + }, + // C++ two-phase template lookup: inside a class template body, // unqualified calls MUST NOT bind to members of a dependent base // class. The standard requires `this->name()` or `Base::name()` diff --git a/gitnexus/src/core/ingestion/languages/csharp/captures.ts b/gitnexus/src/core/ingestion/languages/csharp/captures.ts index 3f28e6be9..fbe338adc 100644 --- a/gitnexus/src/core/ingestion/languages/csharp/captures.ts +++ b/gitnexus/src/core/ingestion/languages/csharp/captures.ts @@ -30,6 +30,7 @@ import { getCsharpParser, getCsharpScopeQuery } from './query.js'; import { recordCacheHit, recordCacheMiss } from './cache-stats.js'; import { getTreeSitterBufferSize } from '../../constants.js'; import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; +import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js'; /** Declaration anchors that carry function-like arity metadata. */ const FUNCTION_DECL_TAGS = [ @@ -48,6 +49,28 @@ const FUNCTION_NODE_TYPES = [ 'local_function_statement', ] as const; +const CSHARP_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set([ + ...FUNCTION_NODE_TYPES, + 'lambda_expression', + 'anonymous_method_expression', + ]), + callNodeTypes: new Set(['invocation_expression']), + parameterListNodeTypes: new Set(['parameter_list', 'argument_list']), + parameterNodeTypes: new Set(['parameter']), + bindingNodeTypes: new Set(['variable_declarator']), + assignmentNodeTypes: new Set(['assignment_expression']), + identifierNodeTypes: new Set(['identifier', 'generic_name']), + callableProtocolMethods: new Set(['Invoke']), + extractAssignment: (node: SyntaxNode) => { + if (node.type !== 'variable_declarator') return undefined; + const destination = node.childForFieldName('name'); + const source = node.lastNamedChild; + if (destination === null || source === null || source.id === destination.id) return undefined; + return { destination, source }; + }, +} as const; + const BUILTIN_TYPE_NAMES = new Set([ 'bool', 'byte', @@ -292,6 +315,7 @@ export function emitCsharpScopeCaptures( out.push(...synthesizeGenericTypeArgumentReferences(tree.rootNode)); out.push(...synthesizeCsharpInheritanceReferences(tree.rootNode)); out.push(...synthesizeCsharpConstructorInitializerReferences(tree.rootNode)); + out.push(...synthesizeCallableFlowCaptures(tree.rootNode, CSHARP_CALLABLE_CAPTURE_OPTIONS)); return out; } diff --git a/gitnexus/src/core/ingestion/languages/dart/captures.ts b/gitnexus/src/core/ingestion/languages/dart/captures.ts index 21d14c7b4..b9733c54f 100644 --- a/gitnexus/src/core/ingestion/languages/dart/captures.ts +++ b/gitnexus/src/core/ingestion/languages/dart/captures.ts @@ -44,6 +44,7 @@ import { getTreeSitterBufferSize } from '../../constants.js'; import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; import { encodeMarker } from '../../utils/heritage-marker.js'; import { DART_BUILT_INS } from './built-ins.js'; +import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js'; const FUNCTION_DECL_TAGS = [ '@declaration.function', @@ -51,6 +52,34 @@ const FUNCTION_DECL_TAGS = [ '@declaration.constructor', ] as const; +const DART_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set(['function_signature', 'function_expression']), + callNodeTypes: new Set(['selector']), + parameterListNodeTypes: new Set(['formal_parameter_list', 'arguments']), + parameterNodeTypes: new Set(['formal_parameter']), + bindingNodeTypes: new Set(['initialized_variable_definition']), + assignmentNodeTypes: new Set(['assignment_expression']), + identifierNodeTypes: new Set(['identifier', 'type_identifier']), + lexicalFunctionOwner: (node: SyntaxNode) => dartLexicalFunctionOwner(node), + isCallNode: (node: SyntaxNode) => node.namedChild(0)?.type === 'argument_part', + extractCallCallee: (node: SyntaxNode) => dartCallableCallee(node) ?? undefined, + callSiteNode: (node: SyntaxNode) => dartCallableCallee(node) ?? undefined, + callableProtocolMethods: new Set(['call']), +} as const; + +function dartLexicalFunctionOwner(input: SyntaxNode): SyntaxNode | undefined { + let node: SyntaxNode | null = input; + while (node !== null) { + if (node.type === 'function_signature' || node.type === 'function_expression') return node; + if (node.type === 'function_body') { + const signature = node.previousNamedSibling; + if (signature?.type === 'function_signature') return signature; + } + node = node.parent; + } + return undefined; +} + export function emitDartScopeCaptures( sourceText: string, _filePath: string, @@ -159,9 +188,20 @@ export function emitDartScopeCaptures( } }); + out.push(...synthesizeCallableFlowCaptures(root, DART_CALLABLE_CAPTURE_OPTIONS)); + return out; } +function dartCallableCallee(selector: SyntaxNode): SyntaxNode | null { + if (selector.namedChild(0)?.type !== 'argument_part') return null; + const previous = selector.previousNamedSibling; + if (previous?.type === 'identifier') return previous; + if (previous?.type !== 'selector') return null; + const inner = previous.namedChild(0); + return inner !== null && ASSIGNABLE_SELECTORS.has(inner.type) ? selectorName(inner) : null; +} + // ─── Function scope synthesis ─────────────────────────────────────────────── /** diff --git a/gitnexus/src/core/ingestion/languages/go/captures.ts b/gitnexus/src/core/ingestion/languages/go/captures.ts index 45b17937f..0179c3d1e 100644 --- a/gitnexus/src/core/ingestion/languages/go/captures.ts +++ b/gitnexus/src/core/ingestion/languages/go/captures.ts @@ -13,6 +13,36 @@ import { synthesizeGoReceiverBinding } from './receiver-binding.js'; import { synthesizeGoTypeBindings, extractSimpleTypeNameText } from './type-binding.js'; import { getTreeSitterBufferSize } from '../../constants.js'; import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; +import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js'; + +const GO_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set(['function_declaration', 'method_declaration', 'func_literal']), + callNodeTypes: new Set(['call_expression']), + parameterListNodeTypes: new Set(['parameter_list', 'argument_list']), + parameterNodeTypes: new Set(['parameter_declaration', 'variadic_parameter_declaration']), + bindingNodeTypes: new Set(['short_var_declaration', 'var_spec']), + assignmentNodeTypes: new Set(['assignment_statement']), + identifierNodeTypes: new Set(['identifier', 'field_identifier', 'package_identifier']), + // Multi-value forms (`a, b := f, g`) pair LHS/RHS expression_list entries + // positionally — the shared field fallback cross-wired first-LHS with + // last-RHS and synthesized a garbage comma-joined qualified name (#2522 + // review). A length mismatch (multi-return call RHS) recognizes the node + // but emits nothing. + extractAssignment: (node: SyntaxNode) => { + if (node.type !== 'short_var_declaration' && node.type !== 'assignment_statement') { + return undefined; + } + const left = node.childForFieldName('left'); + const right = node.childForFieldName('right'); + if (left === null || right === null) return undefined; + if (left.type !== 'expression_list' || right.type !== 'expression_list') return undefined; + const destinations = left.namedChildren.filter((child): child is SyntaxNode => child !== null); + const sources = right.namedChildren.filter((child): child is SyntaxNode => child !== null); + if (destinations.length <= 1 && sources.length <= 1) return undefined; + if (destinations.length !== sources.length) return []; + return destinations.map((destination, index) => ({ destination, source: sources[index]! })); + }, +} as const; export function emitGoScopeCaptures( sourceText: string, @@ -175,6 +205,7 @@ export function emitGoScopeCaptures( } out.push(...synthesizeGoInheritanceReferences(tree.rootNode)); + out.push(...synthesizeCallableFlowCaptures(tree.rootNode, GO_CALLABLE_CAPTURE_OPTIONS)); return out; } diff --git a/gitnexus/src/core/ingestion/languages/java/captures.ts b/gitnexus/src/core/ingestion/languages/java/captures.ts index cd4463459..30b6f90cc 100644 --- a/gitnexus/src/core/ingestion/languages/java/captures.ts +++ b/gitnexus/src/core/ingestion/languages/java/captures.ts @@ -23,6 +23,7 @@ import { getJavaParser, getJavaScopeQuery } from './query.js'; import { recordCacheHit, recordCacheMiss } from './cache-stats.js'; import { getTreeSitterBufferSize } from '../../constants.js'; import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; +import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js'; /** Declaration anchors that carry function-like arity metadata. */ const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.constructor'] as const; @@ -30,6 +31,25 @@ const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.constructor'] a /** tree-sitter-java node types that the method extractor accepts. */ const FUNCTION_NODE_TYPES = ['method_declaration', 'constructor_declaration'] as const; +const JAVA_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set([...FUNCTION_NODE_TYPES, 'lambda_expression']), + callNodeTypes: new Set(['method_invocation']), + parameterListNodeTypes: new Set(['formal_parameters', 'argument_list']), + parameterNodeTypes: new Set(['formal_parameter', 'spread_parameter', 'receiver_parameter']), + bindingNodeTypes: new Set(['variable_declarator']), + assignmentNodeTypes: new Set(['assignment_expression']), + identifierNodeTypes: new Set(['identifier', 'type_identifier']), + callableReferenceNodeTypes: new Set(['method_reference']), + // java.util.function SAM names, MINUS 'get' and 'test': those two collide + // with ubiquitous non-functional APIs (Map/List/Optional/Future.get, + // Predicate-unrelated test methods), emitting a spurious callable-object + // invoke fact for every container access (#2522 review). Supplier.get / + // Predicate.test dispatch is deliberately traded away until the check can + // gate on the receiver's declared type. + callableProtocolMethods: new Set(['run', 'apply', 'accept', 'call']), + normalizeQualifiedName: (raw: string) => raw.replaceAll('::', '.'), +} as const; + /** Suppress read.member emissions when the field_access is already * covered by a method_invocation (object of a call) or an * assignment_expression (write target). */ @@ -220,6 +240,7 @@ export function emitJavaScopeCaptures( ...resolveVarTypeBindings(out), ...synthesizeJavaInheritanceReferences(tree.rootNode), ...synthesizeJavaExplicitConstructorReferences(tree.rootNode), + ...synthesizeCallableFlowCaptures(tree.rootNode, JAVA_CALLABLE_CAPTURE_OPTIONS), ]; } diff --git a/gitnexus/src/core/ingestion/languages/javascript/captures.ts b/gitnexus/src/core/ingestion/languages/javascript/captures.ts index 737d8a83b..b33ad74ac 100644 --- a/gitnexus/src/core/ingestion/languages/javascript/captures.ts +++ b/gitnexus/src/core/ingestion/languages/javascript/captures.ts @@ -41,6 +41,7 @@ import { synthesizeTsReceiverBinding } from '../typescript/receiver-binding.js'; import { isArrayMethodCallbackArrow } from '../typescript/array-callback.js'; import { getTreeSitterBufferSize } from '../../constants.js'; import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; +import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js'; import { deriveDefaultExportHocName, isBlockedDefaultExportHoc, @@ -67,6 +68,21 @@ const CALL_TAGS = [ '@reference.call.constructor', ] as const; +const JS_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set(FUNCTION_NODE_TYPES), + callNodeTypes: new Set(['call_expression']), + parameterListNodeTypes: new Set(['formal_parameters', 'arguments']), + parameterNodeTypes: new Set(['identifier', 'rest_pattern', 'assignment_pattern']), + bindingNodeTypes: new Set(['variable_declarator']), + assignmentNodeTypes: new Set(['assignment_expression', 'augmented_assignment_expression']), + identifierNodeTypes: new Set([ + 'identifier', + 'property_identifier', + 'shorthand_property_identifier_pattern', + 'private_property_identifier', + ]), +} as const; + function pickFirstDefined(grouped: CaptureMatch, tags: readonly string[]): Capture | undefined { for (const tag of tags) { const cap = grouped[tag]; @@ -940,6 +956,7 @@ export function emitJsScopeCaptures( synthesizeForOfMapTupleBindings(tree.rootNode, out); synthesizeInstanceofNarrowings(tree.rootNode, out); synthesizeJsInheritanceReferences(tree.rootNode, out); + out.push(...synthesizeCallableFlowCaptures(tree.rootNode, JS_CALLABLE_CAPTURE_OPTIONS)); return out; } diff --git a/gitnexus/src/core/ingestion/languages/javascript/query.ts b/gitnexus/src/core/ingestion/languages/javascript/query.ts index f7c662b5f..61f691ee4 100644 --- a/gitnexus/src/core/ingestion/languages/javascript/query.ts +++ b/gitnexus/src/core/ingestion/languages/javascript/query.ts @@ -464,6 +464,20 @@ const JAVASCRIPT_SCOPE_QUERY = ` (member_expression object: (_) @reference.receiver property: (property_identifier) @reference.name) @reference.read.member + +;; Value position (#2437): function identifier as object-literal property +;; value ({ emitScopeCaptures: emitHook }) or shorthand ({ emitHook }). +;; Resolution is callable-gated (MethodRegistry) and emits a USES reference; +;; @reference.property-key feeds the property-dispatch pass, which +;; synthesizes CALLS at x.() sites. Two separate patterns (tree-sitter +;; 0.21 alternation hazard); destructuring shorthand is +;; shorthand_property_identifier_pattern and cannot match. +(pair + key: (property_identifier) @reference.property-key + value: (identifier) @reference.name @reference.value-ref) + +(object + (shorthand_property_identifier) @reference.name @reference.property-key @reference.value-ref) `; /** JSX-only suffix — appended when compiling against the JSX grammar for .jsx files. */ diff --git a/gitnexus/src/core/ingestion/languages/kotlin/captures.ts b/gitnexus/src/core/ingestion/languages/kotlin/captures.ts index 16d2a3db8..155658556 100644 --- a/gitnexus/src/core/ingestion/languages/kotlin/captures.ts +++ b/gitnexus/src/core/ingestion/languages/kotlin/captures.ts @@ -14,9 +14,51 @@ import { normalizeKotlinType } from './interpret.js'; import { synthesizeKotlinReceiverBinding } from './receiver-binding.js'; import { getKotlinParser, getKotlinScopeQuery } from './query.js'; import { markCompanionScope } from './companion-scopes.js'; +import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js'; const FUNCTION_DECL_TAGS = ['@declaration.function'] as const; +const KOTLIN_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set(['function_declaration', 'anonymous_function', 'lambda_literal']), + callNodeTypes: new Set(['call_expression']), + parameterListNodeTypes: new Set(['function_value_parameters', 'value_arguments']), + parameterNodeTypes: new Set(['parameter']), + bindingNodeTypes: new Set(['property_declaration']), + assignmentNodeTypes: new Set(['assignment']), + identifierNodeTypes: new Set(['simple_identifier', 'type_identifier']), + callableReferenceNodeTypes: new Set(['callable_reference']), + callableProtocolMethods: new Set(['invoke']), + functionName: (node: SyntaxNode) => + node.namedChildren.find( + (child): child is SyntaxNode => child !== null && child.type === 'simple_identifier', + )?.text, + extractAssignment: (node: SyntaxNode) => { + // tree-sitter-kotlin's `assignment` node is FIELDLESS (positional + // `directly_assignable_expression` then the value), so the shared + // field-based fallback returned nothing and nested reassignments + // (`chosen = ::target` inside a block) never produced flow facts + // (#2522 review, shallow-coverage gap). + if (node.type === 'assignment') { + const named = node.namedChildren.filter((child): child is SyntaxNode => child !== null); + if (named.length < 2) return undefined; + return { destination: named[0]!, source: named[named.length - 1]! }; + } + if (node.type !== 'property_declaration') return undefined; + if (!node.children.some((child) => child.text === '=')) return undefined; + const destination = node.namedChildren.find( + (child): child is SyntaxNode => child !== null && child.type === 'variable_declaration', + ); + const source = [...node.namedChildren] + .reverse() + .find( + (child): child is SyntaxNode => + child !== null && child.id !== destination?.id && child.type !== 'binding_pattern_kind', + ); + return destination === undefined || source === undefined ? undefined : { destination, source }; + }, + normalizeQualifiedName: (raw: string) => raw.replaceAll('::', '.'), +} as const; + export function emitKotlinScopeCaptures( sourceText: string, filePath: string, @@ -222,6 +264,8 @@ export function emitKotlinScopeCaptures( if (extensionFallback !== null) out.push(extensionFallback); } + out.push(...synthesizeCallableFlowCaptures(tree.rootNode, KOTLIN_CALLABLE_CAPTURE_OPTIONS)); + return out; } diff --git a/gitnexus/src/core/ingestion/languages/php/captures.ts b/gitnexus/src/core/ingestion/languages/php/captures.ts index 28481f6db..7e3ce5b15 100644 --- a/gitnexus/src/core/ingestion/languages/php/captures.ts +++ b/gitnexus/src/core/ingestion/languages/php/captures.ts @@ -45,9 +45,41 @@ import { getPhpParser, getPhpScopeQuery } from './query.js'; import { recordCacheHit, recordCacheMiss } from './cache-stats.js'; import { getTreeSitterBufferSize } from '../../constants.js'; import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; +import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js'; type SyntaxNode = ReturnType['parse']>['rootNode']; +const PHP_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set([ + 'function_definition', + 'method_declaration', + 'anonymous_function', + 'arrow_function', + ]), + callNodeTypes: new Set(['function_call_expression']), + parameterListNodeTypes: new Set(['formal_parameters', 'arguments']), + // tree-sitter-php has no 'optional_parameter' node (defaults ride on + // simple_parameter); property promotion is constructor-only and carries no + // callable-flow value (#2522 review). + parameterNodeTypes: new Set(['simple_parameter', 'variadic_parameter']), + bindingNodeTypes: new Set(['assignment_expression']), + assignmentNodeTypes: new Set(['assignment_expression']), + identifierNodeTypes: new Set(['name', 'qualified_name', 'namespace_name']), + functionScopedValueBindings: true, + emitCanonicalInvokeReference: true, + extractCallableReference: (node: SyntaxNode) => { + if (node.type !== 'function_call_expression') return undefined; + const args = node.childForFieldName('arguments'); + const isFirstClass = + args?.namedChildren.some( + (child) => child !== null && child.type === 'variadic_placeholder', + ) === true; + const target = node.childForFieldName('function'); + if (!isFirstClass || target === null || target.type === 'variable_name') return undefined; + return { name: target.text.replace(/^\\+/, ''), anchor: target }; + }, +} as const; + /** Declaration anchors that carry function-like arity metadata. */ const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.function'] as const; @@ -259,6 +291,19 @@ export function emitPhpScopeCaptures( const callTag = ( ['@reference.call.free', '@reference.call.member', '@reference.call.constructor'] as const ).find((t) => grouped[t] !== undefined); + if (callTag !== undefined) { + const possibleFirstClass = nodeIfType(nodeMap[callTag], 'function_call_expression'); + const argumentsNode = possibleFirstClass?.childForFieldName('arguments'); + if ( + argumentsNode?.namedChildren.some( + (child) => child !== null && child.type === 'variadic_placeholder', + ) === true + ) { + // PHP 8.1 `target(...)` creates a Closure; it does not invoke target. + // Callable-flow synthesis below owns this site as a seed. + continue; + } + } if (callTag !== undefined && grouped['@reference.arity'] === undefined) { const callNode = nodeIfType( nodeMap[callTag], @@ -297,6 +342,7 @@ export function emitPhpScopeCaptures( } out.push(...synthesizePhpInheritanceReferences(tree.rootNode)); + out.push(...synthesizeCallableFlowCaptures(tree.rootNode, PHP_CALLABLE_CAPTURE_OPTIONS)); return out; } diff --git a/gitnexus/src/core/ingestion/languages/python/captures.ts b/gitnexus/src/core/ingestion/languages/python/captures.ts index 756066382..6b360a20d 100644 --- a/gitnexus/src/core/ingestion/languages/python/captures.ts +++ b/gitnexus/src/core/ingestion/languages/python/captures.ts @@ -32,6 +32,25 @@ import { recordCacheHit, recordCacheMiss } from './cache-stats.js'; import { getTreeSitterBufferSize } from '../../constants.js'; import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; import { pythonFunctionDefinitionLabel } from './simple-hooks.js'; +import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js'; + +const PYTHON_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set(['function_definition', 'lambda']), + callNodeTypes: new Set(['call']), + parameterListNodeTypes: new Set(['parameters', 'argument_list']), + parameterNodeTypes: new Set([ + 'identifier', + 'default_parameter', + 'typed_parameter', + 'typed_default_parameter', + 'list_splat_pattern', + 'dictionary_splat_pattern', + ]), + bindingNodeTypes: new Set(['assignment', 'named_expression']), + assignmentNodeTypes: new Set(['assignment', 'named_expression']), + identifierNodeTypes: new Set(['identifier']), + functionScopedValueBindings: true, +} as const; export function emitPythonScopeCaptures( sourceText: string, @@ -169,6 +188,7 @@ export function emitPythonScopeCaptures( } out.push(...synthesizePythonInheritanceReferences(tree.rootNode)); + out.push(...synthesizeCallableFlowCaptures(tree.rootNode, PYTHON_CALLABLE_CAPTURE_OPTIONS)); return out; } diff --git a/gitnexus/src/core/ingestion/languages/ruby/captures.ts b/gitnexus/src/core/ingestion/languages/ruby/captures.ts index 2dac9a36a..ea4c5cf6c 100644 --- a/gitnexus/src/core/ingestion/languages/ruby/captures.ts +++ b/gitnexus/src/core/ingestion/languages/ruby/captures.ts @@ -14,6 +14,7 @@ import { getTreeSitterBufferSize } from '../../constants.js'; import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; import { splitQualifiedName } from '../../utils/qualified-name.js'; import { encodeMarker } from '../../utils/heritage-marker.js'; +import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js'; const FUNCTION_NODE_TYPES = ['method', 'singleton_method'] as const; const HERITAGE_CALL_NAMES: ReadonlySet = new Set(['include', 'extend', 'prepend']); @@ -23,6 +24,46 @@ const ATTR_CALL_NAMES: ReadonlySet = new Set([ 'attr_writer', ]); +const RUBY_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set(['method', 'singleton_method', 'lambda']), + callNodeTypes: new Set(['call']), + parameterListNodeTypes: new Set(['method_parameters', 'argument_list']), + parameterNodeTypes: new Set([ + 'identifier', + 'optional_parameter', + 'splat_parameter', + 'hash_splat_parameter', + 'block_parameter', + ]), + bindingNodeTypes: new Set(['assignment']), + assignmentNodeTypes: new Set(['assignment', 'operator_assignment']), + identifierNodeTypes: new Set([ + 'identifier', + 'constant', + 'instance_variable', + 'class_variable', + 'global_variable', + ]), + functionScopedValueBindings: true, + callableProtocolMethods: new Set(['call']), + // A bare receiver-less identifier in value position is a method CALL in + // Ruby (`action = process` stores process's RETURN value) — only explicit + // reference forms (method(:x), &:x, lambda/proc) reference the callable. + bareNamesAreCalls: true, + extractCallableReference: (node: SyntaxNode) => { + if (node.type !== 'call') return undefined; + const method = node.childForFieldName('method'); + if (method?.text !== 'method' && method?.text !== 'public_method') return undefined; + const args = node.childForFieldName('arguments'); + const symbol = args?.namedChildren.find( + (child): child is SyntaxNode => child !== null && child.type === 'simple_symbol', + ); + if (symbol === undefined) return undefined; + const name = symbol.text.replace(/^:/, ''); + return name.length === 0 ? undefined : { name, anchor: symbol }; + }, +} as const; + /** * Build the full `.`-joined qualified owner name for a heritage/attr call by * walking ALL enclosing class/module ancestors (not just the immediate one), @@ -477,6 +518,7 @@ export function emitRubyScopeCaptures( // flow through `emitHeritageEdges` (the `__heritage__:` import path above), // an independent lane that stays intact when the legacy heritage leg is gated off. out.push(...synthesizeRubySuperclassReferences(tree.rootNode)); + out.push(...synthesizeCallableFlowCaptures(tree.rootNode, RUBY_CALLABLE_CAPTURE_OPTIONS)); return out; } diff --git a/gitnexus/src/core/ingestion/languages/rust/captures.ts b/gitnexus/src/core/ingestion/languages/rust/captures.ts index 36a8ddd53..f8f80b9e4 100644 --- a/gitnexus/src/core/ingestion/languages/rust/captures.ts +++ b/gitnexus/src/core/ingestion/languages/rust/captures.ts @@ -12,6 +12,19 @@ import { splitRustUseDeclaration } from './import-decomposer.js'; import { synthesizeRustReceiverBinding } from './receiver-binding.js'; import { getTreeSitterBufferSize } from '../../constants.js'; import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; +import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js'; + +const RUST_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set(['function_item', 'closure_expression']), + callNodeTypes: new Set(['call_expression']), + parameterListNodeTypes: new Set(['parameters', 'arguments']), + parameterNodeTypes: new Set(['parameter', 'self_parameter']), + bindingNodeTypes: new Set(['let_declaration']), + assignmentNodeTypes: new Set(['assignment_expression']), + identifierNodeTypes: new Set(['identifier', 'type_identifier', 'field_identifier']), + callableReferenceNodeTypes: new Set(['scoped_identifier']), + normalizeQualifiedName: (raw: string) => raw.replaceAll('::', '.'), +} as const; export function emitRustScopeCaptures( sourceText: string, @@ -163,6 +176,7 @@ export function emitRustScopeCaptures( } out.push(...synthesizeRustInheritanceReferences(tree.rootNode)); + out.push(...synthesizeCallableFlowCaptures(tree.rootNode, RUST_CALLABLE_CAPTURE_OPTIONS)); return out; } diff --git a/gitnexus/src/core/ingestion/languages/swift/captures.ts b/gitnexus/src/core/ingestion/languages/swift/captures.ts index fa29cdad6..cff4e1366 100644 --- a/gitnexus/src/core/ingestion/languages/swift/captures.ts +++ b/gitnexus/src/core/ingestion/languages/swift/captures.ts @@ -49,10 +49,25 @@ import { getSwiftParser, getSwiftScopeQuery } from './query.js'; import { recordCacheHit, recordCacheMiss } from './cache-stats.js'; import { getTreeSitterBufferSize } from '../../constants.js'; import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; +import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js'; /** Declaration anchors that carry function-like arity metadata. */ const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.constructor'] as const; +const SWIFT_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set(['function_declaration', 'lambda_literal']), + callNodeTypes: new Set(['call_expression']), + parameterListNodeTypes: new Set(['value_arguments']), + parameterNodeTypes: new Set(['parameter']), + bindingNodeTypes: new Set(['property_declaration']), + assignmentNodeTypes: new Set(['assignment']), + identifierNodeTypes: new Set(['simple_identifier', 'type_identifier']), + extractFunctionParameters: (node: SyntaxNode) => + node.namedChildren.filter( + (child): child is SyntaxNode => child !== null && child.type === 'parameter', + ), +} as const; + /** tree-sitter-swift node types that carry arity. */ const FUNCTION_NODE_TYPES = [ 'function_declaration', @@ -292,6 +307,7 @@ export function emitSwiftScopeCaptures( // legacy heritage-capture leg (removed in #942), which the worker pipeline // drops for registry-primary languages (issue #1951). out.push(...synthesizeSwiftInheritanceReferences(tree.rootNode)); + out.push(...synthesizeCallableFlowCaptures(tree.rootNode, SWIFT_CALLABLE_CAPTURE_OPTIONS)); return out; } diff --git a/gitnexus/src/core/ingestion/languages/typescript/captures.ts b/gitnexus/src/core/ingestion/languages/typescript/captures.ts index 9d466c3f9..3df65092c 100644 --- a/gitnexus/src/core/ingestion/languages/typescript/captures.ts +++ b/gitnexus/src/core/ingestion/languages/typescript/captures.ts @@ -40,6 +40,7 @@ import { computeTsArityMetadata } from './arity-metadata.js'; import { isArrayMethodCallbackArrow } from './array-callback.js'; import { getTreeSitterBufferSize } from '../../constants.js'; import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; +import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js'; import { deriveDefaultExportHocName, isBlockedDefaultExportHoc, @@ -70,6 +71,26 @@ const CALL_TAGS = [ '@reference.call.constructor', ] as const; +const TS_CALLABLE_CAPTURE_OPTIONS = { + functionNodeTypes: new Set(FUNCTION_NODE_TYPES), + callNodeTypes: new Set(['call_expression']), + parameterListNodeTypes: new Set(['formal_parameters', 'arguments']), + parameterNodeTypes: new Set([ + 'required_parameter', + 'optional_parameter', + 'rest_pattern', + 'identifier', + ]), + bindingNodeTypes: new Set(['variable_declarator']), + assignmentNodeTypes: new Set(['assignment_expression', 'augmented_assignment_expression']), + identifierNodeTypes: new Set([ + 'identifier', + 'property_identifier', + 'shorthand_property_identifier_pattern', + 'private_property_identifier', + ]), +} as const; + function pickFirstCapture(grouped: CaptureMatch, tags: readonly string[]): Capture | undefined { for (const tag of tags) { const cap = grouped[tag]; @@ -421,6 +442,7 @@ export function emitTsScopeCaptures( synthesizeForOfMapTupleBindings(tree.rootNode, out); synthesizeInstanceofNarrowings(tree.rootNode, out); synthesizeTsInheritanceReferences(tree.rootNode, out); + out.push(...synthesizeCallableFlowCaptures(tree.rootNode, TS_CALLABLE_CAPTURE_OPTIONS)); return out; } diff --git a/gitnexus/src/core/ingestion/languages/typescript/query.ts b/gitnexus/src/core/ingestion/languages/typescript/query.ts index 3c3986143..d048a188b 100644 --- a/gitnexus/src/core/ingestion/languages/typescript/query.ts +++ b/gitnexus/src/core/ingestion/languages/typescript/query.ts @@ -994,6 +994,26 @@ const TYPESCRIPT_SCOPE_QUERY = ` (member_expression object: (_) @reference.receiver property: (property_identifier) @reference.name) @reference.read.member + +;; References — value position (#2437): a function identifier assigned as an +;; object-literal property value (\`{ emitScopeCaptures: emitCppScopeCaptures }\`) +;; or shorthand (\`{ emitHook }\`). Resolution keeps only callable targets +;; (MethodRegistry), so plain-value pairs (\`{ port: DEFAULT_PORT }\`) emit +;; nothing; the emitted edge is a reference-class USES (registration ≠ +;; invocation). \`@reference.property-key\` records the key so the +;; property-dispatch pass can synthesize CALLS at \`x.()\` sites; for +;; shorthand the key IS the name (both tags on one node). Two separate +;; patterns — never combined in \`[...]\` (tree-sitter 0.21 drops alternation +;; siblings around predicates). Destructuring shorthand is a different node +;; (\`shorthand_property_identifier_pattern\`), so \`const { a } = o\` cannot +;; match; string/computed keys carry no \`property_identifier\` and register +;; without a dispatch key. +(pair + key: (property_identifier) @reference.property-key + value: (identifier) @reference.name @reference.value-ref) + +(object + (shorthand_property_identifier) @reference.name @reference.property-key @reference.value-ref) `; /** diff --git a/gitnexus/src/core/ingestion/pipeline-phases/parse-impl.ts b/gitnexus/src/core/ingestion/pipeline-phases/parse-impl.ts index ad72f5506..a9d601f11 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/parse-impl.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/parse-impl.ts @@ -414,7 +414,8 @@ export async function runChunkedParseAndResolve( const skippedByLang = new Map(); for (const f of scannedFiles) { const lang = getLanguageFromFilename(f.path); - if (lang && !isLanguageAvailable(lang)) { + const provider = lang === null ? undefined : getProvider(lang); + if (lang && provider?.parseStrategy !== 'standalone' && !isLanguageAvailable(lang)) { skippedByLang.set(lang, (skippedByLang.get(lang) || 0) + 1); } } diff --git a/gitnexus/src/core/ingestion/resolve-references.ts b/gitnexus/src/core/ingestion/resolve-references.ts index e4413c119..e95659d8e 100644 --- a/gitnexus/src/core/ingestion/resolve-references.ts +++ b/gitnexus/src/core/ingestion/resolve-references.ts @@ -115,6 +115,11 @@ export function resolveReferenceSites(input: ResolveReferencesInput): ResolveRef let unresolved = 0; for (const site of scopes.referenceSites) { + // value-ref sites resolve post-finalize (imports live in finalized + // bindings the registries can't see — same reason free calls need + // `emitFreeCallFallback`). `emitPropertyDispatchCalls` owns their + // resolution and emission entirely (#2437). + if (site.kind === 'value-ref') continue; sitesProcessed++; const resolutions = lookupForSite( @@ -174,6 +179,7 @@ export function resolveReferenceSites(input: ResolveReferencesInput): ResolveRef * | `type-reference` | ClassRegistry | CLASS_KINDS | * | `read`/`write` | FieldRegistry | FIELD_KINDS | * | `import-use` | tiered fallback | METHOD ∪ CLASS ∪ FIELD | + * | `value-ref` | (skipped here) | post-finalize walker in `emitPropertyDispatchCalls` | * | `macro` | MacroRegistry | MACRO_KINDS (`Macro` only) | * * `macro` has its own single-kind registry so a macro invocation @@ -229,6 +235,11 @@ function lookupForSite( if (methodHits.length > 0) return methodHits; return fieldRegistry.lookup(site.name, site.inScope); } + case 'value-ref': { + // Unreachable — filtered before lookup (post-finalize resolution in + // `emitPropertyDispatchCalls`, #2437). Kept for switch exhaustiveness. + return []; + } case 'macro': { // Macro-only namespace: resolves against `Macro`-labeled defs, never // functions. No receiver, no arity — see `MacroRegistry`. diff --git a/gitnexus/src/core/ingestion/scope-extractor.ts b/gitnexus/src/core/ingestion/scope-extractor.ts index 64d4869b5..87439cab3 100644 --- a/gitnexus/src/core/ingestion/scope-extractor.ts +++ b/gitnexus/src/core/ingestion/scope-extractor.ts @@ -61,6 +61,10 @@ import type { BindingRef, + CallableFlowExpectedSignature, + CallableFlowOperand, + CallableFlowPassingMode, + CallableFlowSite, CaptureMatch, ImportEdge, ParameterTypeClass, @@ -187,6 +191,12 @@ export function extract( scopeTree, ); + // ── Pass 6: collect normalized callable-value-flow facts ─────────── + // Kept after (and independent from) Pass 5 so existing reference-site + // extraction remains byte-identical. + const callableFlowSites: CallableFlowSite[] = []; + pass6CollectCallableFlows(partitioned.callableFlow, positionIndex, filePath, callableFlowSites); + // Freeze Scope drafts into final shape and return. const frozenScopes = scopeDrafts.map(draftToScope); return Object.freeze({ @@ -196,6 +206,9 @@ export function extract( parsedImports: Object.freeze(parsedImports.slice()), localDefs: Object.freeze(localDefs.slice()), referenceSites: Object.freeze(referenceSites.slice()), + ...(callableFlowSites.length > 0 + ? { callableFlowSites: Object.freeze(callableFlowSites.slice()) } + : {}), }); } @@ -207,12 +220,13 @@ interface Partitioned { readonly import_: readonly CaptureMatch[]; readonly typeBinding: readonly CaptureMatch[]; readonly reference: readonly CaptureMatch[]; + readonly callableFlow: readonly CaptureMatch[]; } /** - * Bucket each match by the topic of its anchor capture. The anchor is the - * capture whose name is prefixed with the match's topic (`@scope.*`, - * `@declaration.*`, `@import.*`, `@type-binding.*`, `@reference.*`). + * Bucket each match by every topic represented by its anchor captures. An + * emitter may deliberately group a lexical scope and its declaration in one + * match so both passes observe the exact same source range. * * A match may contain additional captures (e.g., `@import.source`, * `@declaration.class.name`) that are used by the provider hooks to @@ -225,49 +239,49 @@ function partitionByTopic(matches: readonly CaptureMatch[]): Partitioned { const import_: CaptureMatch[] = []; const typeBinding: CaptureMatch[] = []; const reference: CaptureMatch[] = []; + const callableFlow: CaptureMatch[] = []; for (const match of matches) { - const topic = topicOf(match); - switch (topic) { - case 'scope': - scope.push(match); - break; - case 'declaration': - declaration.push(match); - break; - case 'import': - import_.push(match); - break; - case 'type-binding': - typeBinding.push(match); - break; - case 'reference': - reference.push(match); - break; - case 'unknown': - // Unrecognized anchor — silently skip. Providers may emit extra - // captures (e.g., `@comment`) that the extractor has no topic for. - break; + for (const topic of topicsOf(match)) { + switch (topic) { + case 'scope': + scope.push(match); + break; + case 'declaration': + declaration.push(match); + break; + case 'import': + import_.push(match); + break; + case 'type-binding': + typeBinding.push(match); + break; + case 'reference': + reference.push(match); + break; + case 'callable-flow': + callableFlow.push(match); + break; + } } } - return { scope, declaration, import_, typeBinding, reference }; + return { scope, declaration, import_, typeBinding, reference, callableFlow }; } -type Topic = 'scope' | 'declaration' | 'import' | 'type-binding' | 'reference' | 'unknown'; +type Topic = 'scope' | 'declaration' | 'import' | 'type-binding' | 'reference' | 'callable-flow'; -function topicOf(match: CaptureMatch): Topic { - // The anchor is the capture whose name uses one of the known topic - // prefixes. For multi-capture matches, ALL captures share the topic; - // we pick the first matching key for efficiency. +function topicsOf(match: CaptureMatch): ReadonlySet { + const topics = new Set(); for (const name of Object.keys(match)) { - if (name.startsWith('@scope.')) return 'scope'; - if (name.startsWith('@declaration.')) return 'declaration'; - if (name.startsWith('@import.')) return 'import'; - if (name.startsWith('@type-binding.')) return 'type-binding'; - if (name.startsWith('@reference.')) return 'reference'; + if (name.startsWith('@scope.')) topics.add('scope'); + else if (name.startsWith('@declaration.')) topics.add('declaration'); + else if (name.startsWith('@import.')) topics.add('import'); + else if (name.startsWith('@type-binding.')) topics.add('type-binding'); + else if (name.startsWith('@reference.')) topics.add('reference'); + else if (name.startsWith('@callable-flow.')) topics.add('callable-flow'); } - return 'unknown'; + return topics; } // ─── Internal: Scope draft model ─────────────────────────────────────────── @@ -754,6 +768,8 @@ function normalizeNodeLabel(kindStr: string): SymbolDefinition['type'] | undefin return 'Annotation'; case 'namespace': return 'Namespace'; + case 'program': + return 'Module'; case 'macro': return 'Macro'; default: @@ -1025,6 +1041,10 @@ function pass5CollectReferences( match['@reference.parameter-type-classes'], ); + // Object-literal key for value-ref sites (`{ key: fn }` / shorthand); + // consumed by the property-dispatch pass (#2437). + const propertyKeyCap = match['@reference.property-key']; + const site: ReferenceSite = { name: nameCap.text, atRange: anchor.range, @@ -1033,6 +1053,9 @@ function pass5CollectReferences( ...(qualifiedCap?.text !== undefined && qualifiedCap.text.length > 0 ? { rawQualifiedName: qualifiedCap.text } : {}), + ...(propertyKeyCap?.text !== undefined && propertyKeyCap.text.length > 0 + ? { propertyKey: propertyKeyCap.text } + : {}), ...(callForm !== undefined ? { callForm } : {}), ...(explicitReceiver !== undefined ? { explicitReceiver } : {}), ...(arity !== undefined ? { arity } : {}), @@ -1065,6 +1088,8 @@ function referenceKindFromAnchor(name: string): ReferenceKind | undefined { return 'import-use'; case 'macro': return 'macro'; + case 'value-ref': + return 'value-ref'; default: return undefined; } @@ -1125,6 +1150,272 @@ function extractArgumentTypes(match: CaptureMatch): readonly string[] | undefine return undefined; } +// ─── Pass 6: collect callable-value-flow facts ───────────────────────────── + +const CALLABLE_FLOW_KINDS = [ + 'seed', + 'copy', + 'alias', + 'address', + 'store', + 'load', + 'formal', + 'argument', + 'invoke', +] as const; + +type CallableFlowKind = (typeof CALLABLE_FLOW_KINDS)[number]; + +function pass6CollectCallableFlows( + matches: readonly CaptureMatch[], + positionIndex: ReturnType, + filePath: string, + out: CallableFlowSite[], +): void { + for (const match of matches) { + const kind = callableFlowKind(match); + if (kind === undefined) continue; + const anchor = match[`@callable-flow.${kind}`]; + if (anchor === undefined) continue; + + switch (kind) { + case 'seed': { + const destination = callableFlowOperand(match, 'destination', positionIndex, filePath); + const target = match['@callable-flow.target']; + const targetName = match['@callable-flow.target-name']?.text ?? target?.text; + if (destination === undefined || target === undefined || !nonEmpty(targetName)) continue; + const expectedSignature = callableFlowExpectedSignature(match); + out.push({ + kind, + destination, + targetName, + targetRange: target.range, + ...(nonEmpty(match['@callable-flow.target-qualified-name']?.text) + ? { targetQualifiedName: match['@callable-flow.target-qualified-name']!.text } + : {}), + ...(expectedSignature !== undefined ? { expectedSignature } : {}), + }); + break; + } + case 'copy': + case 'alias': { + const source = callableFlowOperand(match, 'source', positionIndex, filePath); + const destination = callableFlowOperand(match, 'destination', positionIndex, filePath); + if (source === undefined || destination === undefined) continue; + out.push({ kind, source, destination }); + break; + } + case 'address': { + const source = callableFlowOperand(match, 'source', positionIndex, filePath); + const destination = callableFlowOperand(match, 'destination', positionIndex, filePath); + if (source === undefined || destination === undefined) continue; + out.push({ kind, source, destination }); + break; + } + case 'store': { + const source = callableFlowOperand(match, 'source', positionIndex, filePath); + const pointer = callableFlowOperand(match, 'pointer', positionIndex, filePath); + if (source === undefined || pointer === undefined) continue; + out.push({ kind, source, pointer }); + break; + } + case 'load': { + const pointer = callableFlowOperand(match, 'pointer', positionIndex, filePath); + const destination = callableFlowOperand(match, 'destination', positionIndex, filePath); + if (pointer === undefined || destination === undefined) continue; + out.push({ kind, pointer, destination }); + break; + } + case 'formal': { + const owner = match['@callable-flow.owner']; + const binding = callableFlowOperand(match, 'binding', positionIndex, filePath); + const parameterIndex = parseNonNegativeInt(match['@callable-flow.parameter-index']?.text); + const passingMode = parseCallablePassingMode(match['@callable-flow.passing-mode']?.text); + if ( + owner === undefined || + !nonEmpty(owner.text) || + binding === undefined || + parameterIndex === undefined || + passingMode === undefined + ) { + continue; + } + const expectedSignature = callableFlowExpectedSignature(match); + out.push({ + kind, + ownerName: owner.text, + ownerRange: owner.range, + parameterIndex, + binding, + passingMode, + ...(expectedSignature !== undefined ? { expectedSignature } : {}), + }); + break; + } + case 'argument': { + const source = callableFlowOperand(match, 'source', positionIndex, filePath); + const parameterIndex = parseNonNegativeInt(match['@callable-flow.parameter-index']?.text); + if (source === undefined || parameterIndex === undefined) continue; + out.push({ + kind, + callSite: anchor.range, + parameterIndex, + source, + ...(nonEmpty(match['@callable-flow.direct-callee-name']?.text) + ? { directCalleeName: match['@callable-flow.direct-callee-name']!.text } + : {}), + }); + break; + } + case 'invoke': { + const callee = callableFlowOperand(match, 'callee', positionIndex, filePath); + const inScope = positionIndex.atPosition( + filePath, + anchor.range.startLine, + anchor.range.startCol, + ); + const invocationKind = parseCallableInvocationKind( + match['@callable-flow.invocation-kind']?.text, + ); + if (callee === undefined || inScope === undefined || invocationKind === undefined) continue; + const receiver = callableFlowOperand(match, 'receiver', positionIndex, filePath); + const arity = parseNonNegativeInt(match['@callable-flow.arity']?.text); + out.push({ + kind, + callSite: anchor.range, + inScope, + callee, + invocationKind, + ...(receiver !== undefined ? { receiver } : {}), + ...(arity !== undefined ? { arity } : {}), + }); + break; + } + } + } +} + +function callableFlowKind(match: CaptureMatch): CallableFlowKind | undefined { + return CALLABLE_FLOW_KINDS.find((kind) => match[`@callable-flow.${kind}`] !== undefined); +} + +function callableFlowOperand( + match: CaptureMatch, + role: 'source' | 'destination' | 'pointer' | 'binding' | 'callee' | 'receiver', + positionIndex: ReturnType, + filePath: string, +): CallableFlowOperand | undefined { + const cap = match[`@callable-flow.${role}`]; + if (cap === undefined || !nonEmpty(cap.text)) return undefined; + const inScope = positionIndex.atPosition(filePath, cap.range.startLine, cap.range.startCol); + if (inScope === undefined) return undefined; + const expressionKind = parseCallableOperandKind(match[`@callable-flow.${role}-kind`]?.text); + const indirection = parseNonNegativeInt(match[`@callable-flow.${role}-indirection`]?.text); + if (indirection !== undefined && indirection > 16) return undefined; + return { + name: cap.text, + inScope, + atRange: cap.range, + indirection: indirection ?? 0, + addressOf: match[`@callable-flow.${role}-address`]?.text === 'true', + ...(expressionKind !== undefined ? { expressionKind } : {}), + ...(nonEmpty(match[`@callable-flow.${role}-qualified-name`]?.text) + ? { qualifiedName: match[`@callable-flow.${role}-qualified-name`]!.text } + : {}), + }; +} + +function callableFlowExpectedSignature( + match: CaptureMatch, +): CallableFlowExpectedSignature | undefined { + const parameterCount = parseNonNegativeInt(match['@callable-flow.expected-arity']?.text); + const parameterTypes = parseJsonStringArray(match['@callable-flow.expected-types']?.text); + const parameterTypeClasses = parseJsonParameterTypeClassesCapture( + match['@callable-flow.expected-type-classes'], + ); + const isConst = parseBooleanText(match['@callable-flow.expected-const']?.text); + if ( + parameterCount === undefined && + parameterTypes === undefined && + parameterTypeClasses === undefined && + isConst === undefined + ) { + return undefined; + } + return { + ...(parameterCount !== undefined ? { parameterCount } : {}), + ...(parameterTypes !== undefined ? { parameterTypes } : {}), + ...(parameterTypeClasses !== undefined ? { parameterTypeClasses } : {}), + ...(isConst !== undefined ? { isConst } : {}), + }; +} + +function parseCallableOperandKind( + text: string | undefined, +): 'binding' | 'callable-designator' | 'bound-member' | 'anonymous-callable' | undefined { + switch (text) { + case 'binding': + case 'callable-designator': + case 'bound-member': + case 'anonymous-callable': + return text; + default: + return undefined; + } +} + +function parseBooleanText(text: string | undefined): boolean | undefined { + if (text === 'true') return true; + if (text === 'false') return false; + return undefined; +} + +function parseJsonStringArray(text: string | undefined): readonly string[] | undefined { + if (text === undefined) return undefined; + try { + const parsed: unknown = JSON.parse(text); + return Array.isArray(parsed) && parsed.every((value) => typeof value === 'string') + ? parsed + : undefined; + } catch { + return undefined; + } +} + +function parseNonNegativeInt(text: string | undefined): number | undefined { + if (text === undefined || !/^\d+$/.test(text)) return undefined; + const value = Number.parseInt(text, 10); + return Number.isSafeInteger(value) ? value : undefined; +} + +function parseCallablePassingMode(text: string | undefined): CallableFlowPassingMode | undefined { + switch (text) { + case 'value': + case 'reference': + case 'pointer': + return text; + default: + return undefined; + } +} + +function parseCallableInvocationKind( + text: string | undefined, +): 'indirect' | 'member-pointer' | 'callable-object' | undefined { + switch (text) { + case 'indirect': + case 'member-pointer': + case 'callable-object': + return text; + default: + return undefined; + } +} + +function nonEmpty(value: string | undefined): value is string { + return value !== undefined && value.length > 0; +} + // ─── Internal: range + capture utilities ─────────────────────────────────── function rangesEqual(a: Range, b: Range): boolean { @@ -1155,6 +1446,7 @@ const KNOWN_SUB_TAGS: ReadonlySet = new Set([ '@type-binding.type', '@reference.name', '@reference.qualified-name', + '@reference.property-key', '@reference.receiver', '@reference.operator', '@reference.arity', diff --git a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts index e75b793d7..fd12a222c 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts @@ -413,6 +413,23 @@ export interface ScopeResolver { */ arityCompatibility(callsite: Callsite, def: SymbolDefinition): ArityVerdict; + /** + * Add provider-specific callable value targets beyond the shared + * Function/Method/Constructor set. This is intentionally a predicate hook: + * shared flow analysis never branches on a language name or syntax kind. + */ + readonly isCallableValueTarget?: (def: SymbolDefinition) => boolean; + + /** + * Restrict this provider's scope-resolution graph mutations to callable-value + * CALLS edges. Providers with an existing structural edge pipeline can use + * the shared scope model and callable solver without duplicating their + * established CALLS, IMPORTS, heritage, or property-dispatch edges. + * + * Default: `all`. + */ + readonly scopeResolutionEdgeMode?: 'all' | 'callable-flow-only'; + /** * Per-language constraint compatibility between a callsite and a * candidate `def` that carries `templateConstraints` metadata. @@ -746,6 +763,20 @@ export interface ScopeResolver { */ readonly isFileLocalDef?: (def: SymbolDefinition) => boolean; + /** + * Optional precise linkage predicate used when callable-value flow joins a + * declaration graph node (for example a C/C++ prototype in a caller file) + * to its out-of-file definition. Unlike `isFileLocalDef`, this hook MUST + * answer only language-level internal/file-local linkage. It must not fold + * in broader unqualified-name visibility rules such as namespace or member + * lookup: an explicit declaration already establishes caller visibility. + * + * C and C++ provide this hook for `static` free functions. Languages whose + * declaration/definition identity is already represented by imports or one + * graph node leave it undefined, disabling cross-file prototype joining. + */ + readonly hasFileLocalCallableLinkage?: (def: SymbolDefinition) => boolean; + /** * Optional predicate to identify members for which dispatch through * an instance receiver is **invalid at the language level** — i.e. diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.ts index af878b3b1..1738ed29d 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.ts @@ -21,9 +21,9 @@ * `(atRange.startLine, atRange.startCol)` is byte-equal to U1's `at` — no * normalization needed. * - * Gating (R4): the concrete sink is created in `run.ts` only when - * `input.pdg === true`; otherwise `undefined` is threaded through, so off-mode - * does zero work and emits byte-identical output. + * Gating (R4): the concrete sink is created in `run.ts` when PDG is enabled OR + * always-on callable-flow facts need direct call targets for actual→formal + * propagation. The read-side CFG join remains strictly PDG-gated. * * Multi-target dispatch (R2/KTD8): one site → multiple emit calls → the `Set` * accumulates every resolved target. Capture is per-emit-call, so the @@ -74,15 +74,19 @@ export interface CalleeIdMapView { /** The concrete accumulator: a write sink that also exposes the read view. */ export interface CalleeIdAccumulator extends CalleeIdSink, CalleeIdMapView {} +export type CalleeIdCaptureFilter = (filePath: string, line: number, col: number) => boolean; + /** - * Create the concrete nested-`Map` accumulator. Call ONLY when - * `input.pdg === true` (else thread `undefined` for byte-identity / zero - * overhead — R4). + * Create the concrete nested-`Map` accumulator for PDG callee capture or + * callable-value-flow's direct-target index. */ -export function createCalleeIdAccumulator(): CalleeIdAccumulator { +export function createCalleeIdAccumulator( + shouldCapture?: CalleeIdCaptureFilter, +): CalleeIdAccumulator { const byFile = new Map>>(); return { add(filePath: string, line: number, col: number, calleeId: string): void { + if (shouldCapture?.(filePath, line, col) === false) return; let byPos = byFile.get(filePath); if (byPos === undefined) { byPos = new Map>(); diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/edges.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/edges.ts index 6aa72fc25..f7f246f26 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/edges.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/edges.ts @@ -24,7 +24,9 @@ import type { CalleeIdSink } from './callee-id-sink.js'; /** * Optional resolved-callee-id capture context (#2227 follow-up U2). Threaded - * in only under `--pdg` (else `undefined` → zero overhead, byte-identity R4). + * in under `--pdg` OR when always-on callable-flow facts need direct call + * targets (#2437 — the accumulator then carries a position filter); else + * `undefined` → zero overhead, byte-identity (R4). * `filePath` is NOT on the `site` param, so it rides here alongside the sink. */ export interface CalleeIdCaptureCtx { @@ -50,6 +52,12 @@ export function mapReferenceKindToEdgeType( return 'EXTENDS'; case 'type-reference': return 'USES'; + // A function registered as an object-literal property value emits a + // reference-class USES edge, NOT CALLS — a registration is not an + // invocation (Kythe `ref` vs `ref/call`; Joern `METHOD_REF`). The + // invocation side is synthesized by the property-dispatch pass (#2437). + case 'value-ref': + return 'USES'; // Macro invocations resolve to a `Macro` node (never a function), so // they emit `USES` — kept out of the `CALLS` keyspace which denotes // function/method dispatch (#1934 review). diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts index 7c4b57d35..565bdbb91 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts @@ -48,6 +48,7 @@ function isCallerAnchorLabel(label: NodeLabel): boolean { label === 'Function' || label === 'Method' || label === 'Constructor' || + label === 'Module' || label === 'Class' || label === 'Interface' || label === 'Struct' || diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts index 0d5abe510..b493aa656 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts @@ -178,6 +178,9 @@ export function isLinkableLabel(label: NodeLabel): boolean { label === 'Function' || label === 'Method' || label === 'Constructor' || + // Program-like module declarations are provider-gated callable-value + // targets and need the same def→graph bridge. + label === 'Module' || label === 'Class' || label === 'Interface' || label === 'Struct' || diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/references-to-edges.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/references-to-edges.ts index c3c301d86..e20f65515 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/references-to-edges.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/references-to-edges.ts @@ -88,9 +88,16 @@ export function emitReferencesViaLookup( // Resolved-callee-id capture (#2227 U2/KTD6/R8): record this CALLS site's // resolved target BEFORE the `seen` dedup, keyed on `ref.atRange` // (byte-equal to U1's SiteRecord.at: 1-based line / 0-based col). Only - // CALLS feeds the bridge; ACCESSES/USES/EXTENDS are skipped. `fromFilePath` + // CALLS from real call sites feeds the bridge; ACCESSES/USES/EXTENDS are + // skipped, and so are value-ref CALLS (#2437) — a property-value + // reference is not a call site U1 could have stamped. `fromFilePath` // is the call-site (caller) file — the same file U1 stamps the site on. - if (calleeIdSink !== undefined && edgeType === 'CALLS' && fromFilePath !== undefined) { + if ( + calleeIdSink !== undefined && + edgeType === 'CALLS' && + ref.kind === 'call' && + fromFilePath !== undefined + ) { calleeIdSink.add(fromFilePath, ref.atRange.startLine, ref.atRange.startCol, targetGraphId); } diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/callable-value-flow.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/callable-value-flow.ts new file mode 100644 index 000000000..b39ff8689 --- /dev/null +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/callable-value-flow.ts @@ -0,0 +1,1527 @@ +/** + * Flow-insensitive, inclusion-based resolution for calls through callable + * values. Providers own syntax recognition; this pass consumes only the + * JSON-safe facts carried by ParsedFile. + */ + +import type { + CallableFlowExpectedSignature, + CallableFlowFormalSite, + CallableFlowInvokeSite, + CallableFlowOperand, + CallableFlowSite, + ParsedFile, + ScopeId, + SymbolDefinition, +} from 'gitnexus-shared'; +import type { KnowledgeGraph } from '../../../graph/types.js'; +import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js'; +import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js'; +import type { CalleeIdAccumulator } from '../graph-bridge/callee-id-sink.js'; +import { tryEmitEdgeWithExplicitTargetId } from '../graph-bridge/edges.js'; +import { resolveCallerGraphId, resolveDefGraphId } from '../graph-bridge/ids.js'; +import { resolveInheritanceBaseInScope } from '../scope/walkers.js'; +import { narrowOverloadCandidates } from './overload-narrowing.js'; + +export const MAX_CALLABLE_VALUE_TARGETS = 32; + +interface Target { + readonly id: string; + readonly def: SymbolDefinition; +} + +interface FileFact { + readonly filePath: string; + readonly site: CallableFlowSite; +} + +interface FileInvoke { + readonly filePath: string; + readonly site: CallableFlowInvokeSite; +} + +export interface CallableValueFlowWarning { + readonly language: string; + readonly context: string; + readonly candidateCount: number; + readonly cap: number; +} + +export interface CallableValueFlowResult { + readonly emitted: number; + readonly resolvedInvokes: number; + readonly ambiguousInvokes: number; + readonly unmatchedInvokes: number; + readonly iterations: number; +} + +export interface EmitCallableValueFlowInput { + readonly graph: KnowledgeGraph; + readonly scopes: ScopeResolutionIndexes; + readonly parsedFiles: readonly ParsedFile[]; + readonly nodeLookup: GraphNodeLookup; + readonly calleeIds: CalleeIdAccumulator; + readonly language: string; + readonly collapseByCallerTarget?: boolean; + readonly isCallableValueTarget?: (def: SymbolDefinition) => boolean; + readonly hasFileLocalCallableLinkage?: (def: SymbolDefinition) => boolean; + readonly onWarn?: (warning: CallableValueFlowWarning) => void; +} + +/** Position key shared with the existing free/reference skip-set contract. */ +export function callableFlowSiteKey( + filePath: string, + range: { readonly startLine: number; readonly startCol: number }, +): string { + return `${filePath}:${range.startLine}:${range.startCol}`; +} + +/** + * Return only invoke sites that join to a canonical call ReferenceSite. + * Malformed/stale facts never suppress ordinary resolution. + */ +export function collectDeferredIndirectSites( + parsedFiles: readonly ParsedFile[], + scopes?: ScopeResolutionIndexes, +): ReadonlySet { + const out = new Set(); + const flowCells = new Set(); + if (scopes !== undefined) { + for (const parsed of parsedFiles) { + for (const site of parsed.callableFlowSites ?? []) { + const operand = flowCellOperand(site); + if (operand !== undefined) { + flowCells.add(canonicalBindingKey(parsed.filePath, operand, scopes)); + } + } + } + } + for (const parsed of parsedFiles) { + const canonical = new Set( + parsed.referenceSites + .filter((site) => site.kind === 'call') + .map((site) => callableFlowSiteKey(parsed.filePath, site.atRange)), + ); + for (const site of parsed.callableFlowSites ?? []) { + if (site.kind !== 'invoke') continue; + const key = callableFlowSiteKey(parsed.filePath, site.callSite); + if (!canonical.has(key)) continue; + if ( + scopes === undefined || + site.invocationKind === 'member-pointer' || + site.callee.indirection > 0 || + flowCells.has(canonicalBindingKey(parsed.filePath, site.callee, scopes)) + ) { + out.add(key); + } + } + } + return out; +} + +function flowCellOperand(site: CallableFlowSite): CallableFlowOperand | undefined { + switch (site.kind) { + case 'seed': + case 'copy': + case 'alias': + case 'address': + case 'load': + return site.destination; + case 'store': + return site.pointer; + case 'formal': + return site.binding; + case 'argument': + case 'invoke': + return undefined; + } +} + +export function emitCallableValueFlow(input: EmitCallableValueFlowInput): CallableValueFlowResult { + const facts: FileFact[] = []; + const invokes: FileInvoke[] = []; + const canonicalInvokeKeys = collectDeferredIndirectSites(input.parsedFiles, input.scopes); + let unmatchedInvokes = 0; + for (const parsed of input.parsedFiles) { + for (const site of parsed.callableFlowSites ?? []) { + if (site.kind === 'invoke') { + const key = callableFlowSiteKey(parsed.filePath, site.callSite); + if (canonicalInvokeKeys.has(key)) invokes.push({ filePath: parsed.filePath, site }); + else unmatchedInvokes++; + } else { + facts.push({ filePath: parsed.filePath, site }); + } + } + } + if (facts.length === 0 && invokes.length === 0) { + return { emitted: 0, resolvedInvokes: 0, ambiguousInvokes: 0, unmatchedInvokes, iterations: 0 }; + } + + const targetsByBinding = new Map>(); + const addressesByBinding = new Map>(); + const overflowedTargets = new Set(); + const overflowedAddresses = new Set(); + const overflowWarnings = new Map(); + const rawGraphTargets = buildGraphTargetIndex( + input.scopes, + input.nodeLookup, + input.isCallableValueTarget, + input.graph, + ); + const rawTargetIndexes = buildCallableTargetIndexes(rawGraphTargets.values(), input.scopes); + const { targets: graphTargets, aliasesByTargetId } = canonicalizeCallableDeclarations( + rawGraphTargets, + rawTargetIndexes, + input.scopes, + input.graph, + input.hasFileLocalCallableLinkage, + ); + const uniqueGraphTargets = dedupeTargets([...graphTargets.values()]); + const globalBySimpleName = buildGlobalCallableIndex(uniqueGraphTargets); + const targetIndexes = buildCallableTargetIndexes(uniqueGraphTargets, input.scopes); + + const bindingKey = (filePath: string, operand: CallableFlowOperand): string => + canonicalBindingKey(filePath, operand, input.scopes); + // Lexical binding lookup is suppressed ONLY for cells bound by formal + // facts: a parameter whose grammar emits no declaration binding must not + // adopt a same-named outer function. Value cells (copy/alias/store/load + // destinations) keep their declaration in the inclusion union — reassigning + // a declared function through its own name (`greet = other; greet()`) must + // still count the declaration as a target, or an unresolvable RHS yields + // zero CALLS for a call that resolved pre-flow (#2522 review finding 8). + const formalConstrainedBindings = new Set(); + for (const fact of facts) { + if (fact.site.kind === 'formal') { + formalConstrainedBindings.add(bindingKey(fact.filePath, fact.site.binding)); + } + } + + const warnOverflow = ( + key: string, + count: number, + context: string, + cap = MAX_CALLABLE_VALUE_TARGETS, + ): void => { + if (overflowWarnings.has(key)) return; + overflowWarnings.set(key, { + language: input.language, + context, + candidateCount: count, + cap, + }); + }; + + // Dependency-indexed worklist. Each constraint records the target/address + // cells it reads; only consumers of a changed cell are re-enqueued. This is + // the finite inclusion worklist promised by the plan and avoids rescanning + // every fact for every hop in a reverse-ordered copy chain. + const workItems: Array<() => void> = []; + const watchers = new Map>(); + const queue: number[] = []; + const queued = new Set(); + let activeWorkItem: number | undefined; + const dependencyKey = (kind: 'target' | 'address' | 'call', key: string): string => + `${kind}\0${key}`; + const watch = (kind: 'target' | 'address' | 'call', key: string): void => { + if (activeWorkItem === undefined) return; + const dependency = dependencyKey(kind, key); + let bucket = watchers.get(dependency); + if (bucket === undefined) { + bucket = new Set(); + watchers.set(dependency, bucket); + } + bucket.add(activeWorkItem); + }; + const schedule = (id: number): void => { + if (queued.has(id)) return; + queued.add(id); + queue.push(id); + }; + const notify = (kind: 'target' | 'address' | 'call', key: string): void => { + for (const id of watchers.get(dependencyKey(kind, key)) ?? []) schedule(id); + }; + const addWorkItem = (run: () => void): void => { + const id = workItems.length; + workItems.push(run); + schedule(id); + }; + + const addTarget = (key: string, target: Target, context: string): boolean => { + if (overflowedTargets.has(key)) return false; + let bucket = targetsByBinding.get(key); + if (bucket === undefined) { + bucket = new Map(); + targetsByBinding.set(key, bucket); + } + if (bucket.has(target.id)) return false; + if (bucket.size + 1 > MAX_CALLABLE_VALUE_TARGETS) { + const count = bucket.size + 1; + bucket.clear(); + overflowedTargets.add(key); + warnOverflow(`target:${key}`, count, context); + notify('target', key); + return true; + } + bucket.set(target.id, target); + notify('target', key); + return true; + }; + + const addAddress = (key: string, cell: string, context: string): boolean => { + if (overflowedAddresses.has(key)) return false; + let bucket = addressesByBinding.get(key); + if (bucket === undefined) { + bucket = new Set(); + addressesByBinding.set(key, bucket); + } + if (bucket.has(cell)) return false; + if (bucket.size + 1 > MAX_CALLABLE_VALUE_TARGETS) { + const count = bucket.size + 1; + bucket.clear(); + overflowedAddresses.add(key); + warnOverflow(`address:${key}`, count, context); + notify('address', key); + return true; + } + bucket.add(cell); + notify('address', key); + return true; + }; + + const markTargetOverflow = (key: string, context: string): boolean => { + if (overflowedTargets.has(key)) return false; + targetsByBinding.get(key)?.clear(); + overflowedTargets.add(key); + warnOverflow(`target:${key}`, MAX_CALLABLE_VALUE_TARGETS + 1, context); + notify('target', key); + return true; + }; + + const markAddressOverflow = (key: string, context: string): boolean => { + if (overflowedAddresses.has(key)) return false; + addressesByBinding.get(key)?.clear(); + overflowedAddresses.add(key); + warnOverflow(`address:${key}`, MAX_CALLABLE_VALUE_TARGETS + 1, context); + notify('address', key); + return true; + }; + + const readTargets = ( + key: string, + ): { readonly targets: ReadonlyMap; readonly overflow: boolean } => { + watch('target', key); + return { + targets: targetsByBinding.get(key) ?? new Map(), + overflow: overflowedTargets.has(key), + }; + }; + + const readAddresses = ( + key: string, + ): { readonly cells: ReadonlySet; readonly overflow: boolean } => { + watch('address', key); + return { + cells: addressesByBinding.get(key) ?? new Set(), + overflow: overflowedAddresses.has(key), + }; + }; + + const transferTargets = (source: string, destination: string, context: string): boolean => { + const sourceTargets = readTargets(source); + if (sourceTargets.overflow) return markTargetOverflow(destination, context); + let changed = false; + for (const target of sourceTargets.targets.values()) { + if (addTarget(destination, target, context)) changed = true; + } + return changed; + }; + + const transferAddresses = (source: string, destination: string, context: string): boolean => { + const sourceAddresses = readAddresses(source); + if (sourceAddresses.overflow) return markAddressOverflow(destination, context); + let changed = false; + for (const cell of sourceAddresses.cells) { + if (addAddress(destination, cell, context)) changed = true; + } + return changed; + }; + + const reachedCells = ( + filePath: string, + operand: CallableFlowOperand, + ): { readonly layers: readonly ReadonlySet[]; readonly overflow: boolean } => { + const layers: ReadonlySet[] = [new Set([bindingKey(filePath, operand)])]; + let current = layers[0]!; + for (let depth = 0; depth < operand.indirection; depth++) { + const next = new Set(); + for (const cell of current) { + const addresses = readAddresses(cell); + if (addresses.overflow) return { layers, overflow: true }; + for (const reached of addresses.cells) next.add(reached); + } + layers.push(next); + current = next; + if (current.size === 0) break; + } + return { layers, overflow: false }; + }; + + const operandTargets = ( + filePath: string, + operand: CallableFlowOperand, + ): { readonly targets: Map; readonly overflow: boolean } => { + const reached = reachedCells(filePath, operand); + if (reached.overflow) return { targets: new Map(), overflow: true }; + const out = new Map(); + // A function-pointer binding already denotes its pointee. Explicit `*` + // may therefore terminate at any prefix, while pointer-to-pointer cells + // continue through address edges. This models fp(), (*fp)(), and (**pp) + // without treating the final function-designator dereference as a cell hop. + for (const layer of reached.layers) { + for (const cell of layer) { + const candidates = readTargets(cell); + if (candidates.overflow) return { targets: new Map(), overflow: true }; + for (const target of candidates.targets.values()) { + out.set(target.id, target); + if (out.size > MAX_CALLABLE_VALUE_TARGETS) { + return { targets: new Map(), overflow: true }; + } + } + } + } + return { targets: out, overflow: false }; + }; + + // Explicit seeds use lexical/qualified registries and contextual signature + // narrowing. No arbitrary first-overload choice is permitted. + for (const fact of facts) { + if (fact.site.kind !== 'seed') continue; + const destination = bindingKey(fact.filePath, fact.site.destination); + const candidates = resolveSeedCandidates( + fact.filePath, + fact.site.destination.inScope, + fact.site.targetName, + fact.site.targetQualifiedName, + fact.site.expectedSignature, + input.scopes, + graphTargets, + globalBySimpleName, + targetIndexes, + input.graph, + ); + for (const target of candidates) addTarget(destination, target, `binding:${destination}`); + } + + // Direct callable references used as argument/copy sources acquire lexical + // targets. A nearest non-callable binding is a hard shadowing boundary. + for (const fact of facts) { + const source = sourceOperand(fact.site); + if (source === undefined) continue; + const key = bindingKey(fact.filePath, source); + for (const target of resolveOperandCandidates( + fact.filePath, + source, + undefined, + input.scopes, + graphTargets, + globalBySimpleName, + targetIndexes, + input.graph, + !formalConstrainedBindings.has(key), + )) { + addTarget(key, target, `binding:${key}`); + } + } + + // Explicitly dereferenced named functions (`(*target)()`) have no source + // fact of their own; seed their canonical binding from lexical lookup. + for (const invoke of invokes) { + const key = bindingKey(invoke.filePath, invoke.site.callee); + for (const target of resolveOperandCandidates( + invoke.filePath, + invoke.site.callee, + undefined, + input.scopes, + graphTargets, + globalBySimpleName, + targetIndexes, + input.graph, + !formalConstrainedBindings.has(key), + )) { + addTarget(key, target, `binding:${key}`); + } + } + + // Address-of constraints are static and seed the abstract-cell graph. + for (const fact of facts) { + if (fact.site.kind !== 'address') continue; + addAddress( + bindingKey(fact.filePath, fact.site.destination), + bindingKey(fact.filePath, fact.site.source), + `address:${fact.filePath}:${fact.site.source.name}`, + ); + } + + const formalsByGraphId = indexFormalsByGraphId( + input.parsedFiles, + input.scopes, + input.nodeLookup, + targetIndexes, + aliasesByTargetId, + ); + const callSignaturesBySite = indexCallSignatures(input.parsedFiles); + const dynamicCallees = new Map>(); + const dynamicOverflow = new Set(); + const dynamicTargetHistory = new Map>(); + + for (const fact of facts) { + const site = fact.site; + const context = `${fact.site.kind}:${fact.filePath}`; + switch (site.kind) { + case 'copy': + case 'alias': { + addWorkItem(() => { + const source = bindingKey(fact.filePath, site.source); + const destination = bindingKey(fact.filePath, site.destination); + transferTargets(source, destination, context); + transferAddresses(source, destination, context); + if (site.kind === 'alias') { + transferTargets(destination, source, context); + transferAddresses(destination, source, context); + } + }); + break; + } + case 'load': { + addWorkItem(() => { + const destination = bindingKey(fact.filePath, site.destination); + const reached = reachedCells(fact.filePath, site.pointer); + if (reached.overflow) { + markTargetOverflow(destination, context); + markAddressOverflow(destination, context); + return; + } + for (const cell of reached.layers.at(-1) ?? []) { + transferTargets(cell, destination, context); + transferAddresses(cell, destination, context); + } + }); + break; + } + case 'store': { + addWorkItem(() => { + const sourceTargets = operandTargets(fact.filePath, site.source); + const reached = reachedCells(fact.filePath, site.pointer); + if (reached.overflow) return; + for (const cell of reached.layers.at(-1) ?? []) { + if (sourceTargets.overflow) { + markTargetOverflow(cell, context); + continue; + } + for (const target of sourceTargets.targets.values()) addTarget(cell, target, context); + } + }); + break; + } + case 'seed': + case 'address': + case 'formal': + case 'argument': + case 'invoke': + break; + } + } + + for (const invoke of invokes) { + addWorkItem(() => { + const callKey = callableFlowSiteKey(invoke.filePath, invoke.site.callSite); + const targets = operandTargets(invoke.filePath, invoke.site.callee); + if (targets.overflow || targets.targets.size > MAX_CALLABLE_VALUE_TARGETS) { + if (!dynamicOverflow.has(callKey)) { + dynamicOverflow.add(callKey); + dynamicCallees.delete(callKey); + warnOverflow( + `invoke:${callKey}`, + Math.max(targets.targets.size, MAX_CALLABLE_VALUE_TARGETS + 1), + `site:${callKey}`, + ); + notify('call', callKey); + } + return; + } + const expanded = expandMemberTargets( + invoke, + targets.targets, + input.scopes, + graphTargets, + targetIndexes, + input.graph, + ); + const history = dynamicTargetHistory.get(callKey) ?? new Set(); + for (const id of expanded.keys()) history.add(id); + dynamicTargetHistory.set(callKey, history); + const previous = dynamicCallees.get(callKey); + if (!sameTargetSet(previous, expanded)) { + dynamicCallees.set(callKey, expanded); + notify('call', callKey); + } + }); + } + + for (const fact of facts) { + if (fact.site.kind !== 'argument') continue; + const site = fact.site; + addWorkItem(() => { + const callKey = callableFlowSiteKey(fact.filePath, site.callSite); + const callSignature = callSignaturesBySite.get(callKey); + const indexedFormals = (targetId: string): readonly IndexedFormal[] => + narrowIndexedFormals( + formalsByGraphId.get(targetId)?.get(site.parameterIndex) ?? [], + callSignature, + ); + watch('call', callKey); + const targetIds = new Set(); + for (const id of input.calleeIds.get(fact.filePath)?.get(posKey(site.callSite)) ?? []) { + targetIds.add(id); + } + for (const id of dynamicCallees.get(callKey)?.keys() ?? []) targetIds.add(id); + let hasIndexedFormal = [...targetIds].some((id) => indexedFormals(id).length > 0); + if (!hasIndexedFormal && site.directCalleeName !== undefined) { + for (const target of resolveSeedCandidates( + fact.filePath, + site.source.inScope, + site.directCalleeName, + undefined, + callSignature, + input.scopes, + graphTargets, + globalBySimpleName, + targetIndexes, + input.graph, + )) { + targetIds.add(target.id); + } + hasIndexedFormal = [...targetIds].some((id) => indexedFormals(id).length > 0); + } + const history = dynamicTargetHistory.get(callKey); + const callOverflow = + dynamicOverflow.has(callKey) || targetIds.size > MAX_CALLABLE_VALUE_TARGETS; + if (callOverflow) { + for (const targetId of history ?? targetIds) { + for (const formal of indexedFormals(targetId)) { + const formalKey = bindingKey(formal.filePath, formal.site.binding); + markTargetOverflow(formalKey, `actual-formal-overflow:${callKey}`); + markAddressOverflow(formalKey, `actual-formal-overflow:${callKey}`); + } + } + return; + } + if (targetIds.size === 0 || !hasIndexedFormal) return; + + const sourceKey = bindingKey(fact.filePath, site.source); + const sourceTargets = operandTargets(fact.filePath, site.source); + for (const targetId of targetIds) { + for (const formal of indexedFormals(targetId)) { + const formalKey = bindingKey(formal.filePath, formal.site.binding); + const context = `actual-formal:${callKey}:${site.parameterIndex}`; + const contextualTargets = new Map(sourceTargets.targets); + for (const target of resolveOperandCandidates( + fact.filePath, + site.source, + formal.site.expectedSignature, + input.scopes, + graphTargets, + globalBySimpleName, + targetIndexes, + input.graph, + !formalConstrainedBindings.has(sourceKey), + )) { + contextualTargets.set(target.id, target); + } + const narrowed = narrowTargetMap( + contextualTargets, + formal.site.expectedSignature, + input.graph, + ); + if (sourceTargets.overflow || narrowed.size > MAX_CALLABLE_VALUE_TARGETS) { + markTargetOverflow(formalKey, context); + } else { + for (const target of narrowed.values()) addTarget(formalKey, target, context); + } + transferAddresses(sourceKey, formalKey, context); + if (site.source.addressOf) addAddress(formalKey, sourceKey, context); + if (formal.site.passingMode === 'reference') { + transferTargets(formalKey, sourceKey, context); + transferAddresses(formalKey, sourceKey, context); + } + } + } + }); + } + + let iterations = 0; + const workBudget = Math.max(2_048, workItems.length * 128); + let queueHead = 0; + while (queueHead < queue.length && iterations < workBudget) { + const id = queue[queueHead++]!; + queued.delete(id); + activeWorkItem = id; + workItems[id]!(); + activeWorkItem = undefined; + iterations++; + } + const workBudgetExceeded = queueHead < queue.length; + if (workBudgetExceeded) { + // `invokes` are exactly the deferred sites the ordinary passes already + // skipped (`collectDeferredIndirectSites` feeds both) — every one of them + // ends this run with zero CALLS, so the count goes in the warning context + // rather than being lost (#2522 review finding: budget-bailout honesty). + warnOverflow( + 'work-budget', + iterations + queue.length - queueHead, + `analysis-work-budget:${invokes.length}-deferred-sites-unresolved`, + workBudget, + ); + } + + for (const warning of overflowWarnings.values()) input.onWarn?.(warning); + + // No partial graph output when a hostile/corrupt fact graph exhausts the + // bounded work budget. The caller receives a warning; NOTE this is not + // free for callers — the deferred invoke sites were already excluded from + // free-call fallback and reference emission, so they get no edges at all + // this run. Non-deferred sites keep their ordinary resolution. + if (workBudgetExceeded) { + return { + emitted: 0, + resolvedInvokes: 0, + ambiguousInvokes: invokes.length, + unmatchedInvokes, + iterations, + }; + } + + let emitted = 0; + let resolvedInvokes = 0; + let ambiguousInvokes = 0; + const seen = new Set(); + for (const invoke of invokes) { + const key = callableFlowSiteKey(invoke.filePath, invoke.site.callSite); + if (dynamicOverflow.has(key)) { + ambiguousInvokes++; + continue; + } + const targets = dynamicCallees.get(key); + if (targets === undefined || targets.size === 0) continue; + resolvedInvokes++; + const confidence = targets.size === 1 ? 0.8 : 0.7; + for (const target of targets.values()) { + if ( + tryEmitEdgeWithExplicitTargetId( + input.graph, + input.scopes, + input.nodeLookup, + { inScope: invoke.site.inScope, atRange: invoke.site.callSite, kind: 'call' }, + target.id, + 'callable-value-flow', + seen, + confidence, + input.collapseByCallerTarget === true, + { sink: input.calleeIds, filePath: invoke.filePath }, + ) + ) { + emitted++; + } + } + } + + return { emitted, resolvedInvokes, ambiguousInvokes, unmatchedInvokes, iterations }; +} + +function buildGraphTargetIndex( + scopes: ScopeResolutionIndexes, + nodeLookup: GraphNodeLookup, + providerTarget: ((def: SymbolDefinition) => boolean) | undefined, + graph: KnowledgeGraph, +): ReadonlyMap { + const out = new Map(); + const byAnchor = buildGraphCallableAnchorIndex(graph); + for (const def of scopes.defs.byId.values()) { + if (!isCallable(def) && providerTarget?.(def) !== true) continue; + const anchorKey = definitionAnchorKey(def); + const anchored = anchorKey === undefined ? undefined : byAnchor.get(anchorKey); + const id = + anchored?.length === 1 ? anchored[0] : resolveDefGraphId(def.filePath, def, nodeLookup); + // Overloads can intentionally share one graph node ID. Index by the + // definition identity so contextual signature narrowing still sees the + // complete overload set before a selected target collapses to graph ID. + if (id !== undefined) out.set(def.nodeId, { id, def }); + } + return out; +} + +interface CanonicalCallableTargets { + /** Definition identity remains the key; declaration keys may point at the definition target. */ + readonly targets: ReadonlyMap; + /** Prototype/declaration graph ids grouped by their canonical definition graph id. */ + readonly aliasesByTargetId: ReadonlyMap; +} + +/** + * Associate declaration-only callable nodes with their unique out-of-file + * definition. Function scopes are the body/definition signal; prototypes do + * not create one. The optional provider predicate is deliberately linkage- + * only, keeping language rules out of this shared pass and preventing `static` + * symbols from leaking across translation units. + */ +function canonicalizeCallableDeclarations( + rawTargets: ReadonlyMap, + rawIndexes: CallableTargetIndexes, + scopes: ScopeResolutionIndexes, + graph: KnowledgeGraph, + hasFileLocalCallableLinkage: ((def: SymbolDefinition) => boolean) | undefined, +): CanonicalCallableTargets { + if (hasFileLocalCallableLinkage === undefined) { + return { targets: rawTargets, aliasesByTargetId: new Map() }; + } + + const definitionAnchors = new Set(); + for (const scope of scopes.scopeTree.byId.values()) { + if (scope.kind !== 'Function') continue; + definitionAnchors.add(`${scope.filePath}\0${scope.range.startLine}\0${scope.range.startCol}`); + } + const isDefinition = (target: Target): boolean => { + const position = definitionPosition(target.def); + return position !== undefined && definitionAnchors.has(position); + }; + + const targets = new Map(rawTargets); + for (const [defId, declaration] of rawTargets) { + if (isDefinition(declaration)) continue; + const qualifiedName = effectiveQualifiedName(declaration.def, scopes); + if (qualifiedName === undefined) continue; + const candidates = dedupeTargets( + (rawIndexes.byQualifiedName.get(qualifiedName) ?? []).filter((candidate) => { + if (!isDefinition(candidate)) return false; + if (candidate.def.filePath === declaration.def.filePath) return true; + if ( + hasFileLocalCallableLinkage(declaration.def) || + hasFileLocalCallableLinkage(candidate.def) + ) { + return false; + } + return declarationSignatureCompatible(declaration, candidate, graph); + }), + ); + const byGraphId = new Map(candidates.map((candidate) => [candidate.id, candidate])); + if (byGraphId.size === 1) { + targets.set(defId, byGraphId.values().next().value as Target); + } + } + + const aliases = new Map>(); + for (const [defId, raw] of rawTargets) { + const canonical = targets.get(defId); + if (canonical === undefined || canonical.id === raw.id) continue; + let bucket = aliases.get(canonical.id); + if (bucket === undefined) { + bucket = new Set(); + aliases.set(canonical.id, bucket); + } + bucket.add(raw.id); + } + return { + targets, + aliasesByTargetId: new Map( + [...aliases].map(([targetId, graphIds]) => [targetId, [...graphIds]]), + ), + }; +} + +function definitionPosition(def: SymbolDefinition): string | undefined { + const match = def.nodeId.match(/#(\d+):(\d+):/); + return match === null ? undefined : `${def.filePath}\0${match[1]}\0${match[2]}`; +} + +function declarationSignatureCompatible( + declaration: Target, + definition: Target, + graph: KnowledgeGraph, +): boolean { + const left = declaration.def; + const right = definition.def; + if ( + left.parameterCount !== undefined && + right.parameterCount !== undefined && + left.parameterCount !== right.parameterCount + ) { + return false; + } + if ( + left.parameterTypes !== undefined && + !sameOptionalArray(left.parameterTypes, right.parameterTypes) + ) { + return false; + } + if ( + left.parameterTypeClasses !== undefined && + !sameOptionalArray(left.parameterTypeClasses, right.parameterTypeClasses) + ) { + return false; + } + const declarationConst = graph.getNode(declaration.id)?.properties.isConst; + const definitionConst = graph.getNode(definition.id)?.properties.isConst; + return typeof declarationConst !== 'boolean' || declarationConst === definitionConst; +} + +function buildGraphCallableAnchorIndex( + graph: KnowledgeGraph, +): ReadonlyMap { + const out = new Map(); + for (const node of graph.iterNodes()) { + if (node.label !== 'Function' && node.label !== 'Method' && node.label !== 'Constructor') { + continue; + } + const filePath = node.properties.filePath; + const name = node.properties.name; + const zeroBasedLine = node.properties.startLine; + if ( + typeof filePath !== 'string' || + typeof name !== 'string' || + typeof zeroBasedLine !== 'number' + ) { + continue; + } + const key = `${filePath}\0${node.label}\0${zeroBasedLine + 1}\0${name}`; + const bucket = out.get(key); + if (bucket === undefined) out.set(key, [node.id]); + else bucket.push(node.id); + } + return out; +} + +function definitionAnchorKey(def: SymbolDefinition): string | undefined { + const line = def.nodeId.match(/#(\d+):(\d+):/)?.[1]; + const name = simpleName(def.qualifiedName); + if (line === undefined || name === undefined) return undefined; + return `${def.filePath}\0${def.type}\0${line}\0${name}`; +} + +function buildGlobalCallableIndex( + targets: Iterable, +): ReadonlyMap { + const out = new Map(); + for (const target of targets) { + const name = simpleName(target.def.qualifiedName); + if (name === undefined) continue; + const bucket = out.get(name); + if (bucket === undefined) out.set(name, [target]); + else bucket.push(target); + } + return out; +} + +interface CallableTargetIndexes { + readonly byQualifiedName: ReadonlyMap; + readonly byOwnerAndName: ReadonlyMap; + readonly byFileAndName: ReadonlyMap; +} + +function buildCallableTargetIndexes( + targets: Iterable, + scopes: ScopeResolutionIndexes, +): CallableTargetIndexes { + const byQualifiedName = new Map(); + const byOwnerAndName = new Map(); + const byFileAndName = new Map(); + const add = (index: Map, key: string | undefined, target: Target): void => { + if (key === undefined) return; + const bucket = index.get(key); + if (bucket === undefined) index.set(key, [target]); + else bucket.push(target); + }; + for (const target of targets) { + const name = simpleName(target.def.qualifiedName); + add(byQualifiedName, effectiveQualifiedName(target.def, scopes), target); + add( + byOwnerAndName, + target.def.ownerId === undefined || name === undefined + ? undefined + : `${target.def.ownerId}\0${name}`, + target, + ); + add(byFileAndName, name === undefined ? undefined : `${target.def.filePath}\0${name}`, target); + } + return { byQualifiedName, byOwnerAndName, byFileAndName }; +} + +function resolveSeedCandidates( + filePath: string, + inScope: ScopeId, + targetName: string, + targetQualifiedName: string | undefined, + expected: + | { + readonly parameterCount?: number; + readonly parameterTypes?: readonly string[]; + readonly parameterTypeClasses?: readonly import('gitnexus-shared').ParameterTypeClass[]; + readonly isConst?: boolean; + } + | undefined, + scopes: ScopeResolutionIndexes, + graphTargets: ReadonlyMap, + globalBySimpleName: ReadonlyMap, + targetIndexes: CallableTargetIndexes, + graph: KnowledgeGraph, +): readonly Target[] { + let candidates: Target[] = []; + let lexicalShadow = false; + if (targetQualifiedName !== undefined) { + for (const defId of scopes.qualifiedNames.get(targetQualifiedName)) { + const def = scopes.defs.get(defId); + if (def === undefined) continue; + const target = targetForDef(def, graphTargets); + if (target !== undefined) candidates.push(target); + } + if (candidates.length === 0) { + const normalizedTarget = normalizeSymbolName(targetQualifiedName); + candidates.push(...(targetIndexes.byQualifiedName.get(normalizedTarget) ?? [])); + } + if (candidates.length === 0) { + candidates.push( + ...resolveBoundMemberCandidates( + targetQualifiedName, + targetName, + inScope, + scopes, + graphTargets, + targetIndexes, + ), + ); + } + // HARD INVARIANT — a qualified/bound reference that resolves to nothing + // must return NOTHING, never degrade to a workspace-wide same-simple-name + // lookup. Language layers depend on this as their over-capture backstop: + // Rust's `scoped_identifier` seeds cover non-callables (`Shape::Square` + // unit variants) and Go emits nothing useful for mis-shaped multi-value + // forms — both stay edge-free only because this guard holds (#2522 + // review). Relaxing it converts benign over-capture into wrong CALLS. + if (candidates.length === 0) return []; + } else { + const lexical = lexicalCallableLookup( + { name: targetName, inScope, atRange: zeroRange(), indirection: 0, addressOf: false }, + scopes, + graphTargets, + ); + candidates = lexical.targets; + lexicalShadow = lexical.shadowed; + if (candidates.length === 0 && !lexicalShadow) { + candidates = [...(globalBySimpleName.get(targetName) ?? [])]; + } + } + candidates = narrowTargetCandidates(dedupeTargets(candidates), expected, graph); + if (isUnresolvedOverloadSet(candidates)) return []; + // File-local qualified lookup should not accidentally fan out to same-name + // defs in unrelated files when the source explicitly names a local target. + const sameFile = candidates.filter((candidate) => candidate.def.filePath === filePath); + return sameFile.length > 0 ? sameFile : candidates; +} + +function resolveOperandCandidates( + filePath: string, + operand: CallableFlowOperand, + expected: CallableFlowFormalSite['expectedSignature'] | undefined, + scopes: ScopeResolutionIndexes, + graphTargets: ReadonlyMap, + globalBySimpleName: ReadonlyMap, + targetIndexes: CallableTargetIndexes, + graph: KnowledgeGraph, + allowBindingLookup = true, +): readonly Target[] { + // Plain bindings may resolve through an exact lexical/import binding (needed + // for cross-file `const assigned = importedTarget`) but never through the + // workspace-wide simple-name fallback. The latter can attach an unrelated + // same-named function to a parameter or pointer variable. + if (operand.expressionKind === undefined || operand.expressionKind === 'anonymous-callable') { + return []; + } + if (operand.expressionKind === 'binding') { + if (!allowBindingLookup) return []; + const lexical = lexicalCallableLookup(operand, scopes, graphTargets); + const candidates = narrowTargetCandidates(lexical.targets, expected, graph); + return isUnresolvedOverloadSet(candidates) ? [] : candidates; + } + return resolveSeedCandidates( + filePath, + operand.inScope, + operand.name, + operand.qualifiedName, + expected, + scopes, + graphTargets, + globalBySimpleName, + targetIndexes, + graph, + ); +} + +function resolveBoundMemberCandidates( + qualifiedName: string, + memberName: string, + inScope: ScopeId, + scopes: ScopeResolutionIndexes, + graphTargets: ReadonlyMap, + targetIndexes: CallableTargetIndexes, +): readonly Target[] { + const normalized = normalizeSymbolName(qualifiedName); + const lastDot = normalized.lastIndexOf('.'); + if (lastDot <= 0) return []; + const receiverRaw = normalized.slice(0, lastDot); + const receiverName = receiverRaw.slice(receiverRaw.lastIndexOf('.') + 1); + const receiverOperand: CallableFlowOperand = { + name: receiverName, + inScope, + atRange: zeroRange(), + indirection: 0, + addressOf: false, + }; + const typeRef = receiverType(receiverOperand, scopes) ?? receiverName; + const receiverClass = resolveInheritanceBaseInScope(inScope, typeRef, scopes); + if (receiverClass === undefined) return []; + const owners = [receiverClass.nodeId, ...scopes.methodDispatch.mroFor(receiverClass.nodeId)]; + const out: Target[] = []; + for (const owner of owners) { + for (const target of targetIndexes.byOwnerAndName.get(`${owner}\0${memberName}`) ?? []) { + const canonical = targetForDef(target.def, graphTargets); + if (canonical !== undefined) out.push(canonical); + } + if (out.length > 0) break; + } + return dedupeTargets(out); +} + +function lexicalCallableLookup( + operand: CallableFlowOperand, + scopes: ScopeResolutionIndexes, + graphTargets: ReadonlyMap, +): { readonly targets: Target[]; readonly shadowed: boolean } { + let current: ScopeId | null = operand.inScope; + const visited = new Set(); + while (current !== null && !visited.has(current)) { + visited.add(current); + const scope = scopes.scopeTree.getScope(current); + if (scope === undefined) return { targets: [], shadowed: false }; + const refs = nearestScopeBindings(current, operand.name, scope.bindings, scopes); + if (refs.length > 0) { + const out: Target[] = []; + for (const ref of refs) { + const target = targetForDef(ref.def, graphTargets); + if (target !== undefined) out.push(target); + } + return { targets: dedupeTargets(out), shadowed: true }; + } + current = scope.parent; + } + return { targets: [], shadowed: false }; +} + +function nearestScopeBindings( + scopeId: ScopeId, + name: string, + local: ReadonlyMap, + scopes: ScopeResolutionIndexes, +): readonly { readonly def: SymbolDefinition }[] { + const out: { readonly def: SymbolDefinition }[] = []; + const seen = new Set(); + const add = (refs: readonly { readonly def: SymbolDefinition }[] | undefined): void => { + for (const ref of refs ?? []) { + if (seen.has(ref.def.nodeId)) continue; + seen.add(ref.def.nodeId); + out.push(ref); + } + }; + add(local.get(name)); + add(scopes.bindings.get(scopeId)?.get(name)); + add(scopes.bindingAugmentations.get(scopeId)?.get(name)); + if (out.length === 0 && scopes.scopeTree.getScope(scopeId)?.kind === 'Module') { + add(scopes.workspaceFqnBindings.get(name)); + for (const namespace of scopes.accessibleNamespacesByScope.get(scopeId) ?? []) { + add(scopes.namespaceFqnBindings.get(namespace)?.get(name)); + } + } + return out; +} + +function canonicalBindingKey( + filePath: string, + operand: CallableFlowOperand, + scopes: ScopeResolutionIndexes, +): string { + let current: ScopeId | null = operand.inScope; + const visited = new Set(); + let enclosingFunction: ScopeId | undefined; + while (current !== null && !visited.has(current)) { + visited.add(current); + const scope = scopes.scopeTree.getScope(current); + if (scope === undefined) break; + if (enclosingFunction === undefined && scope.kind === 'Function') enclosingFunction = current; + if (nearestScopeBindings(current, operand.name, scope.bindings, scopes).length > 0) { + return `${filePath}\0${current}\0${operand.name}`; + } + current = scope.parent; + } + // Some grammars emit parameter type-bindings but no declaration binding. + // Canonicalize unresolved names to the enclosing function (not the nested + // block occurrence) so a formal and its body uses still share one cell. + return `${filePath}\0${enclosingFunction ?? operand.inScope}\0${operand.name}`; +} + +function sourceOperand(site: CallableFlowSite): CallableFlowOperand | undefined { + switch (site.kind) { + case 'copy': + case 'alias': + case 'address': + case 'store': + case 'argument': + return site.source; + case 'load': + return site.pointer; + default: + return undefined; + } +} + +interface IndexedFormal { + readonly filePath: string; + readonly site: CallableFlowFormalSite; + /** Exact definition owning this formal, even when overloads share a graph id. */ + readonly ownerDef?: SymbolDefinition; +} + +function indexFormalsByGraphId( + parsedFiles: readonly ParsedFile[], + scopes: ScopeResolutionIndexes, + nodeLookup: GraphNodeLookup, + targetIndexes: CallableTargetIndexes, + aliasesByTargetId: ReadonlyMap, +): ReadonlyMap> { + const building = new Map>(); + for (const parsed of parsedFiles) { + for (const site of parsed.callableFlowSites ?? []) { + if (site.kind !== 'formal') continue; + const resolvedCaller = resolveCallerGraphId( + site.binding.inScope, + scopes, + nodeLookup, + site.ownerRange, + ); + // Signature-bearing graph ids can be more precise than a scope-only + // caller lookup. Join the provider-supplied owner identity to canonical + // defs as a fallback (important for function-reference parameters). + const fallbackTargets = + targetIndexes.byFileAndName.get(`${parsed.filePath}\0${site.ownerName}`) ?? []; + const anchoredFallbacks = fallbackTargets.filter( + (target) => + target.def.filePath === parsed.filePath && + definitionStartsAt(target.def, site.ownerRange.startLine, site.ownerRange.startCol), + ); + const resolvedFallbacks = fallbackTargets.filter((target) => target.id === resolvedCaller); + const ownerTargets = + anchoredFallbacks.length > 0 + ? anchoredFallbacks + : resolvedFallbacks.length === 1 + ? resolvedFallbacks + : []; + const indexFormal = (graphId: string, ownerDef?: SymbolDefinition): void => { + let byIndex = building.get(graphId); + if (byIndex === undefined) { + byIndex = new Map(); + building.set(graphId, byIndex); + } + const bucket = byIndex.get(site.parameterIndex); + const indexed = { filePath: parsed.filePath, site, ownerDef }; + if (bucket === undefined) byIndex.set(site.parameterIndex, [indexed]); + else if ( + !bucket.some( + (entry) => + entry.site.binding.inScope === site.binding.inScope && + entry.ownerDef?.nodeId === ownerDef?.nodeId, + ) + ) { + bucket.push(indexed); + } + }; + for (const target of ownerTargets) { + if (target.def.filePath !== parsed.filePath) continue; + if ( + target.def.parameterCount !== undefined && + site.parameterIndex >= target.def.parameterCount + ) { + continue; + } + indexFormal(target.id, target.def); + for (const aliasId of aliasesByTargetId.get(target.id) ?? []) { + indexFormal(aliasId, target.def); + } + } + // Some providers can resolve an owner graph node without retaining a + // callable SymbolDefinition. Preserve that established fallback; it + // simply cannot participate in overload-owner narrowing. + if ( + ownerTargets.length === 0 && + fallbackTargets.length === 0 && + resolvedCaller !== undefined + ) { + indexFormal(resolvedCaller); + } + } + } + return building; +} + +function indexCallSignatures( + parsedFiles: readonly ParsedFile[], +): ReadonlyMap { + const out = new Map(); + for (const parsed of parsedFiles) { + for (const site of parsed.referenceSites) { + if (site.kind !== 'call') continue; + const signature: CallableFlowExpectedSignature = { + ...(site.arity !== undefined ? { parameterCount: site.arity } : {}), + ...(site.argumentTypes !== undefined ? { parameterTypes: site.argumentTypes } : {}), + ...(site.argumentTypeClasses !== undefined + ? { parameterTypeClasses: site.argumentTypeClasses } + : {}), + }; + const key = callableFlowSiteKey(parsed.filePath, site.atRange); + const previous = out.get(key); + if (previous === undefined || signatureEvidence(signature) > signatureEvidence(previous)) { + out.set(key, signature); + } + } + } + return out; +} + +function signatureEvidence(signature: CallableFlowExpectedSignature): number { + return ( + (signature.parameterCount === undefined ? 0 : 1) + + (signature.parameterTypes?.filter((type) => type.length > 0).length ?? 0) + + (signature.parameterTypeClasses?.length ?? 0) + ); +} + +function narrowIndexedFormals( + formals: readonly IndexedFormal[], + callSignature: CallableFlowExpectedSignature | undefined, +): readonly IndexedFormal[] { + if (formals.length < 2 || callSignature === undefined) return formals; + const owners = dedupeDefinitions(formals.flatMap((formal) => formal.ownerDef ?? [])); + if (owners.length < 2) return formals; + const narrowed = narrowOverloadCandidates( + owners, + callSignature.parameterCount, + callSignature.parameterTypes, + { argumentTypeClasses: callSignature.parameterTypeClasses }, + ); + const allowed = new Set(narrowed.map((owner) => owner.nodeId)); + return formals.filter( + (formal) => formal.ownerDef === undefined || allowed.has(formal.ownerDef.nodeId), + ); +} + +function dedupeDefinitions(defs: readonly SymbolDefinition[]): SymbolDefinition[] { + return [...new Map(defs.map((def) => [def.nodeId, def])).values()]; +} + +function definitionStartsAt(def: SymbolDefinition, line: number, column: number): boolean { + const match = def.nodeId.match(/#(\d+):(\d+):/); + return match !== null && Number(match[1]) === line && Number(match[2]) === column; +} + +function expandMemberTargets( + invoke: FileInvoke, + targets: ReadonlyMap, + scopes: ScopeResolutionIndexes, + graphTargets: ReadonlyMap, + targetIndexes: CallableTargetIndexes, + graph: KnowledgeGraph, +): Map { + if (invoke.site.invocationKind !== 'member-pointer' || invoke.site.receiver === undefined) { + return new Map(targets); + } + const typeRef = receiverType(invoke.site.receiver, scopes); + if (typeRef === undefined) return new Map(targets); + const receiverClass = resolveInheritanceBaseInScope( + invoke.site.receiver.inScope, + typeRef, + scopes, + ); + if (receiverClass === undefined) return new Map(targets); + const ownerChain = [receiverClass.nodeId, ...scopes.methodDispatch.mroFor(receiverClass.nodeId)]; + const out = new Map(); + for (const target of targets.values()) { + const name = simpleName(target.def.qualifiedName); + if (name === undefined || target.def.ownerId === undefined) { + out.set(target.id, target); + continue; + } + // A C++ pointer to a non-virtual member names that exact implementation. + // Dynamic replacement is legal only when the pointed-to base method is + // virtual; treating every member pointer as virtual invents derived calls. + if (graph.getNode(target.id)?.properties.isVirtual !== true) { + out.set(target.id, target); + continue; + } + let replacement: Target | undefined; + for (const ownerId of ownerChain) { + const compatible = (targetIndexes.byOwnerAndName.get(`${ownerId}\0${name}`) ?? []).filter( + (candidate) => sameCallableSignature(target, candidate, graph), + ); + const distinct = new Map(compatible.map((candidate) => [candidate.id, candidate])); + if (distinct.size !== 1) continue; + const candidate = distinct.values().next().value as Target; + replacement = targetForDef(candidate.def, graphTargets); + if (replacement !== undefined) break; + } + out.set((replacement ?? target).id, replacement ?? target); + } + return out; +} + +function receiverType( + operand: CallableFlowOperand, + scopes: ScopeResolutionIndexes, +): string | undefined { + let current: ScopeId | null = operand.inScope; + const visited = new Set(); + while (current !== null && !visited.has(current)) { + visited.add(current); + const scope = scopes.scopeTree.getScope(current); + if (scope === undefined) return undefined; + const hit = scope.typeBindings.get(operand.name); + if (hit !== undefined) return hit.rawName; + current = scope.parent; + } + return scopes.workspaceTypeBindings.get(operand.name)?.rawName; +} + +function targetForDef( + def: SymbolDefinition, + targets: ReadonlyMap, +): Target | undefined { + return targets.get(def.nodeId); +} + +function isCallable(def: SymbolDefinition): boolean { + return def.type === 'Function' || def.type === 'Method' || def.type === 'Constructor'; +} + +function simpleName(qualifiedName: string | undefined): string | undefined { + if (qualifiedName === undefined || qualifiedName.length === 0) return undefined; + const normalized = qualifiedName.replaceAll('::', '.').replaceAll('\\', '.'); + return normalized.slice(normalized.lastIndexOf('.') + 1); +} + +function effectiveQualifiedName( + def: SymbolDefinition, + scopes: ScopeResolutionIndexes, +): string | undefined { + const ownName = simpleName(def.qualifiedName); + if (ownName === undefined) return undefined; + if (def.ownerId === undefined) return normalizeSymbolName(def.qualifiedName!); + const owner = scopes.defs.get(def.ownerId); + const ownerName = owner === undefined ? undefined : effectiveQualifiedName(owner, scopes); + return ownerName === undefined ? ownName : `${ownerName}.${ownName}`; +} + +function normalizeSymbolName(name: string): string { + return name.replaceAll('::', '.').replaceAll('\\', '.'); +} + +function dedupeTargets(targets: readonly Target[]): Target[] { + return [...new Map(targets.map((target) => [target.def.nodeId, target])).values()]; +} + +function narrowTargetCandidates( + candidates: readonly Target[], + expected: CallableFlowExpectedSignature | undefined, + graph: KnowledgeGraph, +): Target[] { + if (candidates.length === 0) return []; + const narrowedDefs = narrowOverloadCandidates( + candidates.map((candidate) => candidate.def), + expected?.parameterCount, + expected?.parameterTypes, + ); + const allowedDefs = new Set(narrowedDefs.map((def) => def.nodeId)); + return candidates.filter((candidate) => { + if (!allowedDefs.has(candidate.def.nodeId)) return false; + if (expected?.isConst === undefined) return true; + const candidateIsConst = graph.getNode(candidate.id)?.properties.isConst === true; + return candidateIsConst === expected.isConst; + }); +} + +function narrowTargetMap( + targets: ReadonlyMap, + expected: CallableFlowExpectedSignature | undefined, + graph: KnowledgeGraph, +): Map { + return new Map( + narrowTargetCandidates([...targets.values()], expected, graph).map((target) => [ + target.id, + target, + ]), + ); +} + +function sameTargetSet( + left: ReadonlyMap | undefined, + right: ReadonlyMap, +): boolean { + if (left === undefined || left.size !== right.size) return false; + for (const id of left.keys()) if (!right.has(id)) return false; + return true; +} + +function sameCallableSignature(base: Target, candidate: Target, graph: KnowledgeGraph): boolean { + const left = base.def; + const right = candidate.def; + if ( + left.parameterCount !== undefined && + right.parameterCount !== undefined && + left.parameterCount !== right.parameterCount + ) { + return false; + } + if (!sameOptionalArray(left.parameterTypes, right.parameterTypes)) return false; + if (!sameOptionalArray(left.parameterTypeClasses, right.parameterTypeClasses)) return false; + const leftConst = graph.getNode(base.id)?.properties.isConst === true; + const rightConst = graph.getNode(candidate.id)?.properties.isConst === true; + return leftConst === rightConst; +} + +function sameOptionalArray(left: readonly T[] | undefined, right: readonly T[] | undefined) { + if (left === undefined || right === undefined) return left === right; + return JSON.stringify(left) === JSON.stringify(right); +} + +function isUnresolvedOverloadSet(targets: readonly Target[]): boolean { + if (targets.length <= 1) return false; + const first = targets[0]!.def; + return targets.every( + (target) => + target.def.filePath === first.filePath && + target.def.qualifiedName === first.qualifiedName && + target.def.type === first.type, + ); +} + +function posKey(range: { readonly startLine: number; readonly startCol: number }): string { + return `${range.startLine}:${range.startCol}`; +} + +function zeroRange() { + return { startLine: 0, startCol: 0, endLine: 0, endCol: 0 } as const; +} diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts index 04382f6be..ccb36230e 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts @@ -89,10 +89,13 @@ export function emitFreeCallFallback( * candidate (monotonicity). */ readonly constraintCompatibility?: ScopeResolver['constraintCompatibility']; readonly recordResolutionOutcome?: ResolutionOutcomeRecorder; - /** Resolved-callee-id capture sink (#2227 U2). Threaded in only under - * `--pdg`; `undefined` ⇒ zero overhead, byte-identity (R4). Captured at - * the CALLS emit below BEFORE the collapsed `seen` dedup (KTD6) so - * same-target multi-line calls are still recorded per site. */ + /** Call sites owned by a later precise pass (for example callable-value-flow). */ + readonly skipSites?: ReadonlySet; + /** Resolved-callee-id capture sink (#2227 U2). Threaded in under `--pdg` + * OR for callable-flow's direct-target index (#2437, position-filtered); + * `undefined` ⇒ zero overhead, byte-identity (R4). Captured at the CALLS + * emit below BEFORE the collapsed `seen` dedup (KTD6) so same-target + * multi-line calls are still recorded per site. */ readonly calleeIdSink?: CalleeIdSink; } = {}, ): number { @@ -123,6 +126,7 @@ export function emitFreeCallFallback( for (const site of parsed.referenceSites) { if (site.kind !== 'call') continue; if (site.explicitReceiver !== undefined) continue; + if (options.skipSites?.has(siteKey(parsed.filePath, site)) === true) continue; // Constructor form (`new User(...)`): resolve the class, then // emit CALLS to its explicit Constructor def (when present) or diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/property-dispatch.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/property-dispatch.ts new file mode 100644 index 000000000..b22799776 --- /dev/null +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/property-dispatch.ts @@ -0,0 +1,148 @@ +/** + * `emitPropertyDispatchCalls` — value-ref registration edges + field-based + * dispatch for functions registered as object-literal property values + * (#2437). + * + * A provider-hook registration (`{ emitScopeCaptures: emitCppScopeCaptures }`) + * emits a USES reference edge — a registration is not an invocation (Kythe + * `ref` vs `ref/call`; Joern `METHOD_REF`). The invocation happens later + * through the property (`provider.emitScopeCaptures(...)`) — a dispatch the + * receiver-bound pass cannot resolve because object literals are not + * IMPLEMENTS-linked implementors. This pass closes that soundness gap the + * field-based way (Feldthaus et al., ICSE'13; CodeQL `impliedReceiverStep`): + * key registrations by property name and connect every member-call site + * `x.(...)` to every function registered under ``. + * + * This pass is the SINGLE owner of `value-ref` resolution. The shared + * registries only consult pre-finalize local bindings — imported names live + * in finalized bindings (the same reason free calls need + * `emitFreeCallFallback`) — so `resolveReferenceSites` skips `value-ref` + * sites and this pass resolves them post-finalize via + * `findCallableBindingInScope` (Function/Method/Constructor only — the + * callable gate that keeps `{ port: DEFAULT_PORT }` from emitting anything). + * + * Precision posture (mirrors `emitInterfaceDispatchFor`): + * - reason `'property-dispatch'` keeps synthesized CALLS auditable; + * - dispatch confidence 0.7 sits below the 0.85 resolved baseline; + * - a per-key fan-out cap drops promiscuous names (`handler`, `callback`) + * entirely rather than truncating silently — property-name collisions + * across unrelated objects are the documented field-based failure mode. + * + * Ordering: runs AFTER the precise emit passes. `graph.addRelationship` is + * first-write-wins on the position-keyed edge id, so a site that already + * resolved precisely to the same target keeps its precise edge. + * + * Language-neutral: consumes only `value-ref` sites (any language that + * emits the capture participates) and generic member-call sites. + */ + +import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared'; +import type { KnowledgeGraph } from '../../../graph/types.js'; +import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js'; +import { tryEmitEdge, type CalleeIdCaptureCtx } from '../graph-bridge/edges.js'; +import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js'; +import type { CalleeIdSink } from '../graph-bridge/callee-id-sink.js'; +import { findCallableBindingInScope } from '../scope/walkers.js'; + +/** + * Keys registered by more than this many distinct functions are skipped — + * dispatch through such a name says nothing about which function runs. + * Calibrated on this repo's own provider tables: `emitScopeCaptures` has 16 + * legitimate registrations (one per language provider), so the cap sits at + * 2× that — dropping the motivating key was the failure the first value (8) + * had. ponytail: flat cap; revisit with per-receiver narrowing if real + * repos show useful keys being dropped (§12 of the #2437 plan). + */ +export const MAX_PROPERTY_DISPATCH_FANOUT = 32; + +/** Below the 0.85 resolved baseline; same discount idea as interface-dispatch. */ +export const PROPERTY_DISPATCH_CONFIDENCE = 0.7; + +export function emitPropertyDispatchCalls( + graph: KnowledgeGraph, + scopes: ScopeResolutionIndexes, + parsedFiles: readonly ParsedFile[], + nodeLookup: GraphNodeLookup, + calleeIdSink?: CalleeIdSink, +): { + usesEmitted: number; + callsEmitted: number; + skippedKeys: number; + skippedKeyNames: readonly string[]; +} { + const seen = new Set(); + let usesEmitted = 0; + + // Sweep 1 — resolve every value-ref site: emit the USES registration edge + // and index dispatchable registrations by property key. + const registrations = new Map>(); + for (const parsed of parsedFiles) { + for (const site of parsed.referenceSites) { + if (site.kind !== 'value-ref') continue; + const def = findCallableBindingInScope(site.inScope, site.name, scopes); + if (def === undefined) continue; + + const ok = tryEmitEdge( + graph, + scopes, + nodeLookup, + site, + def, + 'scope-resolution: value-ref', + seen, + ); + if (ok) usesEmitted++; + + if (site.propertyKey === undefined) continue; + let byDef = registrations.get(site.propertyKey); + if (byDef === undefined) { + byDef = new Map(); + registrations.set(site.propertyKey, byDef); + } + byDef.set(def.nodeId, def); + } + } + + // Keep the dropped key NAMES (bounded) — an over-cap key means no CALLS + // are synthesized through it, and a count alone leaves the operator unable + // to tell WHICH hook table lost coverage (#2522 review). + let skippedKeys = 0; + const skippedKeyNames: string[] = []; + for (const [key, defs] of registrations) { + if (defs.size > MAX_PROPERTY_DISPATCH_FANOUT) { + registrations.delete(key); + skippedKeys++; + if (skippedKeyNames.length < 20) skippedKeyNames.push(key); + } + } + + // Sweep 2 — synthesize CALLS from member-call sites through registered keys. + let callsEmitted = 0; + if (registrations.size > 0) { + for (const parsed of parsedFiles) { + const calleeCapture: CalleeIdCaptureCtx | undefined = + calleeIdSink !== undefined ? { sink: calleeIdSink, filePath: parsed.filePath } : undefined; + for (const site of parsed.referenceSites) { + if (site.kind !== 'call' || site.callForm !== 'member') continue; + const defs = registrations.get(site.name); + if (defs === undefined) continue; + for (const def of defs.values()) { + const ok = tryEmitEdge( + graph, + scopes, + nodeLookup, + site, + def, + 'property-dispatch', + seen, + PROPERTY_DISPATCH_CONFIDENCE, + false, + calleeCapture, + ); + if (ok) callsEmitted++; + } + } + } + } + return { usesEmitted, callsEmitted, skippedKeys, skippedKeyNames }; +} diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts index 1cb85be14..1f3d39604 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts @@ -161,9 +161,10 @@ export function emitReceiverBoundCalls( model: SemanticModel, options: { readonly recordResolutionOutcome?: ResolutionOutcomeRecorder; - /** Resolved-callee-id capture sink (#2227 U2). Threaded in only under - * `--pdg`; `undefined` ⇒ zero overhead, byte-identity (R4). Per-file - * capture contexts are built from this + `parsed.filePath` in the loop. */ + /** Resolved-callee-id capture sink (#2227 U2). Threaded in under `--pdg` + * OR for callable-flow's direct-target index (#2437, position-filtered); + * `undefined` ⇒ zero overhead, byte-identity (R4). Per-file capture + * contexts are built from this + `parsed.filePath` in the loop. */ readonly calleeIdSink?: CalleeIdSink; } = {}, ): number { diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/phase.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/phase.ts index f88cbbaaa..eacbc4fce 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/phase.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/phase.ts @@ -152,11 +152,10 @@ export const scopeResolutionPhase: PipelinePhase = { // extract per file inside runScopeResolution. const parsedFileStorePath = ctx.options?.parseCache?.storagePath; - // Drop pre-extracted entries for standalone providers — these - // languages are skipped by the canonical guard below (line 164) - // and never consume preExtractedByPath, so holding onto their - // entries leaks memory until the cleanup loop at 262-264 which - // also never runs for skipped providers. + // Standalone providers are re-extracted from source on the main thread; + // workers intentionally do not serialize ParsedFile artifacts for them. + // Drop any stale/cache-sourced entry defensively so provider-owned regex + // capture logic is always the source of truth for the current content. for (const [path] of preExtractedByPath) { const lang = getLanguageFromFilename(path); if (lang === null) continue; @@ -205,15 +204,15 @@ export const scopeResolutionPhase: PipelinePhase = { for (const f of scannedFiles) { const fileLang = getLanguageFromFilename(f.path); if (fileLang === null) continue; - // Skip files whose grammar isn't available (optional grammars like - // swift/dart/kotlin on an install where the binding is absent or the - // user set GITNEXUS_SKIP_OPTIONAL_GRAMMARS). The parse phase already - // excluded and warned about these (parse-impl.ts); without this guard the - // file would fall through to the main-thread re-extract in run.ts and - // throw "Unsupported language" (caught, but noisy, and it needlessly - // loads the grammar on the main thread). `isLanguageAvailable` is - // memoized, so this stays O(1) per language. (#2091, #2093) - if (!isLanguageAvailable(fileLang)) continue; + // Tree-sitter providers require an available grammar. Standalone regex + // providers deliberately have none and re-extract on the main thread. + const resolver = SCOPE_RESOLVERS.get(fileLang); + if ( + resolver?.languageProvider.parseStrategy !== 'standalone' && + !isLanguageAvailable(fileLang) + ) { + continue; + } let bucket = filesByLang.get(fileLang); if (bucket === undefined) { bucket = []; @@ -304,14 +303,6 @@ export const scopeResolutionPhase: PipelinePhase = { let pdgSinkSettled = false; try { for (const [lang, provider] of SCOPE_RESOLVERS) { - // Standalone providers (COBOL, JCL) don't emit graph edges yet - // through the scope-resolution path. This is the canonical guard: - // runScopeResolution is never called for standalone providers, which - // keeps cobolPhase as the sole IMPORTS edge producer. Keep this guard - // in sync with any additional standalone providers added to - // SCOPE_RESOLVERS. - if (provider.languageProvider.parseStrategy === 'standalone') continue; - const primaryLangFiles = filesByLang.get(lang) ?? []; if (primaryLangFiles.length === 0) continue; const primaryFilePaths = primaryLangFiles.map((f) => f.path); diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts index 5f11120b2..542c377a6 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts @@ -68,12 +68,21 @@ import { buildPopulatedMethodDispatch } from '../graph-bridge/method-dispatch.js import { propagateImportedReturnTypes } from '../passes/imported-return-types.js'; import { emitReceiverBoundCalls } from '../passes/receiver-bound-calls.js'; import { emitFreeCallFallback } from '../passes/free-call-fallback.js'; +import { + emitPropertyDispatchCalls, + MAX_PROPERTY_DISPATCH_FANOUT, +} from '../passes/property-dispatch.js'; import { emitReferencesViaLookup } from '../graph-bridge/references-to-edges.js'; import { createCalleeIdAccumulator, type CalleeIdAccumulator, } from '../graph-bridge/callee-id-sink.js'; import { emitImportEdges } from '../graph-bridge/imports-to-edges.js'; +import { + callableFlowSiteKey, + collectDeferredIndirectSites, + emitCallableValueFlow, +} from '../passes/callable-value-flow.js'; import type { ScopeResolver } from '../contract/scope-resolver.js'; import { findEnclosingClassDef, resolveInheritanceBaseInScope } from '../scope/walkers.js'; import { buildWorkspaceResolutionIndex } from '../workspace-index.js'; @@ -410,6 +419,12 @@ interface RunScopeResolutionStats { readonly resolve: ResolveStats; readonly referenceEdgesEmitted: number; readonly referenceSkipped: number; + /** + * Property-dispatch keys dropped for exceeding the fan-out cap. Non-zero + * means member calls through those keys got NO synthesized CALLS — the + * #2437 false-safe gap for exactly those keys (names are in the warn log). + */ + readonly propertyDispatchSkippedKeys: number; readonly resolutionOutcomes: readonly ResolutionOutcome[]; /** * Per-function taint summaries harvested in the pdg window (#2084 M4 U1). @@ -434,6 +449,7 @@ export function runScopeResolution( provider: ScopeResolver, ): RunScopeResolutionStats { const { graph, files } = input; + const callableFlowOnly = provider.scopeResolutionEdgeMode === 'callable-flow-only'; const onWarn = input.onWarn ?? (() => {}); const resolutionOutcomes: ResolutionOutcome[] = []; const recordResolutionOutcome: ResolutionOutcomeRecorder = (outcome) => { @@ -521,6 +537,28 @@ export function runScopeResolution( ); provider.populateWorkspaceOwners?.(parsedFiles, { fileContents: getFileContents() }); + // A callable-flow-only provider has no reason to build the whole-graph + // lookup or finalize ordinary references when none of its files emitted a + // callable fact. This keeps the opt-in path proportional to source scanning + // for repositories that use the provider but no first-class callables. + if ( + callableFlowOnly && + !parsedFiles.some((parsed) => (parsed.callableFlowSites?.length ?? 0) > 0) + ) { + return { + filesProcessed: parsedFiles.length, + filesSkipped, + importsEmitted: 0, + resolve: { sitesProcessed: 0, referencesEmitted: 0, unresolved: 0 }, + referenceEdgesEmitted: 0, + referenceSkipped: 0, + propertyDispatchSkippedKeys: 0, + resolutionOutcomes, + functionSummaries: [], + callSummaries: [], + }; + } + // Reconcile scope-resolution's ownership view into the SemanticModel. // See `reconcile-ownership.ts` for the full rationale (Contract // Invariant I9). Debug-mode validator runs immediately after to @@ -543,6 +581,7 @@ export function runScopeResolution( resolve: { sitesProcessed: 0, referencesEmitted: 0, unresolved: 0 }, referenceEdgesEmitted: 0, referenceSkipped: 0, + propertyDispatchSkippedKeys: 0, resolutionOutcomes, functionSummaries: [], callSummaries: [], @@ -573,18 +612,24 @@ export function runScopeResolution( }, }); logHeapProbe('sr-post-finalize', `lang=${provider.language}`); - const preEmittedInheritanceSites = preEmitInheritanceEdges(graph, finalized, nodeLookup); + const preEmittedInheritanceSites = callableFlowOnly + ? new Set() + : preEmitInheritanceEdges(graph, finalized, nodeLookup); // Call-based heritage hook (e.g., Ruby include/extend/prepend) — emits // IMPLEMENTS edges that `preEmitInheritanceEdges` cannot produce because // the heritage declarations are syntactic method calls, not grammar-level // heritage clauses. Must run BEFORE `buildMro` so MRO construction sees // the freshly-emitted IMPLEMENTS edges. - provider.emitHeritageEdges?.(graph, parsedFiles, nodeLookup, finalized); + if (!callableFlowOnly) { + provider.emitHeritageEdges?.(graph, parsedFiles, nodeLookup, finalized); + } // Implicit IMPORTS-edge hook — for languages whose files have compiler- // implicit cross-file visibility (no syntactic import statement). The // finalized-ImportEdge pipeline (`emitImportEdges`) cannot produce these // because there is no `ImportEdge` to materialize. Idempotent. - provider.emitImplicitImportEdges?.(graph, parsedFiles, nodeLookup, resolutionConfig); + if (!callableFlowOnly) { + provider.emitImplicitImportEdges?.(graph, parsedFiles, nodeLookup, resolutionConfig); + } // Rebuild the node lookup after heritage-edge emission. Languages like // Ruby create Property graph nodes inside `emitHeritageEdges`; those // nodes must be visible to downstream passes (`emitReceiverBoundCalls` @@ -592,15 +637,19 @@ export function runScopeResolution( // `nodeLookup`). Without this rebuild, Property nodes added by the // heritage hook are invisible and ACCESSES edges silently fail to emit. const postHeritageNodeLookup = - provider.emitHeritageEdges !== undefined ? buildGraphNodeLookup(graph) : nodeLookup; - emitDetectedInterfaceImplementations( - graph, - parsedFiles, - postHeritageNodeLookup, - provider, - finalized, - readonlyModel, - ); + !callableFlowOnly && provider.emitHeritageEdges !== undefined + ? buildGraphNodeLookup(graph) + : nodeLookup; + if (!callableFlowOnly) { + emitDetectedInterfaceImplementations( + graph, + parsedFiles, + postHeritageNodeLookup, + provider, + finalized, + readonlyModel, + ); + } const mroByClassDefId = provider.buildMro(graph, parsedFiles, postHeritageNodeLookup); const extendsOnlyMroByClassDefId = provider.buildExtendsOnlyMro?.( graph, @@ -721,29 +770,47 @@ export function runScopeResolution( // ── Phase 4: emit graph edges (LOAD-BEARING ORDER — see I1) ──────────── input.onProgress?.('linking symbols', files.length, files.length); const handledSites = new Set(preEmittedInheritanceSites); - // Resolved-callee-id capture accumulator (#2227 U2). Created ONLY under - // `--pdg` — `undefined` otherwise so the three emitters do zero work and emit - // byte-identical output (R4). Populated below at all three CALLS emit paths - // (each before its dedup, KTD6/R8); consumed by the CFG-emit join (U3) at - // `emitFileCfgs` below to produce `BasicBlock.calleeIds`. + const deferredIndirectSites = collectDeferredIndirectSites(emitParsedFiles, indexes); + const callableArgumentSites = new Set(); + if (input.pdg !== true && deferredIndirectSites.size > 0) { + for (const parsed of emitParsedFiles) { + for (const site of parsed.callableFlowSites ?? []) { + if (site.kind === 'argument') { + callableArgumentSites.add(callableFlowSiteKey(parsed.filePath, site.callSite)); + } + } + } + } + // Resolved-callee-id accumulator (#2227 U2 + callable-value-flow). Created + // for PDG OR when indirect-call facts need direct targets for actual→formal + // propagation. Populated below at every CALLS emit path before dedup; the CFG + // join still consumes it only inside the `input.pdg` block. const calleeIdAccumulator: CalleeIdAccumulator | undefined = - input.pdg === true ? createCalleeIdAccumulator() : undefined; - const receiverExtras = emitReceiverBoundCalls( - graph, - indexes, - emitParsedFiles, - postHeritageNodeLookup, - handledSites, - provider, - workspaceIndex, - readonlyModel, - { - recordResolutionOutcome, - calleeIdSink: calleeIdAccumulator, - }, - ); + input.pdg === true || deferredIndirectSites.size > 0 + ? createCalleeIdAccumulator( + input.pdg === true + ? undefined + : (filePath, line, col) => callableArgumentSites.has(`${filePath}:${line}:${col}`), + ) + : undefined; + const receiverExtras = callableFlowOnly + ? 0 + : emitReceiverBoundCalls( + graph, + indexes, + emitParsedFiles, + postHeritageNodeLookup, + handledSites, + provider, + workspaceIndex, + readonlyModel, + { + recordResolutionOutcome, + calleeIdSink: calleeIdAccumulator, + }, + ); const unresolvedReceiverExtras = - provider.emitUnresolvedReceiverEdges !== undefined + !callableFlowOnly && provider.emitUnresolvedReceiverEdges !== undefined ? provider.emitUnresolvedReceiverEdges( graph, indexes, @@ -753,47 +820,106 @@ export function runScopeResolution( readonlyModel, ) : 0; - const freeCallExtras = emitFreeCallFallback( - graph, - indexes, - emitParsedFiles, - postHeritageNodeLookup, - referenceIndex, - handledSites, - readonlyModel, - workspaceIndex, - { - allowGlobalFallback: provider.allowGlobalFreeCallFallback === true, - constructorCallTargetsClass: provider.constructorCallTargetsClass === true, - isFileLocalDef: provider.isFileLocalDef, - isCallableVisibleFromCaller: provider.isCallableVisibleFromCaller, - resolveAdlCandidates: provider.resolveAdlCandidates, - conversionRankFn: provider.conversionRankFn, - conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes, - constraintCompatibility: provider.constraintCompatibility, - recordResolutionOutcome, - calleeIdSink: calleeIdAccumulator, - }, - ); - const { emitted, skipped } = emitReferencesViaLookup( - graph, - indexes, - referenceIndex, - postHeritageNodeLookup, - handledSites, - calleeIdAccumulator, - ); - const importsEmitted = emitImportEdges( - graph, - indexes.imports, - indexes.scopeTree, - provider.importEdgeReason, - ); + const freeCallExtras = callableFlowOnly + ? 0 + : emitFreeCallFallback( + graph, + indexes, + emitParsedFiles, + postHeritageNodeLookup, + referenceIndex, + handledSites, + readonlyModel, + workspaceIndex, + { + allowGlobalFallback: provider.allowGlobalFreeCallFallback === true, + constructorCallTargetsClass: provider.constructorCallTargetsClass === true, + isFileLocalDef: provider.isFileLocalDef, + isCallableVisibleFromCaller: provider.isCallableVisibleFromCaller, + resolveAdlCandidates: provider.resolveAdlCandidates, + conversionRankFn: provider.conversionRankFn, + conversionOnlyArgTypePrefixes: provider.conversionOnlyArgTypePrefixes, + constraintCompatibility: provider.constraintCompatibility, + recordResolutionOutcome, + calleeIdSink: calleeIdAccumulator, + skipSites: deferredIndirectSites, + }, + ); + const referenceSkipSites = new Set(handledSites); + for (const key of deferredIndirectSites) referenceSkipSites.add(key); + const { emitted, skipped } = callableFlowOnly + ? { emitted: 0, skipped: 0 } + : emitReferencesViaLookup( + graph, + indexes, + referenceIndex, + postHeritageNodeLookup, + referenceSkipSites, + calleeIdAccumulator, + ); + // value-ref registrations (#2437): USES edges at the registration sites + // plus field-based dispatch — synthesized CALLS from member-call sites to + // functions registered under the same property key. This runs after the + // ordinary precise passes but before callable-value-flow: property-dispatched + // wrapper calls must populate the callee accumulator before actual→formal + // propagation. `graph.addRelationship` remains first-write-wins, so precise + // edges already emitted for a site retain ownership. + const propertyDispatch = callableFlowOnly + ? { usesEmitted: 0, callsEmitted: 0, skippedKeys: 0, skippedKeyNames: [] as readonly string[] } + : emitPropertyDispatchCalls( + graph, + indexes, + emitParsedFiles, + postHeritageNodeLookup, + calleeIdAccumulator, + ); + if (propertyDispatch.skippedKeys > 0) { + // Never drop dispatch coverage silently: a hook table larger than the + // fan-out cap means member calls through those keys get no synthesized + // CALLS — the #2437 false-safe gap reappears for exactly those keys. + logger.warn( + { + lang: provider.language, + skippedKeys: propertyDispatch.skippedKeys, + skippedKeyNames: propertyDispatch.skippedKeyNames, + fanoutCap: MAX_PROPERTY_DISPATCH_FANOUT, + }, + 'property-dispatch: keys over the fan-out cap were dropped (no CALLS synthesized for them)', + ); + } + const callableValueFlow = + calleeIdAccumulator === undefined + ? { + emitted: 0, + resolvedInvokes: 0, + ambiguousInvokes: 0, + unmatchedInvokes: 0, + iterations: 0, + } + : emitCallableValueFlow({ + graph, + scopes: indexes, + parsedFiles: emitParsedFiles, + nodeLookup: postHeritageNodeLookup, + calleeIds: calleeIdAccumulator, + language: provider.language, + collapseByCallerTarget: provider.collapseMemberCallsByCallerTarget === true, + isCallableValueTarget: provider.isCallableValueTarget, + hasFileLocalCallableLinkage: provider.hasFileLocalCallableLinkage, + onWarn: (warning) => + logger.warn( + warning, + 'callable-value-flow: candidate set exceeded the cap; no partial CALLS emitted', + ), + }); + const importsEmitted = callableFlowOnly + ? 0 + : emitImportEdges(graph, indexes.imports, indexes.scopeTree, provider.importEdgeReason); // Language-specific supplementary edges (e.g. Vue template-derived // BINDS_EVENT_HANDLER / EMITS_EVENT / CALLS / ACCESSES edges). // Runs last so the full graph — including import edges — is visible. - if (provider.emitPostResolutionEdges !== undefined) { + if (!callableFlowOnly && provider.emitPostResolutionEdges !== undefined) { provider.emitPostResolutionEdges(graph, emitParsedFiles, postHeritageNodeLookup, indexes, { fileContents: getFileContents(), resolutionConfig, @@ -941,8 +1067,8 @@ export function runScopeResolution( (message) => logger.warn(message), // U3 (#2227): the resolved-callee-id map for this file (captured at the // three CALLS emit paths in U2), joined by exact call-site position to - // emit `BasicBlock.calleeIds`. `undefined` when pdg is off (the - // accumulator is only created under `input.pdg === true`). + // emit `BasicBlock.calleeIds`. Callable-flow may also have allocated + // the accumulator in a normal run, but this join remains PDG-only. calleeIdAccumulator?.get(pf.filePath), ); cfgBlocks += emitted.blocks; @@ -1198,8 +1324,16 @@ export function runScopeResolution( filesSkipped, importsEmitted, resolve: resolveStats, - referenceEdgesEmitted: emitted + receiverExtras + unresolvedReceiverExtras + freeCallExtras, + referenceEdgesEmitted: + emitted + + receiverExtras + + unresolvedReceiverExtras + + freeCallExtras + + callableValueFlow.emitted + + propertyDispatch.usesEmitted + + propertyDispatch.callsEmitted, referenceSkipped: skipped, + propertyDispatchSkippedKeys: propertyDispatch.skippedKeys, resolutionOutcomes, functionSummaries: harvestedSummaries, callSummaries: harvestedCallSummaries, diff --git a/gitnexus/src/core/ingestion/utils/callable-flow-captures.ts b/gitnexus/src/core/ingestion/utils/callable-flow-captures.ts new file mode 100644 index 000000000..45fb41d87 --- /dev/null +++ b/gitnexus/src/core/ingestion/utils/callable-flow-captures.ts @@ -0,0 +1,1245 @@ +/** + * Configurable AST-to-`@callable-flow.*` capture synthesis. + * + * The traversal is language-neutral: providers supply their grammar's node + * vocabulary and the small semantic callbacks (true-reference bindings, + * callable signatures, protocol invocation names). The central extractor + * never sees a parser node and shared ingestion code never branches on a + * language name. + */ + +import type { CaptureMatch, ParameterTypeClass } from 'gitnexus-shared'; +import { nodeToCapture, syntheticCapture, type SyntaxNode } from './ast-helpers.js'; + +export interface CallableCaptureSignature { + readonly parameterCount?: number; + readonly parameterTypes?: readonly string[]; + readonly parameterTypeClasses?: readonly ParameterTypeClass[]; + readonly isConst?: boolean; +} + +export interface CallableFlowCaptureOptions { + readonly functionNodeTypes: ReadonlySet; + readonly callNodeTypes: ReadonlySet; + readonly parameterListNodeTypes: ReadonlySet; + readonly parameterNodeTypes: ReadonlySet; + readonly bindingNodeTypes: ReadonlySet; + readonly assignmentNodeTypes: ReadonlySet; + readonly identifierNodeTypes: ReadonlySet; + /** Control-flow blocks do not introduce a new local-variable scope. */ + readonly functionScopedValueBindings?: boolean; + /** + * Declaration nodes whose callable type can contextually select an overload + * at a later assignment (for example `void (*fp)(int); fp = target;`). + */ + readonly callableSignatureDeclarationNodeTypes?: ReadonlySet; + /** Nodes that denote a named callable reference rather than a value read. */ + readonly callableReferenceNodeTypes?: ReadonlySet; + /** Member methods whose receiver itself is the callable object. */ + readonly callableProtocolMethods?: ReadonlySet; + /** Operators used for receiver-bound member-pointer invocation. */ + readonly memberPointerOperators?: ReadonlySet; + /** Provider fallback for member-pointer syntax a grammar recovers as ERROR nodes. */ + readonly memberPointerParts?: (node: SyntaxNode) => + | { + readonly receiver: SyntaxNode; + readonly member: SyntaxNode; + readonly operator: string; + } + | undefined; + readonly functionName?: (node: SyntaxNode) => string | undefined; + /** Map grammar-specific split signature/body shapes to one lexical owner. */ + readonly lexicalFunctionOwner?: (node: SyntaxNode) => SyntaxNode | undefined; + readonly parameterPassingMode?: ( + parameter: SyntaxNode, + ) => 'value' | 'reference' | 'pointer' | 'callable-object'; + readonly isTrueReferenceBinding?: (container: SyntaxNode, destination: SyntaxNode) => boolean; + readonly expectedSignature?: ( + container: SyntaxNode, + destination: SyntaxNode, + ) => CallableCaptureSignature | undefined; + readonly normalizeQualifiedName?: (raw: string) => string; + /** + * Provider-owned assignment decomposition. May return MULTIPLE pairs for + * one node — Go's multi-value `a, b := f, g` pairs positionally; the + * shared field fallback would cross-wire first-LHS with last-RHS (#2522 + * review). Returning an empty array means "recognized, but emit nothing" + * (e.g. a multi-return call RHS with mismatched arity). + */ + readonly extractAssignment?: ( + node: SyntaxNode, + ) => + | { readonly destination: SyntaxNode; readonly source: SyntaxNode } + | readonly { readonly destination: SyntaxNode; readonly source: SyntaxNode }[] + | undefined; + readonly extractFunctionParameters?: (node: SyntaxNode) => readonly SyntaxNode[] | undefined; + readonly extractCallCallee?: (node: SyntaxNode) => SyntaxNode | undefined; + readonly isCallNode?: (node: SyntaxNode) => boolean; + /** + * Languages where a bare, receiver-less, paren-less name in value position + * is a CALL, not a reference (Ruby: `action = process` invokes `process` + * and stores its return). When true, a bare name that is not a provably + * local value binding and not an explicit reference form (`method(:x)`, + * lambda/proc) emits NO flow fact — treating it as a callable minted CALLS + * edges to methods that were merely invoked (#2522 review). + */ + readonly bareNamesAreCalls?: boolean; + readonly callSiteNode?: (node: SyntaxNode) => SyntaxNode | undefined; + /** Emit a canonical call ReferenceSite when the provider query omits variable calls. */ + readonly emitCanonicalInvokeReference?: boolean; + readonly extractCallableReference?: (node: SyntaxNode) => + | { + readonly name: string; + readonly anchor: SyntaxNode; + readonly qualifiedName?: string; + } + | undefined; +} + +interface OperandSyntax { + readonly name: string; + readonly node: SyntaxNode; + readonly indirection: number; + readonly addressOf: boolean; + readonly qualifiedName?: string; + readonly directDesignator: boolean; + readonly callableReference: boolean; + readonly anonymousCallable: boolean; + readonly expressionKind: + | 'binding' + | 'callable-designator' + | 'bound-member' + | 'anonymous-callable'; +} + +interface AssignmentParts { + readonly container: SyntaxNode; + readonly destination: SyntaxNode; + readonly source: SyntaxNode; +} + +interface FunctionInfo { + readonly node: SyntaxNode; + readonly name: string; + readonly parameters: readonly SyntaxNode[]; +} + +interface ValueBindingIndex { + readonly assignmentRegionIdsByName: ReadonlyMap>; + readonly formalByOwner: ReadonlyMap>; + readonly signatureByNameAndRegion: ReadonlyMap< + string, + ReadonlyMap + >; +} + +/** + * Emit normalized flow captures in deterministic source order. + * + * One explicit DFS supplies all phases below. Query-backed emitters may still + * perform their existing query walk; this helper never reparses and remains + * linear in AST size (the scope-capture benchmark guards the scaling ratio). + */ +export function synthesizeCallableFlowCaptures( + root: SyntaxNode, + options: CallableFlowCaptureOptions, +): readonly CaptureMatch[] { + const nodes = collectNodes(root); + const functions = collectFunctions(nodes, options); + const knownCallableNames = new Set(functions.map((fn) => fn.name)); + const assignments = collectAssignments(nodes, options); + const valueBindings = buildValueBindingIndex(nodes, assignments, functions, options); + + const out: CaptureMatch[] = []; + for (const assignment of assignments) { + emitAssignmentFact(assignment, knownCallableNames, valueBindings, options, out); + } + for (const fn of functions) emitFormalFacts(fn, options, out); + for (const node of nodes) { + if ( + options.callNodeTypes.has(node.type) && + (options.isCallNode === undefined || options.isCallNode(node)) + ) { + emitCallFacts(node, knownCallableNames, valueBindings, options, out); + } + } + + out.sort((a, b) => compareCaptures(firstCapture(a), firstCapture(b))); + return out; +} + +function collectNodes(root: SyntaxNode): SyntaxNode[] { + const out: SyntaxNode[] = []; + const stack: SyntaxNode[] = [root]; + while (stack.length > 0) { + const node = stack.pop()!; + out.push(node); + const children = node.namedChildren; + for (let i = children.length - 1; i >= 0; i--) { + const child = children[i]; + if (child !== null) stack.push(child); + } + } + return out; +} + +function collectFunctions( + nodes: readonly SyntaxNode[], + options: CallableFlowCaptureOptions, +): FunctionInfo[] { + const out: FunctionInfo[] = []; + for (const node of nodes) { + if (!options.functionNodeTypes.has(node.type)) continue; + const name = options.functionName?.(node) ?? defaultFunctionName(node, options); + if (name === undefined || name.length === 0) continue; + out.push({ node, name, parameters: functionParameters(node, options) }); + } + return out; +} + +function defaultFunctionName( + node: SyntaxNode, + options: CallableFlowCaptureOptions, +): string | undefined { + const direct = node.childForFieldName('name'); + if (direct !== null) { + const id = terminalIdentifier(direct, options); + if (id !== undefined) return id.text; + } + const declarator = node.childForFieldName('declarator'); + if (declarator !== null) { + const id = bindingIdentifier(declarator, options); + if (id !== undefined) return id.text; + } + const parent = node.parent; + if (parent !== null && options.bindingNodeTypes.has(parent.type)) { + const destination = assignmentParts(parent, options)[0]?.destination; + if (destination !== undefined) return bindingIdentifier(destination, options)?.text; + } + return undefined; +} + +function functionParameters( + node: SyntaxNode, + options: CallableFlowCaptureOptions, +): readonly SyntaxNode[] { + const providerParameters = options.extractFunctionParameters?.(node); + if (providerParameters !== undefined) return providerParameters; + const explicit = + node.childForFieldName('parameters') ?? + node.childForFieldName('parameter') ?? + findFirstDescendantOfTypes(node, options.parameterListNodeTypes); + if (explicit === null) return []; + const direct = explicit.namedChildren.filter( + (child): child is SyntaxNode => + child !== null && + (options.parameterNodeTypes.has(child.type) || + (options.parameterListNodeTypes.has(explicit.type) && + bindingIdentifier(child, options) !== undefined)), + ); + return direct; +} + +function collectAssignments( + nodes: readonly SyntaxNode[], + options: CallableFlowCaptureOptions, +): AssignmentParts[] { + const out: AssignmentParts[] = []; + const seen = new Set(); + for (const node of nodes) { + if (!options.bindingNodeTypes.has(node.type) && !options.assignmentNodeTypes.has(node.type)) { + continue; + } + if (seen.has(node.id)) continue; + const parts = assignmentParts(node, options); + if (parts.length === 0) continue; + seen.add(node.id); + out.push(...parts); + } + return out; +} + +function buildValueBindingIndex( + nodes: readonly SyntaxNode[], + assignments: readonly AssignmentParts[], + functions: readonly FunctionInfo[], + options: CallableFlowCaptureOptions, +): ValueBindingIndex { + const assignmentRegionIdsByName = new Map>(); + const formalByOwner = new Map>(); + const signatureByNameAndRegion = new Map>(); + const add = ( + index: Map>, + owner: number | undefined, + name: string, + ): void => { + let names = index.get(owner); + if (names === undefined) { + names = new Set(); + index.set(owner, names); + } + names.add(name); + }; + + for (const assignment of assignments) { + const destinationNames = new Set(); + const destination = operandSyntax(assignment.destination, options, true); + if (destination !== undefined) destinationNames.add(destination.name); + // Member-path destinations (`o->run = handler`) store into the MEMBER's + // name-cell — the seed fact is keyed on `run`, so the store must be + // visible under that name too or the member-call invoke gate never sees + // it (#2522 review, M3 ops-vtable pattern). + const terminal = terminalIdentifier(assignment.destination, options); + if (terminal !== undefined) destinationNames.add(terminal.text); + for (const name of destinationNames) { + const region = nearestLexicalRegion(assignment.container, options); + let regionIds = assignmentRegionIdsByName.get(name); + if (regionIds === undefined) { + regionIds = new Set(); + assignmentRegionIdsByName.set(name, regionIds); + } + regionIds.add(region.id); + if (options.functionScopedValueBindings === true) { + const functionOwner = nearestFunctionOwner(assignment.container, options); + if (functionOwner !== undefined) regionIds.add(functionOwner.id); + } + } + } + for (const fn of functions) { + for (const parameter of fn.parameters) { + const binding = bindingIdentifier(parameter, options); + if (binding !== undefined) add(formalByOwner, fn.node.id, binding.text); + } + } + for (const node of nodes) { + if (options.callableSignatureDeclarationNodeTypes?.has(node.type) !== true) continue; + const binding = bindingIdentifier(node, options); + const signature = options.expectedSignature?.(node, node); + if (binding === undefined || signature === undefined) continue; + // Only callable-typed VARIABLE declarations (`void (*fp)(int);`) create + // value cells — a plain function/method prototype (`void f(int);`) + // declares a callee, not a value. A variable declarator interposes a + // pointer/parenthesized declarator between the declaration and its + // identifier; a prototype's identifier hangs directly off the function + // declarator. Indexing prototypes here turned every call to a declared + // function into an indirect invoke — and, with + // `emitCanonicalInvokeReference`, minted a free-call reference that + // bypassed the precise passes' two-phase/ambiguity suppression + // (#2522 CI: eight cpp resolver tests gained a phantom edge). + if (!bindingInterposesValueDeclarator(node, binding)) continue; + const region = nearestLexicalRegion(node, options); + let byRegion = signatureByNameAndRegion.get(binding.text); + if (byRegion === undefined) { + byRegion = new Map(); + signatureByNameAndRegion.set(binding.text, byRegion); + } + byRegion.set(region.id, signature); + if (options.functionScopedValueBindings === true) { + const functionOwner = nearestFunctionOwner(node, options); + if (functionOwner !== undefined) byRegion.set(functionOwner.id, signature); + } + } + return { assignmentRegionIdsByName, formalByOwner, signatureByNameAndRegion }; +} + +/** True when a pointer/parenthesized declarator sits between the declaration + * and its binding identifier — the shape of a callable-typed variable, never + * of a plain prototype. Only C/C++ supply signature-declaration node types, + * so the type-name sniff stays scoped to those grammars. */ +function bindingInterposesValueDeclarator(declaration: SyntaxNode, binding: SyntaxNode): boolean { + let node: SyntaxNode | null = binding.parent; + while (node !== null && node.id !== declaration.id) { + if (node.type.includes('pointer') || node.type.includes('parenthesized')) return true; + node = node.parent; + } + return false; +} + +function nearestFunctionOwner( + input: SyntaxNode, + options: CallableFlowCaptureOptions, +): SyntaxNode | undefined { + const providerOwner = options.lexicalFunctionOwner?.(input); + if (providerOwner !== undefined) return providerOwner; + let node: SyntaxNode | null = input; + while (node !== null) { + if (options.functionNodeTypes.has(node.type)) return node; + node = node.parent; + } + return undefined; +} + +function nearestLexicalRegion(input: SyntaxNode, options: CallableFlowCaptureOptions): SyntaxNode { + const providerOwner = options.lexicalFunctionOwner?.(input); + let node: SyntaxNode | null = input.parent; + let outermost = input; + while (node !== null) { + outermost = node; + if (providerOwner !== undefined && node.id === providerOwner.id) return node; + if (options.functionNodeTypes.has(node.type) || isLexicalRegionNode(node)) return node; + node = node.parent; + } + return providerOwner ?? outermost; +} + +function isLexicalRegionNode(node: SyntaxNode): boolean { + return /(?:^|_)(?:block|body|compound_statement|source_file|program|module|script)(?:$|_)/.test( + node.type, + ); +} + +function isVisibleValueBinding( + input: SyntaxNode, + name: string, + bindings: ValueBindingIndex, + options: CallableFlowCaptureOptions, +): boolean { + const assignmentRegionIds = bindings.assignmentRegionIdsByName.get(name); + const providerOwner = options.lexicalFunctionOwner?.(input); + if ( + providerOwner !== undefined && + (assignmentRegionIds?.has(providerOwner.id) === true || + bindings.formalByOwner.get(providerOwner.id)?.has(name) === true) + ) { + return true; + } + let node: SyntaxNode | null = input; + while (node !== null) { + if (assignmentRegionIds?.has(node.id) === true) return true; + if ( + options.functionNodeTypes.has(node.type) && + bindings.formalByOwner.get(node.id)?.has(name) === true + ) { + return true; + } + node = node.parent; + } + if (bindings.formalByOwner.get(undefined)?.has(name) === true) return true; + // A declared callable-typed binding (file-scope `void (*fp)(int);`) is a + // value binding wherever its declaration is visible — its assignments may + // live in OTHER functions (the init/register callback pattern), so + // assignment regions alone under-approximate visibility and the cross- + // function call emitted no invoke fact at all (#2522 review, H1). + return visibleCallableSignature(input, name, bindings, options) !== undefined; +} + +function visibleCallableSignature( + input: SyntaxNode, + name: string, + bindings: ValueBindingIndex, + options: CallableFlowCaptureOptions, +): CallableCaptureSignature | undefined { + const byRegion = bindings.signatureByNameAndRegion.get(name); + if (byRegion === undefined) return undefined; + const providerOwner = options.lexicalFunctionOwner?.(input); + if (providerOwner !== undefined) { + const signature = byRegion.get(providerOwner.id); + if (signature !== undefined) return signature; + } + let node: SyntaxNode | null = input; + while (node !== null) { + const signature = byRegion.get(node.id); + if (signature !== undefined) return signature; + node = node.parent; + } + return undefined; +} + +function assignmentParts( + node: SyntaxNode, + options: CallableFlowCaptureOptions, +): readonly AssignmentParts[] { + const providerParts = options.extractAssignment?.(node); + if (providerParts !== undefined) { + const pairs = Array.isArray(providerParts) ? providerParts : [providerParts]; + return pairs.map((pair) => ({ container: node, ...pair })); + } + const destination = + node.childForFieldName('left') ?? + node.childForFieldName('name') ?? + node.childForFieldName('pattern') ?? + node.childForFieldName('declarator') ?? + node.childForFieldName('target'); // Swift assignment + const source = + node.childForFieldName('right') ?? + node.childForFieldName('value') ?? + node.childForFieldName('initializer') ?? + node.childForFieldName('default_value') ?? + node.childForFieldName('result'); // Swift assignment + + // Declaration wrappers often contain the real initialized declarator one + // level below (for example a declarator list). + if (destination === null || source === null) { + for (const child of node.namedChildren) { + if (child === null || child.id === node.id) continue; + if (!options.bindingNodeTypes.has(child.type)) continue; + const nested = assignmentParts(child, options); + if (nested.length > 0) return nested; + } + } + if (destination === null || source === null) return []; + return [{ container: node, destination, source }]; +} + +function emitAssignmentFact( + assignment: AssignmentParts, + knownCallableNames: ReadonlySet, + valueBindings: ValueBindingIndex, + options: CallableFlowCaptureOptions, + out: CaptureMatch[], +): void { + const destination = operandSyntax( + assignment.destination, + options, + options.bindingNodeTypes.has(assignment.container.type), + ); + const source = operandSyntax(assignment.source, options); + if (destination === undefined || source === undefined) return; + + const signature = + options.expectedSignature?.(assignment.container, assignment.destination) ?? + visibleCallableSignature(assignment.container, destination.name, valueBindings, options); + const sourceIsValueBinding = isVisibleValueBinding( + assignment.source, + source.name, + valueBindings, + options, + ); + if ( + options.bareNamesAreCalls === true && + source.directDesignator && + !source.callableReference && + !source.anonymousCallable && + !sourceIsValueBinding + ) { + // The RHS is the produced value of a call, not a reference to the callee. + return; + } + const sourceIsKnownCallable = + source.callableReference || + source.anonymousCallable || + (source.directDesignator && + !sourceIsValueBinding && + (knownCallableNames.has(source.name) || + source.qualifiedName !== undefined || + signature !== undefined)); + const flowSource = sourceIsKnownCallable ? asCallableDesignator(source) : source; + + // A call/constructor/composite result is not the function designator that + // appears inside it. This guard must precede the store path: otherwise + // `*slot = factory()` stores `factory` itself into `slot`. + if ( + !source.directDesignator && + !sourceIsKnownCallable && + source.indirection === 0 && + !source.addressOf + ) { + return; + } + + if (destination.indirection > 0) { + out.push( + binaryFact('store', assignment.container, 'pointer', destination, 'source', flowSource), + ); + return; + } + if (source.addressOf && !sourceIsKnownCallable) { + // An address of a value binding creates an abstract-cell edge. Addresses + // of named callables are seeds instead (function designator semantics). + if (sourceIsValueBinding) { + out.push( + binaryFact('address', assignment.container, 'destination', destination, 'source', source), + ); + } else if (source.directDesignator) { + // A declaration imported from another file is only visible after scope + // finalization. Preserve both interpretations here: the address relation + // supports pointer-to-pointer cells, while the copy lets the solver's + // exact lexical/import lookup recognize `auto fp = &importedTarget`. + out.push( + binaryFact('address', assignment.container, 'destination', destination, 'source', source), + ); + out.push( + binaryFact('copy', assignment.container, 'destination', destination, 'source', source), + ); + } + return; + } + if (source.indirection > 0 && !sourceIsKnownCallable) { + out.push( + binaryFact('load', assignment.container, 'destination', destination, 'pointer', source), + ); + return; + } + if (sourceIsKnownCallable) { + const match: Record> = { + '@callable-flow.seed': nodeToCapture('@callable-flow.seed', assignment.container), + '@callable-flow.destination': operandCapture('@callable-flow.destination', destination), + '@callable-flow.target': operandCapture('@callable-flow.target', flowSource), + '@callable-flow.target-name': syntheticCapture( + '@callable-flow.target-name', + flowSource.node, + flowSource.anonymousCallable ? destination.name : flowSource.name, + ), + }; + if (flowSource.qualifiedName !== undefined) { + match['@callable-flow.target-qualified-name'] = syntheticCapture( + '@callable-flow.target-qualified-name', + flowSource.node, + options.normalizeQualifiedName?.(flowSource.qualifiedName) ?? flowSource.qualifiedName, + ); + } + addSignatureCaptures(match, assignment.container, signature); + out.push(match); + return; + } + + const kind = options.isTrueReferenceBinding?.(assignment.container, assignment.destination) + ? 'alias' + : 'copy'; + out.push( + binaryFact(kind, assignment.container, 'destination', destination, 'source', flowSource), + ); +} + +function emitFormalFacts( + fn: FunctionInfo, + options: CallableFlowCaptureOptions, + out: CaptureMatch[], +): void { + for (let index = 0; index < fn.parameters.length; index++) { + const parameter = fn.parameters[index]!; + const binding = bindingIdentifier(parameter, options); + if (binding === undefined) continue; + const mode = options.parameterPassingMode?.(parameter) ?? 'value'; + const match: Record> = { + '@callable-flow.formal': nodeToCapture('@callable-flow.formal', fn.node), + '@callable-flow.owner': syntheticCapture('@callable-flow.owner', fn.node, fn.name), + '@callable-flow.binding': syntheticCapture('@callable-flow.binding', binding, binding.text), + '@callable-flow.parameter-index': syntheticCapture( + '@callable-flow.parameter-index', + binding, + String(index), + ), + '@callable-flow.passing-mode': syntheticCapture( + '@callable-flow.passing-mode', + parameter, + mode, + ), + }; + addSignatureCaptures(match, parameter, options.expectedSignature?.(parameter, parameter)); + out.push(match); + } +} + +function emitCallFacts( + call: SyntaxNode, + knownCallableNames: ReadonlySet, + valueBindings: ValueBindingIndex, + options: CallableFlowCaptureOptions, + out: CaptureMatch[], +): void { + const calleeNode = + options.extractCallCallee?.(call) ?? + call.childForFieldName('function') ?? + call.childForFieldName('callee') ?? + call.childForFieldName('name') ?? + call.childForFieldName('method') ?? + call.firstNamedChild; + if (calleeNode === null) return; + + const callSite = options.callSiteNode?.(call) ?? call; + const args = callArguments(call, options); + const member = directCallMemberParts(call, options) ?? memberParts(calleeNode, options); + const callee = operandSyntax(calleeNode, options); + const calleeIsValueBinding = + callee !== undefined && isVisibleValueBinding(call, callee.name, valueBindings, options); + const directCalleeName = + member === undefined && + callee !== undefined && + callee.indirection === 0 && + !calleeIsValueBinding + ? callee.name + : undefined; + for (let index = 0; index < args.length; index++) { + const rawSource = operandSyntax(args[index]!, options); + if (rawSource === undefined) continue; + // A call/constructor/composite result is a produced value, not a reference + // to the terminal identifier inside the expression. + if (!rawSource.directDesignator && rawSource.indirection === 0 && !rawSource.addressOf) { + continue; + } + const sourceIsValueBinding = isVisibleValueBinding( + args[index]!, + rawSource.name, + valueBindings, + options, + ); + if ( + options.bareNamesAreCalls === true && + rawSource.directDesignator && + !rawSource.callableReference && + !rawSource.anonymousCallable && + !sourceIsValueBinding + ) { + // Bare argument name = a call's produced value in this language. + continue; + } + const sourceIsKnownCallable = + rawSource.callableReference || + rawSource.anonymousCallable || + (rawSource.directDesignator && + !sourceIsValueBinding && + (knownCallableNames.has(rawSource.name) || rawSource.qualifiedName !== undefined)); + // Keep unresolved direct identifiers (not arbitrary expressions): imports + // and cross-file callable bindings are only known after registry finalize. + // The solver requires an exact callable lexical binding before propagating, + // so ordinary value arguments remain inert. + if ( + !sourceIsValueBinding && + !sourceIsKnownCallable && + !rawSource.directDesignator && + !rawSource.addressOf + ) { + continue; + } + const source = sourceIsKnownCallable ? asCallableDesignator(rawSource) : rawSource; + out.push({ + '@callable-flow.argument': nodeToCapture('@callable-flow.argument', callSite), + '@callable-flow.source': operandCapture('@callable-flow.source', source), + ...operandMetadataCaptures('source', source), + ...(source.indirection > 0 + ? { + '@callable-flow.source-indirection': syntheticCapture( + '@callable-flow.source-indirection', + source.node, + String(source.indirection), + ), + } + : {}), + ...(source.addressOf + ? { + '@callable-flow.source-address': syntheticCapture( + '@callable-flow.source-address', + source.node, + 'true', + ), + } + : {}), + '@callable-flow.parameter-index': syntheticCapture( + '@callable-flow.parameter-index', + args[index]!, + String(index), + ), + ...(directCalleeName !== undefined + ? { + '@callable-flow.direct-callee-name': syntheticCapture( + '@callable-flow.direct-callee-name', + callee!.node, + directCalleeName, + ), + } + : {}), + }); + } + + if (member !== undefined) { + if ( + member.operator !== undefined && + options.memberPointerOperators?.has(member.operator) === true + ) { + emitInvoke( + callSite, + member.member, + 'member-pointer', + args.length, + out, + options, + member.receiver, + ); + return; + } + if (options.callableProtocolMethods?.has(member.member.name) === true) { + emitInvoke(callSite, member.receiver, 'callable-object', args.length, out, options); + return; + } + // Field-stored callables (`o->run = handler; o->run(1)` — the C ops-vtable + // pattern): when a visible assignment wrote this member's name-cell, the + // member call is an indirect invoke through that cell (#2522 review, M3). + // Gated on the store so plain accessor calls (`map.get(x)`) stay inert; + // name-keyed field collapse matches the solver's store/load model. + // ponytail: same-region joins only — cross-function vtable installs need + // a field-sensitive cell model. + if (isVisibleValueBinding(call, member.member.name, valueBindings, options)) { + emitInvoke(callSite, member.member, 'indirect', args.length, out, options, member.receiver); + } + return; + } + + if (callee === undefined) return; + if (callee.indirection > 0 || calleeIsValueBinding) { + const invokeCallee = + !calleeIsValueBinding && knownCallableNames.has(callee.name) + ? asCallableDesignator(callee) + : callee; + emitInvoke(callSite, invokeCallee, 'indirect', args.length, out, options); + } +} + +function emitInvoke( + call: SyntaxNode, + callee: OperandSyntax, + invocationKind: 'indirect' | 'member-pointer' | 'callable-object', + arity: number, + out: CaptureMatch[], + options: CallableFlowCaptureOptions, + receiver?: OperandSyntax, +): void { + out.push({ + '@callable-flow.invoke': nodeToCapture('@callable-flow.invoke', call), + '@callable-flow.callee': operandCapture('@callable-flow.callee', callee), + ...operandMetadataCaptures('callee', callee), + ...(callee.indirection > 0 + ? { + '@callable-flow.callee-indirection': syntheticCapture( + '@callable-flow.callee-indirection', + callee.node, + String(callee.indirection), + ), + } + : {}), + ...(receiver !== undefined + ? { + '@callable-flow.receiver': operandCapture('@callable-flow.receiver', receiver), + ...operandMetadataCaptures('receiver', receiver), + } + : {}), + '@callable-flow.invocation-kind': syntheticCapture( + '@callable-flow.invocation-kind', + call, + invocationKind, + ), + '@callable-flow.arity': syntheticCapture('@callable-flow.arity', call, String(arity)), + }); + if (invocationKind === 'member-pointer' && receiver !== undefined) { + // Parenthesized member-pointer callees are not matched by the ordinary + // member-call query shape. + // Supply the same canonical callsite contract so the shared resolver can + // exact-join this fact instead of trusting an unanchored/stale side fact. + out.push({ + '@reference.call.member': nodeToCapture('@reference.call.member', call), + '@reference.receiver': operandCapture('@reference.receiver', receiver), + '@reference.name': operandCapture('@reference.name', callee), + '@reference.arity': syntheticCapture('@reference.arity', call, String(arity)), + }); + } else if (options.emitCanonicalInvokeReference === true) { + out.push({ + '@reference.call.free': nodeToCapture('@reference.call.free', call), + '@reference.name': operandCapture('@reference.name', callee), + '@reference.arity': syntheticCapture('@reference.arity', call, String(arity)), + }); + } +} + +function callArguments( + call: SyntaxNode, + options: CallableFlowCaptureOptions, +): readonly SyntaxNode[] { + const list = + call.childForFieldName('arguments') ?? + call.childForFieldName('argument') ?? + call.namedChildren.find( + (child): child is SyntaxNode => + child !== null && options.parameterListNodeTypes.has(child.type), + ) ?? + findFirstDescendantOfTypes(call, options.parameterListNodeTypes) ?? + null; + if (list === null) return []; + if (options.parameterListNodeTypes.has(list.type)) { + return list.namedChildren.filter( + (child): child is SyntaxNode => child !== null && child.type !== 'comment', + ); + } + return [list]; +} + +function directCallMemberParts( + call: SyntaxNode, + options: CallableFlowCaptureOptions, +): + | { readonly receiver: OperandSyntax; readonly member: OperandSyntax; readonly operator?: string } + | undefined { + const receiverNode = call.childForFieldName('object') ?? call.childForFieldName('receiver'); + const memberNode = call.childForFieldName('method') ?? call.childForFieldName('name'); + if (receiverNode === null || memberNode === null) return undefined; + const receiver = operandSyntax(receiverNode, options); + const member = operandSyntax(memberNode, options); + return receiver === undefined || member === undefined ? undefined : { receiver, member }; +} + +function memberParts( + input: SyntaxNode, + options: CallableFlowCaptureOptions, +): + | { readonly receiver: OperandSyntax; readonly member: OperandSyntax; readonly operator?: string } + | undefined { + const providerParts = options.memberPointerParts?.(input); + if (providerParts !== undefined) { + const receiver = operandSyntax(providerParts.receiver, options); + const member = operandSyntax(providerParts.member, options); + if (receiver !== undefined && member !== undefined) { + return { receiver, member, operator: providerParts.operator }; + } + } + let node = input; + for (let guard = 0; guard < 8; guard++) { + const wrapped = wrappedExpression(node); + if (wrapped === null) break; + node = wrapped; + } + const receiverNode = + node.childForFieldName('object') ?? + node.childForFieldName('argument') ?? + node.childForFieldName('receiver'); + const memberNode = + node.childForFieldName('property') ?? + node.childForFieldName('field') ?? + node.childForFieldName('method'); + if (receiverNode === null || memberNode === null) return undefined; + const receiver = operandSyntax(receiverNode, options); + const member = operandSyntax(memberNode, options); + if (receiver === undefined || member === undefined) return undefined; + const operator = node.children.find( + (child) => + options.memberPointerOperators?.has(child.type) === true || + options.memberPointerOperators?.has(child.text) === true, + ); + return { receiver, member, ...(operator !== undefined ? { operator: operator.text } : {}) }; +} + +function operandSyntax( + input: SyntaxNode, + options: CallableFlowCaptureOptions, + declarationBinding = false, +): OperandSyntax | undefined { + if (declarationBinding) { + const binding = bindingIdentifier(input, options); + if (binding === undefined) return undefined; + return { + name: binding.text, + node: binding, + indirection: 0, + addressOf: false, + directDesignator: false, + callableReference: false, + anonymousCallable: false, + expressionKind: 'binding', + }; + } + const providerReference = options.extractCallableReference?.(input); + if (providerReference !== undefined) { + return { + name: providerReference.name, + node: providerReference.anchor, + indirection: 0, + addressOf: false, + ...(providerReference.qualifiedName !== undefined + ? { qualifiedName: providerReference.qualifiedName } + : {}), + directDesignator: true, + callableReference: true, + anonymousCallable: false, + expressionKind: + providerReference.qualifiedName === undefined ? 'callable-designator' : 'bound-member', + }; + } + let node = input; + let indirection = 0; + let addressOf = false; + + for (let guard = 0; guard < 16; guard++) { + if (options.functionNodeTypes.has(node.type)) { + return { + name: '', + node, + indirection, + addressOf, + directDesignator: true, + callableReference: true, + anonymousCallable: true, + expressionKind: 'anonymous-callable', + }; + } + const operator = unaryOperator(node); + if (operator === '&') addressOf = true; + if (operator === '*') indirection++; + const wrapped = wrappedExpression(node); + if (wrapped === null) break; + node = wrapped; + } + + const id = terminalIdentifier(node, options); + if (id === undefined) return undefined; + const isQualified = id.id !== node.id && hasMultipleIdentifierLeaves(node, options); + const singleNameText = node.text.trim().replace(/^[\$@]+/, ''); + const valueProducingExpression = /call|invocation|creation|new_expression|composite_literal/.test( + node.type, + ); + return { + name: id.text, + node: id, + indirection, + addressOf, + ...(isQualified ? { qualifiedName: node.text } : {}), + directDesignator: + options.identifierNodeTypes.has(node.type) || + (!isQualified && singleNameText === id.text) || + (isQualified && !valueProducingExpression && !/[([{]/.test(node.text)), + callableReference: options.callableReferenceNodeTypes?.has(node.type) === true, + anonymousCallable: false, + expressionKind: isQualified + ? 'bound-member' + : options.callableReferenceNodeTypes?.has(node.type) === true + ? 'callable-designator' + : 'binding', + }; +} + +function asCallableDesignator(operand: OperandSyntax): OperandSyntax { + if (operand.expressionKind !== 'binding') return operand; + return { ...operand, expressionKind: 'callable-designator' }; +} + +function wrappedExpression(node: SyntaxNode): SyntaxNode | null { + const field = + node.childForFieldName('argument') ?? + node.childForFieldName('expression') ?? + node.childForFieldName('value'); + if (field !== null && field.id !== node.id && node.namedChildCount === 1) return field; + if ( + node.type.includes('parenthesized') || + node.type.includes('reference_expression') || + node.type.includes('pointer_expression') || + node.type.includes('unary_expression') || + node.type.includes('cast_expression') + ) { + return node.namedChild(node.namedChildCount - 1); + } + return null; +} + +function unaryOperator(node: SyntaxNode): string | undefined { + for (const child of node.children) { + if (child.text === '&' || child.text === '*') return child.text; + } + const trimmed = node.text.trimStart(); + if (trimmed.startsWith('&')) return '&'; + if (trimmed.startsWith('*')) return '*'; + return undefined; +} + +/** + * For a subscript/index expression, the identifier of interest is always in + * the container operand, never the index: `tbl[i] = h` must bind the cell + * `tbl` (index-insensitive, Andersen-style), not the index variable `i` — + * seeding `i` both pollutes a same-named formal and misses the later + * `tbl[i]()` join (#2522 review). Field names cover the grammars that field + * their subscript nodes; others keep the generic traversal. + */ +function subscriptBase(node: SyntaxNode): SyntaxNode | null { + if (node.childForFieldName('index') === null) return null; + return ( + node.childForFieldName('argument') ?? // C/C++ subscript_expression + node.childForFieldName('object') ?? // JS/TS subscript_expression + node.childForFieldName('value') ?? // Python subscript + node.childForFieldName('operand') ?? // Go index_expression + node.childForFieldName('array') // Java array_access + ); +} + +function bindingIdentifier( + node: SyntaxNode, + options: CallableFlowCaptureOptions, +): SyntaxNode | undefined { + if (options.identifierNodeTypes.has(node.type)) return node; + const base = subscriptBase(node); + if (base !== null) return bindingIdentifier(base, options); + for (const fieldName of ['name', 'declarator', 'pattern', 'left']) { + const field = node.childForFieldName(fieldName); + if (field === null || field.id === node.id) continue; + const found = bindingIdentifier(field, options); + if (found !== undefined) return found; + } + for (const child of node.namedChildren) { + if (child === null) continue; + const found = bindingIdentifier(child, options); + if (found !== undefined) return found; + } + return undefined; +} + +function terminalIdentifier( + node: SyntaxNode, + options: CallableFlowCaptureOptions, +): SyntaxNode | undefined { + if (options.identifierNodeTypes.has(node.type)) return node; + const base = subscriptBase(node); + if (base !== null) return terminalIdentifier(base, options); + for (const fieldName of ['name', 'property', 'field', 'method', 'declarator']) { + const field = node.childForFieldName(fieldName); + if (field === null || field.id === node.id) continue; + const found = terminalIdentifier(field, options); + if (found !== undefined) return found; + } + for (let i = node.namedChildCount - 1; i >= 0; i--) { + const child = node.namedChild(i); + if (child === null) continue; + const found = terminalIdentifier(child, options); + if (found !== undefined) return found; + } + return undefined; +} + +function hasMultipleIdentifierLeaves( + node: SyntaxNode, + options: CallableFlowCaptureOptions, +): boolean { + let count = 0; + const stack: SyntaxNode[] = [node]; + while (stack.length > 0) { + const current = stack.pop()!; + if (options.identifierNodeTypes.has(current.type)) { + if (++count > 1) return true; + continue; + } + for (const child of current.namedChildren) if (child !== null) stack.push(child); + } + return false; +} + +function findFirstDescendantOfTypes( + root: SyntaxNode, + types: ReadonlySet, +): SyntaxNode | null { + const stack: SyntaxNode[] = [...root.namedChildren].filter( + (child): child is SyntaxNode => child !== null, + ); + while (stack.length > 0) { + const node = stack.shift()!; + if (types.has(node.type)) return node; + for (const child of node.namedChildren) if (child !== null) stack.push(child); + } + return null; +} + +function binaryFact( + kind: 'copy' | 'alias' | 'address' | 'store' | 'load', + anchor: SyntaxNode, + leftRole: 'destination' | 'pointer', + left: OperandSyntax, + rightRole: 'source' | 'pointer', + right: OperandSyntax, +): CaptureMatch { + return { + [`@callable-flow.${kind}`]: nodeToCapture(`@callable-flow.${kind}`, anchor), + [`@callable-flow.${leftRole}`]: operandCapture(`@callable-flow.${leftRole}`, left), + ...operandMetadataCaptures(leftRole, left), + ...(left.indirection > 0 + ? { + [`@callable-flow.${leftRole}-indirection`]: syntheticCapture( + `@callable-flow.${leftRole}-indirection`, + left.node, + String(left.indirection), + ), + } + : {}), + [`@callable-flow.${rightRole}`]: operandCapture(`@callable-flow.${rightRole}`, right), + ...operandMetadataCaptures(rightRole, right), + ...(right.indirection > 0 + ? { + [`@callable-flow.${rightRole}-indirection`]: syntheticCapture( + `@callable-flow.${rightRole}-indirection`, + right.node, + String(right.indirection), + ), + } + : {}), + }; +} + +function operandCapture(name: string, operand: OperandSyntax) { + return syntheticCapture(name, operand.node, operand.name); +} + +function operandMetadataCaptures( + role: 'source' | 'destination' | 'pointer' | 'binding' | 'callee' | 'receiver', + operand: OperandSyntax, +): Record> { + return { + [`@callable-flow.${role}-kind`]: syntheticCapture( + `@callable-flow.${role}-kind`, + operand.node, + operand.expressionKind, + ), + ...(operand.qualifiedName !== undefined + ? { + [`@callable-flow.${role}-qualified-name`]: syntheticCapture( + `@callable-flow.${role}-qualified-name`, + operand.node, + operand.qualifiedName, + ), + } + : {}), + }; +} + +function addSignatureCaptures( + match: Record>, + anchor: SyntaxNode, + signature: CallableCaptureSignature | undefined, +): void { + if (signature?.parameterCount !== undefined) { + match['@callable-flow.expected-arity'] = syntheticCapture( + '@callable-flow.expected-arity', + anchor, + String(signature.parameterCount), + ); + } + if (signature?.parameterTypes !== undefined) { + match['@callable-flow.expected-types'] = syntheticCapture( + '@callable-flow.expected-types', + anchor, + JSON.stringify(signature.parameterTypes), + ); + } + if (signature?.parameterTypeClasses !== undefined) { + match['@callable-flow.expected-type-classes'] = syntheticCapture( + '@callable-flow.expected-type-classes', + anchor, + JSON.stringify(signature.parameterTypeClasses), + ); + } + if (signature?.isConst !== undefined) { + match['@callable-flow.expected-const'] = syntheticCapture( + '@callable-flow.expected-const', + anchor, + String(signature.isConst), + ); + } +} + +function firstCapture(match: CaptureMatch) { + return Object.values(match)[0]; +} + +function compareCaptures( + a: ReturnType | undefined, + b: ReturnType | undefined, +): number { + if (a === undefined) return b === undefined ? 0 : 1; + if (b === undefined) return -1; + return ( + a.range.startLine - b.range.startLine || + a.range.startCol - b.range.startCol || + a.name.localeCompare(b.name) + ); +} diff --git a/gitnexus/src/storage/parse-cache.ts b/gitnexus/src/storage/parse-cache.ts index b11b8c27f..e1eed3484 100644 --- a/gitnexus/src/storage/parse-cache.ts +++ b/gitnexus/src/storage/parse-cache.ts @@ -55,7 +55,7 @@ import type { ParseWorkerResult } from '../core/ingestion/workers/parse-worker.j // the main thread (the #1983 OOM). Because the two stores share this version, // any future change to the `ParsedFile` serialization shape MUST bump // SCHEMA_BUMP so both invalidate in lockstep. -const SCHEMA_BUMP = 13; // Durable ParsedFile chunk directories now replace one complete generation instead of accumulating worker shards across cache-miss analyses. Invalidate once so existing unbounded stores are rebuilt under the bounded contract. (12 = #2391 follow-up: Python module constant extraction semantics changed.) +const SCHEMA_BUMP = 17; // Callable-value-flow operands now retain expression kind/qualified identity and formal signature metadata. (16 = direct callee identity; 15 = always-on callableFlowSites; 14 = #2437 value refs.) const GITNEXUS_PKG_VERSION = (() => { try { // package.json sits at gitnexus/package.json — two levels up from diff --git a/gitnexus/src/storage/parsedfile-store.ts b/gitnexus/src/storage/parsedfile-store.ts index 302202c5d..608e1afdc 100644 --- a/gitnexus/src/storage/parsedfile-store.ts +++ b/gitnexus/src/storage/parsedfile-store.ts @@ -52,12 +52,17 @@ import { promises as fs, mkdirSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import v8 from 'node:v8'; import vm from 'node:vm'; -import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared'; +import type { CallableFlowSite, ParsedFile, SymbolDefinition } from 'gitnexus-shared'; +import { logger } from '../core/logger.js'; import { mapReplacer, mapReviver } from './parse-cache.js'; const STORE_DIRNAME = 'parsedfile-store'; const DURABLE_DIRNAME = 'parsedfile-cache'; const DURABLE_INDEX_FILENAME = 'index.json'; +const MAX_CALLABLE_FLOW_SITES_PER_FILE = 100_000; +const MAX_CALLABLE_FLOW_INDIRECTION = 16; +const MAX_CALLABLE_FLOW_NAME_LENGTH = 4_096; +const MAX_CALLABLE_FLOW_PARAMETERS = 1_024; /** * Build a JSON.parse reviver that (a) interns every string against a shared @@ -240,6 +245,8 @@ export const loadParsedFilesForPaths = async ( // (one `int` / one repeated filePath for the whole language), which is where // most of the saving comes from. Dropped when this function returns. const pool = new Map(); + let droppedSites = 0; + let filesWithDroppedSites = 0; for (let i = 0; i < shards.length; i++) { // Per-shard def pool: a SymbolDefinition's three serialized copies live within // a single shard (one ParsedFile), so the dedup is shard-local. A cross-shard @@ -256,8 +263,15 @@ export const loadParsedFilesForPaths = async ( } if (!Array.isArray(parsed)) continue; for (const pf of parsed) { - if (pf && typeof pf.filePath === 'string' && wantPaths.has(pf.filePath)) { + if (!pf || typeof pf.filePath !== 'string' || !wantPaths.has(pf.filePath)) continue; + const flow = sanitizeCallableFlowSites(pf.callableFlowSites); + if (flow === undefined) continue; // non-array garbage → distrust the file, re-extract + if (flow.dropped === 0) { out.set(pf.filePath, pf); + } else { + droppedSites += flow.dropped; + filesWithDroppedSites++; + out.set(pf.filePath, { ...pf, callableFlowSites: flow.sites }); } } // Every few shards, reclaim the transient pre-intern parse churn before it @@ -268,9 +282,174 @@ export const loadParsedFilesForPaths = async ( await new Promise((resolve) => setImmediate(resolve)); } } + if (droppedSites > 0) { + // Facts for the dropped sites are omitted this run (the file itself is + // retained, so no re-extract happens) — surface it so a recurring drop + // on every warm load is observable rather than silent (#2522 review). + logger.warn( + { droppedSites, files: filesWithDroppedSites }, + 'parsedfile-store: dropped malformed/over-bound callable-flow sites at load; files retained without those facts', + ); + } return out; }; +/** + * Treat the durable ParsedFile store as an untrusted serialization boundary. + * Sanitation is per-SITE, not per-file: one malformed or over-bound fact drops + * only itself (counted, logged by the caller), so a legitimately pathological + * source file cannot push its whole ParsedFile into a permanent, silent + * warm-cache-miss reparse loop (#2522 review). Only a non-array field — + * i.e. garbage that says the serialization itself is untrustworthy — rejects + * the file, and `undefined` (never emitted / no facts) passes through. + * Returns `undefined` for the reject-file case. + */ +function sanitizeCallableFlowSites( + value: unknown, +): { sites: readonly CallableFlowSite[] | undefined; dropped: number } | undefined { + if (value === undefined) return { sites: undefined, dropped: 0 }; + if (!Array.isArray(value)) return undefined; + const bounded = + value.length > MAX_CALLABLE_FLOW_SITES_PER_FILE + ? value.slice(0, MAX_CALLABLE_FLOW_SITES_PER_FILE) + : value; + const sites = bounded.filter(isValidCallableFlowSite); + return { sites, dropped: value.length - sites.length }; +} + +function isValidCallableFlowSite(value: unknown): value is CallableFlowSite { + if (!isRecord(value) || typeof value.kind !== 'string') return false; + switch (value.kind) { + case 'seed': + return ( + isValidOperand(value.destination) && + isBoundedString(value.targetName) && + isOptionalBoundedString(value.targetQualifiedName) && + isValidRange(value.targetRange) && + isValidExpectedSignature(value.expectedSignature) + ); + case 'copy': + case 'alias': + case 'address': + return isValidOperand(value.source) && isValidOperand(value.destination); + case 'store': + return isValidOperand(value.source) && isValidOperand(value.pointer); + case 'load': + return isValidOperand(value.pointer) && isValidOperand(value.destination); + case 'formal': + return ( + isBoundedString(value.ownerName) && + isValidRange(value.ownerRange) && + isSafeIndex(value.parameterIndex) && + isValidOperand(value.binding) && + (value.passingMode === 'value' || + value.passingMode === 'reference' || + value.passingMode === 'pointer') && + isValidExpectedSignature(value.expectedSignature) + ); + case 'argument': + return ( + isValidRange(value.callSite) && + isSafeIndex(value.parameterIndex) && + isValidOperand(value.source) && + isOptionalBoundedString(value.directCalleeName) + ); + case 'invoke': + return ( + isValidRange(value.callSite) && + isBoundedString(value.inScope) && + isValidOperand(value.callee) && + (value.receiver === undefined || isValidOperand(value.receiver)) && + (value.invocationKind === 'indirect' || + value.invocationKind === 'member-pointer' || + value.invocationKind === 'callable-object') && + (value.arity === undefined || isSafeIndex(value.arity)) + ); + default: + return false; + } +} + +function isValidOperand(value: unknown): boolean { + if (!isRecord(value)) return false; + return ( + isBoundedString(value.name) && + isBoundedString(value.inScope) && + isValidRange(value.atRange) && + Number.isInteger(value.indirection) && + (value.indirection as number) >= 0 && + (value.indirection as number) <= MAX_CALLABLE_FLOW_INDIRECTION && + typeof value.addressOf === 'boolean' && + (value.expressionKind === undefined || + value.expressionKind === 'binding' || + value.expressionKind === 'callable-designator' || + value.expressionKind === 'bound-member' || + value.expressionKind === 'anonymous-callable') && + isOptionalBoundedString(value.qualifiedName) + ); +} + +function isValidExpectedSignature(value: unknown): boolean { + if (value === undefined) return true; + if (!isRecord(value)) return false; + return ( + (value.parameterCount === undefined || isSafeIndex(value.parameterCount)) && + isValidBoundedStringArray(value.parameterTypes) && + (value.parameterTypeClasses === undefined || + (Array.isArray(value.parameterTypeClasses) && + value.parameterTypeClasses.length <= MAX_CALLABLE_FLOW_PARAMETERS && + value.parameterTypeClasses.every(isRecord))) && + (value.isConst === undefined || typeof value.isConst === 'boolean') + ); +} + +function isValidBoundedStringArray(value: unknown): boolean { + return ( + value === undefined || + (Array.isArray(value) && + value.length <= MAX_CALLABLE_FLOW_PARAMETERS && + // '' is a legitimate entry meaning "unknown type" — the same convention + // as ReferenceSite.argumentTypes. C++ emits it for cv-qualifier-only or + // ERROR-recovered parameter types, so requiring non-empty here rejected + // real extractor output (#2522 review). + value.every( + (entry) => typeof entry === 'string' && entry.length <= MAX_CALLABLE_FLOW_NAME_LENGTH, + )) + ); +} + +function isValidRange(value: unknown): boolean { + if (!isRecord(value)) return false; + return ( + isNonNegativeInteger(value.startLine) && + isNonNegativeInteger(value.startCol) && + isNonNegativeInteger(value.endLine) && + isNonNegativeInteger(value.endCol) + ); +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function isBoundedString(value: unknown): value is string { + return ( + typeof value === 'string' && value.length > 0 && value.length <= MAX_CALLABLE_FLOW_NAME_LENGTH + ); +} + +function isOptionalBoundedString(value: unknown): boolean { + return value === undefined || isBoundedString(value); +} + +function isNonNegativeInteger(value: unknown): boolean { + return Number.isSafeInteger(value) && (value as number) >= 0; +} + +function isSafeIndex(value: unknown): boolean { + return isNonNegativeInteger(value) && (value as number) <= MAX_CALLABLE_FLOW_PARAMETERS; +} + // ─── Durable, content-addressed sibling store (warm-cache coverage) ────────── // // Layout: `//-w-.json` plus a diff --git a/gitnexus/src/storage/repo-manager.ts b/gitnexus/src/storage/repo-manager.ts index 9b3751898..f9c12f49b 100644 --- a/gitnexus/src/storage/repo-manager.ts +++ b/gitnexus/src/storage/repo-manager.ts @@ -355,8 +355,13 @@ export interface RepoMeta { * writeback preserves unchanged-file rows, so a top-up against a pre-v6 index * would MIX old 1-based rows with new 0-based ones — and the 1-based MCP display * would render the stale rows one line too high — so force a full re-analyze. + * v7: callable-value-flow CALLS/USES edges added (#2437/#2522) — new edges can + * connect two files whose content did not change, but the incremental write set + * only covers changed files (`computeEffectiveWriteSet`), so a top-up against a + * pre-v7 index would silently omit the new edges for every unchanged file pair; + * force a full re-analyze instead (same contract as v2–v6). */ -export const INCREMENTAL_SCHEMA_VERSION = 6; +export const INCREMENTAL_SCHEMA_VERSION = 7; export interface IndexedRepo { repoPath: string; diff --git a/gitnexus/test/fixtures/csharp-captures-golden/expected-captures.json b/gitnexus/test/fixtures/csharp-captures-golden/expected-captures.json index 0f6fd1f07..dc05e7cdf 100644 --- a/gitnexus/test/fixtures/csharp-captures-golden/expected-captures.json +++ b/gitnexus/test/fixtures/csharp-captures-golden/expected-captures.json @@ -1,11 +1,11 @@ { "csharp-alias-imports/Models/Repo.cs": { - "captureGroups": 13, - "digest": "89499f7c26b495994d869b802deaeecc1de68c7d1e1b41629b57f9baaff4c240" + "captureGroups": 15, + "digest": "044a25113f7b90a72ad0e42cd7f747848222a21c4093775f08c133188dc25687" }, "csharp-alias-imports/Models/User.cs": { - "captureGroups": 13, - "digest": "d6f73d9a16c5d07f7b7c3c85fe2464426ad6fec89b05312ebccdf3391b6e52e5" + "captureGroups": 15, + "digest": "a3fe19d6bf25831c2e1afc836e0ed82cc903a43e006d7428a2386b0bc7ea0c47" }, "csharp-alias-imports/Services/Main.cs": { "captureGroups": 18, @@ -40,40 +40,40 @@ "digest": "064640e172244bee5078073d4eed69df177681cba8c0a8105a391a65864612f9" }, "csharp-assignment-chain/Program.cs": { - "captureGroups": 30, - "digest": "a3eb329f0ca262da99fa1000b49f7b10e7e5c3fbc157f0d95825e866cdd4c190" + "captureGroups": 32, + "digest": "7698bdabe97661a2a2539a13cf9d886cd7efb2f4924b17a15597ce63cec5126a" }, "csharp-async-binding/Order.cs": { "captureGroups": 9, "digest": "c2d5ef9688ff2fce345fe8d8148ee0d8281032b55083f70ac925e5407b3c0d0c" }, "csharp-async-binding/OrderService.cs": { - "captureGroups": 12, - "digest": "9ea84a606a4bbee462b5cbc8becff91aa07847187b40f1af6d7c429c2876df4a" + "captureGroups": 14, + "digest": "712fb5f3a791581ab56c37df58d8245a17a674a6d2f7bd25b2bc8d1632f751c3" }, "csharp-async-binding/Program.cs": { - "captureGroups": 32, - "digest": "a90d5d0f8b41f8752b5b629aa4d1bf58b8dcda6181c41fbe9453eb758c4bbfa3" + "captureGroups": 37, + "digest": "73735c3910ed4db423302d9575cec86156d420e9961c592c34e0436301cac7ce" }, "csharp-async-binding/User.cs": { "captureGroups": 9, "digest": "d771395595ea17b22e3f7b448bb10463086aa89fe49f54fe24ee514e2392a880" }, "csharp-async-binding/UserService.cs": { - "captureGroups": 12, - "digest": "08c1546fd0a591d45041f4840725aa16cb832d6cd848563db45a83868352afa7" + "captureGroups": 14, + "digest": "a4e6b093fa23a86313bc468f8b6a89a96d5c90b1d16f166417745bd36dfbd10f" }, "csharp-call-result-binding/App.cs": { - "captureGroups": 24, - "digest": "da0e6286460e3eb719452574c2c627f3d242394b165525e026926c08b26cb88c" + "captureGroups": 27, + "digest": "3d8c7dc0b7f5bd60c74d6a595bd4b49bdb13b62522fb7f0c1434495e10e1171b" }, "csharp-calls/Services/UserService.cs": { "captureGroups": 11, "digest": "d527d47a183733c34d56227c383267f25acc951413639f1e6cc6cb7eabd2440a" }, "csharp-calls/Utils/OneArg.cs": { - "captureGroups": 7, - "digest": "8d9ac2bdedab0c1202d99233bbf03364d17e67fbfe9ecdd0ecb924198e1cd393" + "captureGroups": 8, + "digest": "65bf57fabe6074033a64e5e5adca6aa433e5087576eac96b13172934f55f0c82" }, "csharp-calls/Utils/ZeroArg.cs": { "captureGroups": 7, @@ -124,12 +124,12 @@ "digest": "37c48de332491deb8c31c5f1f8184e301900f87258eac47321ebf12065991cad" }, "csharp-deep-field-chain/Service.cs": { - "captureGroups": 13, - "digest": "a10037598e4ee49a9e2ce2c4bf397bee1ca44faec8e7abda852efe665f7dfbfa" + "captureGroups": 14, + "digest": "f988dfa882e07384c055a431d970550281aa1fa5d14de0daf7ecdde3a1b9e6f2" }, "csharp-dictionary-keys-values/App.cs": { - "captureGroups": 19, - "digest": "b445e081f6556e4f84e45f0ad3b4d6f8a4b64ff24a6e53a7ac2109d80fc3225a" + "captureGroups": 21, + "digest": "ee8eb9c569b71d7050f292bc7fbf89b68cdbb60b4bcd557f2523c86831891543" }, "csharp-dictionary-keys-values/Repo.cs": { "captureGroups": 7, @@ -140,20 +140,20 @@ "digest": "49ec4930562a8e12040010b12535a1620aa5ab86e2aab3cad2623e8aec106518" }, "csharp-explicit-ctor-init/Models/Base.cs": { - "captureGroups": 8, - "digest": "a96546d27fbb8b2ddf8571f0253bb145565223e52f45d8ff0cf84c6ff5cb1ee1" + "captureGroups": 9, + "digest": "6adf4ac489b35e51161a4d67a0ab2fe3c7191e08a1c0c6bf051cb653a1cb4b31" }, "csharp-explicit-ctor-init/Models/Child.cs": { - "captureGroups": 16, - "digest": "fe50206ccb6df1b631bb5c22892e1e3f668181e9ba9f3b55981e9df97ff10ff2" + "captureGroups": 17, + "digest": "f056fbf3be0445392d0f67e35dff5e7690c6244d650322d20a2d35ca9f04c9aa" }, "csharp-field-types/Models.cs": { "captureGroups": 17, "digest": "2e698fecbe4099c8aa5c63ba5ec7adad0dbb64912163685507a96fb681545d03" }, "csharp-field-types/Service.cs": { - "captureGroups": 10, - "digest": "4cf80db658b9491052b4f629fce5f8397d1e8d537753ff3e729f2ab9c446f367" + "captureGroups": 11, + "digest": "6f6a983bab2d73fb0f83d879311a4822356b2aa6ee1029fc12d04b7a1c3acd58" }, "csharp-foreach/Models/Repo.cs": { "captureGroups": 8, @@ -164,8 +164,8 @@ "digest": "064640e172244bee5078073d4eed69df177681cba8c0a8105a391a65864612f9" }, "csharp-foreach/Program.cs": { - "captureGroups": 18, - "digest": "be6192b93b692af37eb8564714faa1486c9463c8fac4c41d4c17c47e3306474d" + "captureGroups": 20, + "digest": "810f0f65e956343cf6817918dc5b28ce7bc1f1f755f89e008a6e8b05864ff469" }, "csharp-frozen-binding-collision/App/Program.cs": { "captureGroups": 18, @@ -188,8 +188,8 @@ "digest": "2f193c298218e84b404be421c513c2f0ee2f21c05e266b0c523b293b17529aeb" }, "csharp-generic-type-refs/Program.cs": { - "captureGroups": 23, - "digest": "97ce65fba71e65ad7dfebb630d9f38b0ad598d45cd461b5e1275ec80168df27b" + "captureGroups": 25, + "digest": "11958e1426be1f2f06d82c93fd1f813607b7665072e95356a8584aa21c0c2119" }, "csharp-grandparent-resolution/Models/A.cs": { "captureGroups": 10, @@ -212,16 +212,16 @@ "digest": "ec4326a3db72676fb5e17b9fad3de503989a7f34b1b7159fe86093728fbf1737" }, "csharp-hello/Hello.cs": { - "captureGroups": 20, - "digest": "140b0d111483cecb5a875d42a081fb0e19e378cd98802890210c23773350e6dd" + "captureGroups": 22, + "digest": "0765643d95d2c40a64d3f9b71fd488fc6cbfdb0cc72cc91eec94e46da1267722" }, "csharp-interface-default-method/App.cs": { "captureGroups": 12, "digest": "1df65065f20b8d316238bd3f7c43f95cc0e2527c8f7cca88e95a872016a04ed2" }, "csharp-interface-default-method/User.cs": { - "captureGroups": 12, - "digest": "647035b25d003e3ddf4763e36311b638391b0147e762865618a83285fab50f36" + "captureGroups": 14, + "digest": "ef10faa8e723eab836331e5c2d29c4b167cfd2c1e15c391989ea394eb1cab5c4" }, "csharp-interface-default-method/Validator.cs": { "captureGroups": 8, @@ -232,12 +232,12 @@ "digest": "ca1d074837f867a78d9b5e1f197ab967128e89b20d62acdba34bfaa08b01ef6d" }, "csharp-interface-dispatch/IRepository.cs": { - "captureGroups": 7, - "digest": "14743965ad586b13254a9618686d601b0dd36f3be97531c9fb0605139e83809d" + "captureGroups": 9, + "digest": "88c0a5b4146a78b07cfe361bfb20ced73c05be707997fbfa4bcefb8dd60f8b50" }, "csharp-interface-dispatch/SqlRepository.cs": { - "captureGroups": 12, - "digest": "65bd6fd311969af60313ea33345188894fa236ca26020b3e681fc8c3144898fb" + "captureGroups": 14, + "digest": "4ec288844786e2b45a1cb7421474b3c1cc5606e42c34f2364fce642c60f1aae2" }, "csharp-interface-heritage/src/IAuditableService.cs": { "captureGroups": 8, @@ -264,8 +264,8 @@ "digest": "c5a6484f74d530463f3028036f401323d84ec2a825fd52450326d95c8088ccb2" }, "csharp-interface-receiver-static/src/ILogger.cs": { - "captureGroups": 7, - "digest": "8bfccfdf1696c38539df95c001029d4d40f09d2f84ed273e5e4ddf860dad7d10" + "captureGroups": 8, + "digest": "57032d77c03d756e71307aab5fc993754b4972598b6362689bf905786c4a32ac" }, "csharp-interface-receiver-static/src/Runner.cs": { "captureGroups": 9, @@ -280,8 +280,8 @@ "digest": "2c97632b3beb8f5500e4d02b1150d2845fa6d6bc26ee1838e4e1118ec6a8e847" }, "csharp-is-pattern/services/App.cs": { - "captureGroups": 11, - "digest": "e125e671dfcf30a41b26c29bfd57fb9fe9b8e0d1a0dabedbdd3ac1451693672e" + "captureGroups": 12, + "digest": "4ad57edbcf5462d7a254fe63f59e5e513ab4032c18d437cc1154cde5c857897d" }, "csharp-large-cache-miss-resolution/Models/User.cs": { "captureGroups": 8, @@ -296,12 +296,12 @@ "digest": "ac7dcf56199eb29174cfbfb775e956247f72abc476cc8e28751d94084d1b5c7a" }, "csharp-local-shadow/App/Main.cs": { - "captureGroups": 13, - "digest": "e1ba476d7984abedd654e461d9a971e8e566116e6414871f1962feef75b91d50" + "captureGroups": 14, + "digest": "12c7b2f27fc6e68ece2e588ab060d60d5ab0b2ac679bbccc236c170dacd5d130" }, "csharp-local-shadow/Utils/Logger.cs": { - "captureGroups": 9, - "digest": "324feff6c194e3f07493e63efe46165832033c855978358f0a0fd3cd6d6d732a" + "captureGroups": 10, + "digest": "0b65e06e2c32dabd8e3c5ede48d088323f145ad2f55760b79f4f4af59aa5f033" }, "csharp-member-calls/Models/User.cs": { "captureGroups": 8, @@ -312,12 +312,12 @@ "digest": "662512b485c31efe8b5a37df38e31bccf130e7625c7ef7594ea44e27b0b79d2c" }, "csharp-method-chain-binding/App.cs": { - "captureGroups": 53, - "digest": "09e0bed66a03cddb567b863b8d916b5b2f4210547caf2f8682c266d36102125e" + "captureGroups": 60, + "digest": "2b4761e1dfe2d48ac25cfda7ccce95175607926b47db43726f38ad2a16acc6f1" }, "csharp-method-enrichment/Animal.cs": { - "captureGroups": 17, - "digest": "b21133dbc6ecaaf9ba13de8d1b70c11c6a89d02f89696a137c8d79ae9e5e287a" + "captureGroups": 18, + "digest": "ad7890731caf219c98664630dfe886f00ed33a4a3f6a4a30f945c889835eae1e" }, "csharp-method-enrichment/App.cs": { "captureGroups": 14, @@ -332,8 +332,8 @@ "digest": "be74672b6af6192d0b7872dcad9a6ba4af08156aad5cc01c6fe3eb2ee02b4127" }, "csharp-mixed-decl-chain/Program.cs": { - "captureGroups": 25, - "digest": "4f25b7d6327c0f9d1f4ca9e8d1af07ced3b6fedbfbb8e4996c73cbfb963ef24e" + "captureGroups": 28, + "digest": "466f29bc93a0293011c8c43f4655d87a26a57166261aaf85df3d08c2d38cf10b" }, "csharp-namespace-as-root-no-trailing-newline/App/Program.cs": { "captureGroups": 13, @@ -384,8 +384,8 @@ "digest": "d019925a0e3ab366b3fb8fbd1abac38f564b9f4877ea0dd0b802c181c07c8587" }, "csharp-null-check-narrowing/Services/App.cs": { - "captureGroups": 30, - "digest": "5c41de929accffe5f747a6891ca323081e529e877171d4545bd7e47d6400a8cd" + "captureGroups": 36, + "digest": "5d840c524610b6a84a2f09981c15327e9f7ea5c2c4b11b09e959d880ac6b9bc9" }, "csharp-null-conditional/App.cs": { "captureGroups": 17, @@ -400,28 +400,28 @@ "digest": "064640e172244bee5078073d4eed69df177681cba8c0a8105a391a65864612f9" }, "csharp-optional-params/Services/App.cs": { - "captureGroups": 15, - "digest": "d146e09259dd67a7bc33d1117d4df7a65e760645d024539fcee787db67ed0fdf" + "captureGroups": 17, + "digest": "5fb11a605cac8fe2abf017e1724bf8881eacbc147f9a29052e432bf3ca26b284" }, "csharp-overload-dispatch/App.cs": { "captureGroups": 12, "digest": "c0ac495ae6dc34db5cdb9f508be050fab06920e8f110b14a8c9f095613a269c5" }, "csharp-overload-dispatch/IRepository.cs": { - "captureGroups": 9, - "digest": "5211301516540daf9c54fb3c8ca3b43bddcce4f81a14034056f2bc897ae925a4" + "captureGroups": 13, + "digest": "9f7fc8cfd4c0e3b67581f96945760ffe96183f55ce93809d1fc9e0abb9d42b67" }, "csharp-overload-dispatch/SqlRepository.cs": { - "captureGroups": 17, - "digest": "8b67299c96e0331a44bfd56edf13bc71f957e39bfa95888108d3180f454050a3" + "captureGroups": 22, + "digest": "d4d7156fc95fa515126011bffed810ecdcf94b2f4ea6e537b1d8dfa5d8d7abe9" }, "csharp-overload-interface/App/Caller.cs": { - "captureGroups": 14, - "digest": "8b37ca49176cdc59f0ad0033b75b1936c80eb1c74db4475876923c098a52b496" + "captureGroups": 15, + "digest": "f1ea2e564c46dab5fb93fb19e81ab3411f458b172820b5dc0bdc57f49ffa88e0" }, "csharp-overload-interface/App/Logger.cs": { - "captureGroups": 11, - "digest": "6e363259f8427756f497269ca0a3f4838ce16ad7518be6fe76289ee766cbc8e0" + "captureGroups": 14, + "digest": "71eb7a633b950c765dcfb50a28047b4156a121c653ee189a9288c1998c3c71a7" }, "csharp-overload-interface/Greeting/EnGreeter.cs": { "captureGroups": 10, @@ -436,8 +436,8 @@ "digest": "4a42025fdcb4db0bf1b77e1e4fa024af5c2a4dc36fe6806ce397b980047790d0" }, "csharp-overload-param-types/Models/UserService.cs": { - "captureGroups": 28, - "digest": "b934c80be640f7d5707d5f03b352c059ed225272d494f93337805ad9b6ece895" + "captureGroups": 30, + "digest": "178e1a7dd5b07ba3361e1eb28ce73ca6a6075fa8ecb8f2ca40e8e87a410de552" }, "csharp-parent-resolution/src/Models/BaseModel.cs": { "captureGroups": 8, @@ -456,8 +456,8 @@ "digest": "b724c5e2a0742901a09a8c85e0fd5504905471bd8096e6541b05c2d2a1d65527" }, "csharp-pattern-matching/Services/AnimalService.cs": { - "captureGroups": 12, - "digest": "209cee0681fa4f035a081f44e38f9cd39eda3d56936471396f78f5265d745e4b" + "captureGroups": 13, + "digest": "2623dcd94520473dc3dd830cfc21675349b6981185b779db3f5a47200b2f44a3" }, "csharp-primary-ctor-heritage/src/BaseEntity.cs": { "captureGroups": 6, @@ -492,8 +492,8 @@ "digest": "01bb40f83fca48db34d1c1c970317cf0fece5af7eb7e02836ef9e64304154b3a" }, "csharp-proj/Interfaces/IRepository.cs": { - "captureGroups": 13, - "digest": "12e7cea9ae7c97e97789f2ef83e689530002c084b06a904a17994ce43f8b6af4" + "captureGroups": 14, + "digest": "902f871892b4b772b60d7e852d1a4a85b3ad4b12358d73a0a946e17c8943942c" }, "csharp-proj/Models/BaseEntity.cs": { "captureGroups": 9, @@ -504,8 +504,8 @@ "digest": "6c1b2e9adee1a7cf34ecd647be966c28aab7a8ac57c0fcb14373f2e562f3f456" }, "csharp-proj/Services/UserService.cs": { - "captureGroups": 20, - "digest": "26ff7130ea2290fefd0160458c2b1fb7cd8572643f04545a634696a1f1773e5f" + "captureGroups": 21, + "digest": "8004a914844e0a5ff0630b019889cca8db09612678683ade419d4c40a9f60e15" }, "csharp-qualified-base/src/Domain.cs": { "captureGroups": 16, @@ -564,36 +564,36 @@ "digest": "80b9061d53bd0ceba23699c828100ebce5af9a7c006fff7e4a99d2f8e006b92e" }, "csharp-recursive-pattern/Program.cs": { - "captureGroups": 14, - "digest": "c2fe0ca3b5bf9b26774ee896b7f2fbae1cdaaf3c27d1469897cde3547c297bae" + "captureGroups": 15, + "digest": "ef252310d50287971d87e96e72b64575b9ee691602b62c74ea5b23811d2080a6" }, "csharp-return-type/Models/Repo.cs": { "captureGroups": 8, "digest": "4953327fdde44fada4df0c55bd1dab9c2e96b28f42a1fbfb224c374f75ce0fbb" }, "csharp-return-type/Models/User.cs": { - "captureGroups": 20, - "digest": "7a41c40abf08b3fbc68b34e581636b51dcfb32c9e49dd195e5dfe29680d3cf2e" + "captureGroups": 23, + "digest": "6681e6830c71c25908e50273e4babb41611bc229395d1dbab70ac9f219b68ca8" }, "csharp-return-type/Services/App.cs": { "captureGroups": 16, "digest": "6c22fca1da52147fdbfbe48f1563e95f291ef0af8856b5c6e82ed04a110d064e" }, "csharp-same-arity-cross-file/App.cs": { - "captureGroups": 47, - "digest": "3b66f87310d7dae98a79a6983996abca827e8b78d37e5a7234e26e9520634c5b" + "captureGroups": 49, + "digest": "899ca0e32e844b1d347eb0e7558da99644892dc894fcf7e3f632071bbd9cfcd7" }, "csharp-same-arity-cross-file/DbLookup.cs": { - "captureGroups": 12, - "digest": "870869bc7dec677c4f037cd1ef6da71ff6a180b40b76a313a4e6f8e6a2030ffa" + "captureGroups": 14, + "digest": "a33d9e4c3e1f7bc4f54e3cadbbf5cb044669ba5cc5912b19836cf58d8b9d5910" }, "csharp-same-arity-cross-file/Formatter.cs": { - "captureGroups": 11, - "digest": "83c7bbf6774596aa06aa75480a136f442356bf44bf2a048e9f9a561be613d781" + "captureGroups": 13, + "digest": "e2305d9beeb8c7d4452bacfa7cc078dba02a17f34a993d2986cd55f03bdcfc41" }, "csharp-same-arity-cross-file/ILookup.cs": { - "captureGroups": 7, - "digest": "ee3c7f29ba0638d2917d364fa39cd80220655b4ee0825eda1d2640e7d9e8c500" + "captureGroups": 9, + "digest": "f700d06ecdcb6d140c3c9514f85d30d04cfd19d131e570213ed166eaba691ec7" }, "csharp-self-this-resolution/src/Models/Repo.cs": { "captureGroups": 8, @@ -628,8 +628,8 @@ "digest": "2574c61dc312d531301a6d08c828ac743b1198e32946980c0f44bc115ec9bcdc" }, "csharp-struct-overloads/src/Calc.cs": { - "captureGroups": 16, - "digest": "03b9f3273cdd46364956b6cf15d29a849c6be70567d7d26ffd23023f60a24591" + "captureGroups": 19, + "digest": "b45f1440bd589f070e2adfa99872d93f357a8c3831daecb1b9da31476115914e" }, "csharp-super-resolution/src/Models/BaseModel.cs": { "captureGroups": 8, @@ -652,16 +652,16 @@ "digest": "c51e7c7e0423087ddd3a439b99c95101a3bb8611bb6677d7bd1630a464a8cacb" }, "csharp-switch-pattern/Program.cs": { - "captureGroups": 13, - "digest": "bee49e211ae6d5a6391a56a68044762500d600e2a8a5b9d78c1350e342dcab19" + "captureGroups": 14, + "digest": "a090eff3ff572948716c931da3d62eeb137e36f08397b60748d4177293b10dc1" }, "csharp-using-static/App/Calculator.cs": { - "captureGroups": 10, - "digest": "9f1d4ed78f5653ce5da1604afb322ee10ad212fbc4f791612a77a52530a8fa07" + "captureGroups": 12, + "digest": "b6f9dd906e1309338f21633d71e663cdfd95a8707d38b9a8bf74813415ee5d13" }, "csharp-using-static/Helpers/MathUtils.cs": { - "captureGroups": 7, - "digest": "f6db029267c1562d1b5defe08089f66f9bd183509b0a7a9df4f638d9170b36f1" + "captureGroups": 8, + "digest": "32c174cbaade4e2d6e0aa7e95a2b5addd138441deb43ced286c9cf5cd30750aa" }, "csharp-var-foreach/Models/Repo.cs": { "captureGroups": 8, @@ -672,27 +672,27 @@ "digest": "c51e7c7e0423087ddd3a439b99c95101a3bb8611bb6677d7bd1630a464a8cacb" }, "csharp-var-foreach/Program.cs": { - "captureGroups": 28, - "digest": "f70572e67859f3e853b84a123d70836324509abb1e60e4e4179b354e3bb52988" + "captureGroups": 32, + "digest": "a9c7bf1f2425cece1ecb698abc0c6fa5e7a3bb24e3cc7d2dba50ef7d0651badc" }, "csharp-variadic-resolution/Services/App.cs": { "captureGroups": 10, "digest": "892a1570c699c625fa1be6d4c53216589622a1b7df5e297a625edffd2fc31e14" }, "csharp-variadic-resolution/Utils/Logger.cs": { - "captureGroups": 8, - "digest": "835f4084b55107b725ac675350df638be92436685d39eac1518b234fd9f618e3" + "captureGroups": 10, + "digest": "46905fc6f0d59035df05fe60d29c57217d321c5a8d3c3223466061b56c7f785d" }, "csharp-write-access/Models.cs": { "captureGroups": 9, "digest": "0d65b1d41a42a5a79eecf3a2ff2c7b3585c52dadd479770c2572a146afdc6606" }, "csharp-write-access/Service.cs": { - "captureGroups": 10, - "digest": "1782eca84697c9ebd7fe802d1e0e0952d29cef8d8834e826b359388561a41c06" + "captureGroups": 11, + "digest": "aa6d8a61ac39db413df10a6bc8b9bad3305327dcdce09e01cf01eff31f945537" }, "synthetic:dao-20": { - "captureGroups": 223, - "digest": "05b2727c2e20430921fe536ad4d421764c9791e5f370fdce97c3fb0d07ada703" + "captureGroups": 263, + "digest": "f082745a8fcb7a0acb364f3e3739b70f96fd2cf821ada7c4e743090348a06dcc" } } diff --git a/gitnexus/test/fixtures/go-captures-golden/expected-captures.json b/gitnexus/test/fixtures/go-captures-golden/expected-captures.json index cb65455eb..3e928e0ad 100644 --- a/gitnexus/test/fixtures/go-captures-golden/expected-captures.json +++ b/gitnexus/test/fixtures/go-captures-golden/expected-captures.json @@ -20,8 +20,8 @@ "digest": "0a988c48838a34eb7a6f29b324191c39c19087e728914fffa06bbb9221841d23" }, "go-assignment-chain/cmd/main.go": { - "captureGroups": 50, - "digest": "570d1eb9d287345a00a5a05bde00724b609523b41953cb1c277f5c056980d7b5" + "captureGroups": 54, + "digest": "2bd7f8e2df13345ed77b4b438fa5af0b666aa238fe32be0d9ef39865b04a27e2" }, "go-assignment-chain/models/repo.go": { "captureGroups": 8, @@ -32,8 +32,8 @@ "digest": "fcee44ed373eda2ff00778ff951fc2fa1503c5187b9e7d43dea3ee8b63656907" }, "go-call-result-binding/cmd/main.go": { - "captureGroups": 16, - "digest": "ecf35e5a0e148d55f98a0de636f743db026fb75ae6a609b3fa90bd285c322145" + "captureGroups": 17, + "digest": "fadab2167d8037193746a4bd13b50c63a7053c18a4810b75da16e931cfcdbb8a" }, "go-call-result-binding/models/user.go": { "captureGroups": 10, @@ -44,8 +44,8 @@ "digest": "deaea55087c2652aa2d39fefa30048d5393cec40f7af1ab13921331b035030d6" }, "go-calls/internal/onearg/log.go": { - "captureGroups": 6, - "digest": "762e8143c4ab116272c38d46586106ec4d5e01153c7abe831ca434269da58d94" + "captureGroups": 7, + "digest": "d6a41da15dcba594b747b76b726738bcfbb86b9fb9d444571f6749c361bd4d59" }, "go-calls/internal/zeroarg/log.go": { "captureGroups": 5, @@ -84,8 +84,8 @@ "digest": "a1f9453bd71926d60e3f148f43b9af813cbd1cccc11b323896a55bdb443f8931" }, "go-constructor-type-inference/cmd/main.go": { - "captureGroups": 18, - "digest": "849606bb6f94982923de95a9cacc2eeae567b1eb473937cc635dcc7789cc1c01" + "captureGroups": 19, + "digest": "3cfb964e7c0fa6d5b702a1312e4cd7dc5c6153d45e29f8d422afebcf131930f4" }, "go-constructor-type-inference/models/repo.go": { "captureGroups": 8, @@ -96,16 +96,16 @@ "digest": "bf814d4db5d2b0026caab6aa5ade9c173abd96fdf2c1e127b57a854d0203f9a5" }, "go-deep-field-chain/cmd/main.go": { - "captureGroups": 13, - "digest": "ca9cc7ae0f75928b1ea338f42e58cf02502e0c93ce4ea8867258c90543f79d16" + "captureGroups": 14, + "digest": "2a2d67f1fd9813558ce13d969fd9f6a50368f303c7e925b4cf8df9b52e66d54f" }, "go-deep-field-chain/models/models.go": { "captureGroups": 33, "digest": "3941943919b0066138d8a759cceadd95784c8a89978e7a345ea056ef1ca78c9c" }, "go-field-types/cmd/main.go": { - "captureGroups": 9, - "digest": "3bc98cf5640568cdff3202f2594fb39d563ee595560cb28d1adb31f264670ceb" + "captureGroups": 10, + "digest": "358f4003af7b957c35bb049af4941f8a23c36981b7e777bf1b943bcf734c7d1b" }, "go-field-types/models/models.go": { "captureGroups": 22, @@ -124,16 +124,16 @@ "digest": "964cf1bd181cc7196726c3261a683884ac9c6fc3aee6170b47bd54427a6cb947" }, "go-inc-dec-write-access/main.go": { - "captureGroups": 21, - "digest": "0414398239624e44b1589f6a68f2c19636bf4b3ce74990cee1bd7cbeeb0591e6" + "captureGroups": 23, + "digest": "fd35d917bc7a039ef4bcb0a6501c31ffbea0fe39bf3604938191597415aa33f7" }, "go-local-shadow/cmd/main.go": { - "captureGroups": 12, - "digest": "1cda9982d9b4e4878208894523f0a9c7a806c6f08a5582d4771205716a1b2733" + "captureGroups": 14, + "digest": "ce7220df98df08741f772150b5572bec0364d576741bc8c76647615fab859f92" }, "go-local-shadow/internal/utils/utils.go": { - "captureGroups": 6, - "digest": "51e941fa4c7765efc6e472d15a9d7ea31f59b67a6b606be868f4af47e5f54cb1" + "captureGroups": 7, + "digest": "9e4c8b37e90b1abeb8a0f8ca615793fc4a77bea5178ebc4875a3a595ec59f03d" }, "go-make-builtin/main.go": { "captureGroups": 18, @@ -144,8 +144,8 @@ "digest": "2af382fcd2e59cf01d9439bdb8709cba150bb239fae71b6f0d09c1fa7248fc5b" }, "go-map-range/main.go": { - "captureGroups": 10, - "digest": "7f3580a0e7858e6eb3176e0b5e1bec4867a2d5d07f2b216a04a3de79ff96170b" + "captureGroups": 11, + "digest": "5e8b9ed5580cd51f988049c4a43e212bf9d5b655f5de85c80a956baf5892a6a0" }, "go-map-range/models/repo.go": { "captureGroups": 9, @@ -164,40 +164,40 @@ "digest": "fcee44ed373eda2ff00778ff951fc2fa1503c5187b9e7d43dea3ee8b63656907" }, "go-method-chain-binding/cmd/main.go": { - "captureGroups": 21, - "digest": "884589d31ad9c4463d48323b7aa5f80c15a4d3eb1097210abd88c3d013d2fbef" + "captureGroups": 22, + "digest": "ede80f2009f68d880d957bc90af74898dc0b3f7d86a48bf26e4d9d3587dec2d4" }, "go-method-chain-binding/models/user.go": { "captureGroups": 24, "digest": "d72468ce0567ce35393b570e7e3e86f0c004e37a9614dc5ee698aff6d3a4348a" }, "go-method-enrichment/animal.go": { - "captureGroups": 16, - "digest": "7ef52b78d7c920a34961f3275ceb4a34d0172067459ab594287b7e4b7ec695a6" + "captureGroups": 17, + "digest": "090d1bba93a5271a8e8e75d2e9d9790e58e4c5a6679b7fb9290f318591eb619b" }, "go-method-enrichment/app.go": { "captureGroups": 15, "digest": "ac0bdc2e6daf7d4e28fd255fe2edd143e8fab7ff316f4e32e2ca04a3b33f71c2" }, "go-mixed-chain/cmd/main.go": { - "captureGroups": 20, - "digest": "3662b803da4f4fc1ac0552a45d3d262bd0db57f97b056614caacea8ad856250e" + "captureGroups": 22, + "digest": "486c3edfe09e6bc6ba3652f55e167c1e73cdac3b5419ef513811a1e6bfcdb9d5" }, "go-mixed-chain/models/models.go": { "captureGroups": 41, "digest": "afb1eaf2869f534033c5cd844ca111a93b63df1c098cec9c9518c4480a47ddb0" }, "go-multi-assign/app.go": { - "captureGroups": 16, - "digest": "66958a3e86caa54aedd795227dac274cfb3f4b39ab98964e8a7bcbdfc8a08ca6" + "captureGroups": 18, + "digest": "6ca3fdfb8708da1503367902ea458927bd2eb33582eeccbbb0a1dab585cc30c1" }, "go-multi-assign/models.go": { "captureGroups": 19, "digest": "e071ebdd59b5bbebb42785ebf850a9c0ee7c2e3a8cc13fb0b17233480efa26ed" }, "go-multi-return-inference/cmd/main.go": { - "captureGroups": 36, - "digest": "179fca92694ce3e1f2a9b38cde0c283e4bab00f240316a6116931115e079057a" + "captureGroups": 38, + "digest": "9f7320349e8fd04f40c23a9a04ab67826140034e98d9851538db1b8a6f9f2b7b" }, "go-multi-return-inference/models/repo.go": { "captureGroups": 10, @@ -236,24 +236,24 @@ "digest": "e1b57b0c5865d84b80518fe52e3807f813ae5589b4b558d0037e6ea136843f85" }, "go-pkg/cmd/main.go": { - "captureGroups": 14, - "digest": "a7781d23802e876b9ca2951b7ebb317c5df5adf390ae9a57e8e2906af34f3255" + "captureGroups": 15, + "digest": "b763b372c88c64d31b4a746d24a422b490dfad0f33a55ac4e0f697979995b34f" }, "go-pkg/internal/auth/service.go": { - "captureGroups": 17, - "digest": "dc481fe7319f4e69d6f53d04a10029bb36b5f964b5ecd5a367d6e081c0158fec" + "captureGroups": 21, + "digest": "8a5a2fef24f2803396b2314ffc951487b1adbc2a28a733bd2dcb65e2961fc479" }, "go-pkg/internal/models/admin.go": { - "captureGroups": 14, - "digest": "6c307b22f7ddd3c2c34a525a61e4ed36602e22b5845fc6533be15515ef7d78fb" + "captureGroups": 17, + "digest": "087b8188a2e3d5d58f70d5a812b1a31a135dbd435de0ee7799d63199a38eb6cf" }, "go-pkg/internal/models/repository.go": { "captureGroups": 5, "digest": "73a5c9bfcb59011f36c47e1b806edba89bad1ed32f81d46acbefdb8abdba67c7" }, "go-pkg/internal/models/user.go": { - "captureGroups": 13, - "digest": "a9eeed64d83ab471847db27f8ce5c4ba65b6ac2664f372da67ee4fc974016224" + "captureGroups": 14, + "digest": "04a3005d9f97325b79c0ac524a424c84c004aed5af24318389a413462dee2c4f" }, "go-pointer-constructor-inference/cmd/main.go": { "captureGroups": 15, @@ -300,16 +300,16 @@ "digest": "fcee44ed373eda2ff00778ff951fc2fa1503c5187b9e7d43dea3ee8b63656907" }, "go-return-type-inference/cmd/main.go": { - "captureGroups": 39, - "digest": "71b935bc70b194c2a8136f053319aaba86a4ba80a5d171ef39bc6e8d5c144865" + "captureGroups": 41, + "digest": "6133d61b247732eddad431c28c1374e46d85937ab42e88d7b1ab134aca1e593b" }, "go-return-type-inference/models/repo.go": { - "captureGroups": 16, - "digest": "0dde6336b070f5e9919189de576fdad96d69eb993f9b29a7a48875bc044f229b" + "captureGroups": 17, + "digest": "1117b573967ac1c687fe336f476f3c4e94680925f9a3ff2a84a47a87bdf86208" }, "go-return-type-inference/models/user.go": { - "captureGroups": 16, - "digest": "c133a3db65e1952277b7c986f0870db86d4bb762ff0043d2160ed772317e03ca" + "captureGroups": 17, + "digest": "a83045c31b12d9dc45b07315b081bef26ec380479d39706ee8c5d79e4efe5d88" }, "go-same-package-factory/main.go": { "captureGroups": 14, @@ -340,8 +340,8 @@ "digest": "827dc0208b47776976313a4560fbd250ab7fa521ae46581213a79fc15fd52ce8" }, "go-struct-literals/app.go": { - "captureGroups": 10, - "digest": "97ec29ec0e0dc1f23a804ff6808023a32eafdeb00f194d3bea9b50ed54c7f258" + "captureGroups": 11, + "digest": "bcdf97254bb1518219d85ff75795f72ef7a41c4e43ddeb7a09648349d46ec347" }, "go-struct-literals/user.go": { "captureGroups": 10, @@ -352,8 +352,8 @@ "digest": "b5ef588c11391fa93237260baae4c4bb67ee4dfc381651444a9925c3f52adec3" }, "go-structural-interface-cross-package/cmd/main.go": { - "captureGroups": 27, - "digest": "b44adcfaa2b965479abe17b406c6c74ddd51bb54e7d129e8a99a26a2a3f2fc16" + "captureGroups": 33, + "digest": "340f5f3f501be8e4f0fb7fdc63949ec2c6ee535eac1480d0090f9f5aab0dfd11" }, "go-structural-interface-cross-package/contracts/read_closer.go": { "captureGroups": 6, @@ -368,16 +368,16 @@ "digest": "4d6298ea05addadfa17f6dd8a2eb0fa2ea63b73de69355015594504ae4e6c1b1" }, "go-structural-interface-cross-package/store/repository.go": { - "captureGroups": 33, - "digest": "9a497524610994e9a47555cd5874074b278d3e12850e3ee62a1afadb25fcabe8" + "captureGroups": 37, + "digest": "1b69c40092cd49588936e68bfb831d1ccfd5764bcb5bf42279bd66d9cbc57450" }, "go-structural-interface-dispatch/repository.go": { - "captureGroups": 129, - "digest": "f31bc522991e59527834bb7dcd45b8738490c622c3d25181a8148cc85d27844b" + "captureGroups": 142, + "digest": "986d18b1db74887f5f6f91876aeb332111d85be658dd5d737d104a86204b5575" }, "go-type-assertion/main.go": { - "captureGroups": 11, - "digest": "b8bd327d3965531802a93bc01e3c24969a027540397a1635119ef0b7da46c348" + "captureGroups": 12, + "digest": "046533d699a7a037ea6b891b0e5dbcdeda19cc1554c2c0aca14005151a72c7f5" }, "go-type-assertion/models.go": { "captureGroups": 18, @@ -388,15 +388,15 @@ "digest": "443f9736b9c67df30fe97d8353ddc2584de4c88ba698e62878683d5d77362d89" }, "go-variadic-resolution/internal/logger/logger.go": { - "captureGroups": 4, - "digest": "83b987f527f95e360966793148d07e569fa02fdc5e4febc09daa51cfce16b114" + "captureGroups": 6, + "digest": "7c8738c8300962e5ade309013e229008499c6e34971eacf1d785bf17cf17af7f" }, "go-write-access/main.go": { - "captureGroups": 20, - "digest": "92fdad46b6933fd78dcbb19677a720f037306a4f03fedcca9156b54c1c9bf994" + "captureGroups": 21, + "digest": "20c312ccec297b19022c681a486656b0f2d5d3562c791588df170043f4e45b09" }, "synthetic:dao-20": { - "captureGroups": 481, - "digest": "611e1f8147dff51e8857e493e06089aadf17a042cee6a522520433025b2bc0da" + "captureGroups": 521, + "digest": "cd4e6169abfe6016f000a884acf0604f7fea0ea00d9e1f8f52c7d5ad9b97a01a" } } diff --git a/gitnexus/test/fixtures/php-captures-golden/expected-captures.json b/gitnexus/test/fixtures/php-captures-golden/expected-captures.json index 06ba54705..476165c99 100644 --- a/gitnexus/test/fixtures/php-captures-golden/expected-captures.json +++ b/gitnexus/test/fixtures/php-captures-golden/expected-captures.json @@ -1,23 +1,23 @@ { "php-abstract-dispatch/src/Contracts/Repository.php": { - "captureGroups": 10, - "digest": "3459af9360d51aaaba72963fc49bb79edc188e97f9831421d6504c7919b61dc5" + "captureGroups": 12, + "digest": "e7bc6e98e30f2cca0cb4917e951a1b7713cd36c8565cd50e8d49819d6775c7bb" }, "php-abstract-dispatch/src/Repositories/SqlRepository.php": { - "captureGroups": 14, - "digest": "584da0b8d38ba3b2e45513e24ee17e0366bcfe48e353c274daed367250a0ccd9" + "captureGroups": 16, + "digest": "d5a159f9082316966a3cd1e61db93ee513ea3a6eba287080a2fcfbf3eb9f8fe9" }, "php-abstract-dispatch/src/app.php": { "captureGroups": 10, "digest": "af85999f2ddf2bf718cc419553efd09425919835460bd6739277cc570cb2d3c4" }, "php-alias-imports/app/Models/Repo.php": { - "captureGroups": 14, - "digest": "4fd03e4a19a7a7c2da925f850563b6dccd21b898223c00278bc2753e8841d844" + "captureGroups": 16, + "digest": "e975f7f079a5ee0d00cdbc9834bb33fc5d86392a613e1dd8c9bc37cf78bbaaa0" }, "php-alias-imports/app/Models/User.php": { - "captureGroups": 14, - "digest": "5a20d985b3f1c04e998feb586e8b9b382ab069829db69b156ab2636867096a93" + "captureGroups": 16, + "digest": "aefe482576d3a0e03ab62d69fc20ff08ddac8175ee6ff02b08e2422dccc39baf" }, "php-alias-imports/app/Services/Main.php": { "captureGroups": 15, @@ -48,28 +48,28 @@ "digest": "0628d6520b02da49ace196f582704c3ffe88aadb3ea20cff642699aa122d5f7f" }, "php-app/app/Contracts/Loggable.php": { - "captureGroups": 7, - "digest": "46cf7e385db7656514b796908134a50f4a536f3adda64196e17d8c54f5cd5e3a" + "captureGroups": 8, + "digest": "a42a03745e80b9e7938ffe1ca9bd5839054903c3b9fc7afe0f516eaf7a4bac21" }, "php-app/app/Contracts/Repository.php": { - "captureGroups": 10, - "digest": "40bbdb3fc0ec9b5a9bb8fbdf4a053099ac926951ecd746f2436569add3f0e3d5" + "captureGroups": 12, + "digest": "d495916b169cbba1486dd02e9a7d6b2b958a56cc4c31c7da3437d4f3e2808260" }, "php-app/app/Enums/UserRole.php": { "captureGroups": 10, "digest": "203eb6943ff7527cc52ad93df040a4d15dbcf16beb9e78914401069c54f20bb8" }, "php-app/app/Models/BaseModel.php": { - "captureGroups": 18, - "digest": "7d5a013f34f909846e3a91cef4b3daa8638034e5c6939d90ccd7dae34c8f9272" + "captureGroups": 20, + "digest": "0858aad2d5de0e09cf8cad1cdcd8cfcd6774aacb3673203e9ebedf6b5e220fb4" }, "php-app/app/Models/User.php": { - "captureGroups": 27, - "digest": "718b9ba0238e2139ff34027d68cb135c4698a2fc401a4f85200864b1e27a9a78" + "captureGroups": 31, + "digest": "ce6edc7b23136e6b428901a380461cfb24f20eeebdb88800017bef8780e709a5" }, "php-app/app/Services/UserService.php": { - "captureGroups": 38, - "digest": "a911c5af5042738b1ca9aeb71dad06f4918d8c41e84b6a27ffaa0eadfe56ca22" + "captureGroups": 44, + "digest": "98a1f7f498c0b8298f6ca23ace99f9a767b6b0ca402c9ed881b114ce64098eab" }, "php-app/app/Traits/HasTimestamps.php": { "captureGroups": 11, @@ -88,20 +88,20 @@ "digest": "5e6b0b6f2bbe05e8ee310e5a91163b087b846c38a07dc22ab3ba165b939d7691" }, "php-assignment-chain/app/Services/AppService.php": { - "captureGroups": 15, - "digest": "0e9a9ef52a987dbd12c9478a75d5019c2a0059ab4b862924597d2ecd32f03aa7" + "captureGroups": 19, + "digest": "2fca0da2f6c317b6fae76267e9c0eb4cee0f3467e1f0ae856e9806ee2d932316" }, "php-call-result-binding/App.php": { - "captureGroups": 23, - "digest": "48c7af06cd4c12f07f308d8804c1c3652c2af87d155831fc5ee915fd984737d2" + "captureGroups": 26, + "digest": "d3d3c6b3cb4c9a1dff62921d054ded2168f6400b22826e6484dba12afbb623c9" }, "php-calls/app/Services/UserService.php": { "captureGroups": 7, "digest": "84e2e65cbb4026ef9e67ac2710d1304c8dffe335f2b1b1cb70123157a04acf50" }, "php-calls/app/Utils/OneArg/log.php": { - "captureGroups": 5, - "digest": "bd3d0b4f7da34edfa86b3e5e9cd6664439baf7cf07318098ac562f48c40f2a72" + "captureGroups": 6, + "digest": "850fa336ae84361e86e208f14118429624758372716bbaa47deabe994151f774" }, "php-calls/app/Utils/ZeroArg/log.php": { "captureGroups": 4, @@ -120,20 +120,20 @@ "digest": "e7bbcd72127b97475851b17cf7fbc5a86786091b2117313dfb24deab8a0d6d09" }, "php-constructor-calls/Models/User.php": { - "captureGroups": 14, - "digest": "a7d4f11e20fb687f537d1ca0735f8e85c9ea8a2b29867bb57326f02da3bb63d1" + "captureGroups": 16, + "digest": "420d4e556aa30e133013cd162039a002f94804dec4b84c7866e3b07985f7ce65" }, "php-constructor-calls/app.php": { - "captureGroups": 8, - "digest": "0f3e6cf60248ae02ca0a744d3fa4f67b2d8421858c29243c4fcaf60763c7ac3d" + "captureGroups": 9, + "digest": "8850b2a9c3b4c13840321da4a7c7fb3921b19712e64aa6a57bec66368c1cfc30" }, "php-constructor-promotion-fields/Models.php": { - "captureGroups": 22, - "digest": "ac49ef4de6665d63a0bef9449e1c685f2fd20fbb01c5d26c924ee0d469f614c5" + "captureGroups": 24, + "digest": "425bcdf14f3825cb78a864287ff9d9a1228a0dbb526aa5d05905945c24c101c7" }, "php-constructor-promotion-fields/Service.php": { - "captureGroups": 8, - "digest": "d7093850a8c703a5aefbcd584e5bc8e7ac5668e14ff79e27b1db401f72e83073" + "captureGroups": 9, + "digest": "c5efa7143b83d8677edfdb3cdedf27b63b5587babd12b861cb90dd05bf739327" }, "php-constructor-type-inference/app/Models/Repo.php": { "captureGroups": 7, @@ -160,16 +160,16 @@ "digest": "f20d780f2bc95ed438047c8e9bb694f5a020454161d8a20f8d5e02be68e4a602" }, "php-deep-field-chain/Service.php": { - "captureGroups": 9, - "digest": "0026ff9cbad4125b5c27ce356b7c954d369604c5129460d9a0f51cd1c46afbca" + "captureGroups": 10, + "digest": "f5bb845f2a06252e7d030cc1bb8adca213dece5162ea6240b52b2545a9c6bf4d" }, "php-default-params/app.php": { - "captureGroups": 9, - "digest": "c5b100802ae7d78869f099ab886133d16f865a653e05bdc477e6b4e523accf91" + "captureGroups": 11, + "digest": "3181bc162c5f271be83d1e38a3d1ba6a6ab0421d4763e9c80dbef439305bd72e" }, "php-dynamic-calls/app/Services/Dynamic.php": { - "captureGroups": 52, - "digest": "1a15600d585851ee1f05927c27e6ce6ccaf6a3f56aecf36e27f84a20a65bb9b2" + "captureGroups": 64, + "digest": "43e09d594d62b1586fc9f9450c9713132e1e2cd3682cbe14b5fd4105f421ef42" }, "php-dynamic-calls/app/Services/OtherTargets.php": { "captureGroups": 6, @@ -184,56 +184,56 @@ "digest": "23e6855d9ac081c156d7f273a6ca4076c878d900d97822b436d2f190e2272064" }, "php-field-types/Service.php": { - "captureGroups": 8, - "digest": "d7093850a8c703a5aefbcd584e5bc8e7ac5668e14ff79e27b1db401f72e83073" + "captureGroups": 9, + "digest": "c5efa7143b83d8677edfdb3cdedf27b63b5587babd12b861cb90dd05bf739327" }, "php-foreach-call-expr/Repo.php": { - "captureGroups": 17, - "digest": "f6082a43eb279f1de8f8ffea93c1a8ee05b7c5b39fdf2eb0a308c8170b7ab7d1" + "captureGroups": 19, + "digest": "819b69e2a722176f735fdcdcba9ff214a697649c94c927729ae7ce0b935e0157" }, "php-foreach-call-expr/User.php": { - "captureGroups": 17, - "digest": "799e12a9ebddde34f030e0612724f8a31893d33d45421607e619070036e30937" + "captureGroups": 19, + "digest": "28d841e1f3ff97ccabbb3291294d91e1cabf6121809df95c1cc0c2c2f221614a" }, "php-foreach-call-expr/main.php": { "captureGroups": 11, "digest": "ce332b01f1bbdfaf0e03a3d77f7f708e9f7e5fd41eaa795be8fd2584ab86711a" }, "php-foreach-generic/App.php": { - "captureGroups": 16, - "digest": "6f5355bc0d315224e3ce2696f3fac3b067f90771f291ce73e2cfc9c5c9ece59c" + "captureGroups": 18, + "digest": "150b1244810ad7cc2f6125e3ac2ad945f40222797905c8db3a142da813a5a200" }, "php-foreach-generic/Repo.php": { - "captureGroups": 13, - "digest": "0ba7bc5bc2387e1f07898a288a4d856349b777c9be12ace6ebd2ccf39124c446" + "captureGroups": 15, + "digest": "f166a1db89a938d16ebb4503e424df6f0e3fe5d706eb06408eeee8449dc55f45" }, "php-foreach-generic/User.php": { - "captureGroups": 13, - "digest": "eb34a7e89368d13116de3cb7b0430bc2d1b9c982e3b3f7c9511dae8a73759af6" + "captureGroups": 15, + "digest": "3c646f44302a13d7714c30853063acbdea673eb78f5039be56062c7a453707eb" }, "php-foreach-loop/App.php": { - "captureGroups": 10, - "digest": "0a648289cb93a52808d1d6ee8b519fa28c413227607deb0c3d3a9a0cdaa52e02" + "captureGroups": 11, + "digest": "64409c5307bc7d29f9107867358dc1c46a8de901c31fcc67d017353903331de4" }, "php-foreach-loop/Repo.php": { - "captureGroups": 13, - "digest": "0ba7bc5bc2387e1f07898a288a4d856349b777c9be12ace6ebd2ccf39124c446" + "captureGroups": 15, + "digest": "f166a1db89a938d16ebb4503e424df6f0e3fe5d706eb06408eeee8449dc55f45" }, "php-foreach-loop/User.php": { - "captureGroups": 13, - "digest": "eb34a7e89368d13116de3cb7b0430bc2d1b9c982e3b3f7c9511dae8a73759af6" + "captureGroups": 15, + "digest": "3c646f44302a13d7714c30853063acbdea673eb78f5039be56062c7a453707eb" }, "php-foreach-member-access/App.php": { "captureGroups": 14, "digest": "44e6f810de40d566b5c909d91dcc50de5282fde2f597bf38302edbcb8d825ca3" }, "php-foreach-member-access/Repo.php": { - "captureGroups": 13, - "digest": "0ba7bc5bc2387e1f07898a288a4d856349b777c9be12ace6ebd2ccf39124c446" + "captureGroups": 15, + "digest": "f166a1db89a938d16ebb4503e424df6f0e3fe5d706eb06408eeee8449dc55f45" }, "php-foreach-member-access/User.php": { - "captureGroups": 13, - "digest": "eb34a7e89368d13116de3cb7b0430bc2d1b9c982e3b3f7c9511dae8a73759af6" + "captureGroups": 15, + "digest": "3c646f44302a13d7714c30853063acbdea673eb78f5039be56062c7a453707eb" }, "php-fqn-cross-namespace/app/Models/User.php": { "captureGroups": 7, @@ -244,8 +244,8 @@ "digest": "9989dd5b86b705df941fc63dfd7fcbe4520dd1378a16a50b5f3dfd0ebed7bd90" }, "php-fqn-cross-namespace/app/Services/Service.php": { - "captureGroups": 15, - "digest": "e711548813d38a0db268d89a7edff1d31bbaa9ff0399898e09f8e93bdbc1c785" + "captureGroups": 17, + "digest": "9600f70b3d9eab344b61ddb8d9e278718377c72934dc5187036cc976b0660012" }, "php-grandparent-resolution/app/Models/A.php": { "captureGroups": 9, @@ -280,12 +280,12 @@ "digest": "aef9ddf30722a053664637b7fefdfcdd541baf09b24e65a3a0d80fe920189828" }, "php-local-shadow/app/Services/Main.php": { - "captureGroups": 9, - "digest": "52549cd82f7e2e69fb8bb81bca4e5e3487e216f4305023b77579de92374b9ee3" + "captureGroups": 10, + "digest": "bb72a6453d4f54935a0cdab1eb35b65182340ddc9a335dc94cafdadab2a5f586" }, "php-local-shadow/app/Utils/Logger.php": { - "captureGroups": 5, - "digest": "7434c0a1b2282daf8d60334a85523d603bffd256eaaf5c241509e72c0a6d1e5d" + "captureGroups": 6, + "digest": "2ffa672fe694cc3fd9a66b1b9456da9fbb92e9cf5a1c43a43116c8cba698f635" }, "php-member-calls/app/Models/User.php": { "captureGroups": 7, @@ -296,12 +296,12 @@ "digest": "dfe3ef69a8d6dbd95cf5a6bdc754914973b7208ad0c9575bf2595a0f84ac22cf" }, "php-method-chain-binding/App.php": { - "captureGroups": 48, - "digest": "8cab0917ecbbd65f43046dcc3c03f7af2d8e1a51780d4dbf3f7bc29ccabd2fa2" + "captureGroups": 54, + "digest": "dcb82de39fad8dcba84622d28c304842950dd7bb1c16e0e4ac7cd2117481d226" }, "php-method-enrichment/src/Models/Animal.php": { - "captureGroups": 12, - "digest": "f32de8a30558f2678e73c68d7db9c6eab827fd8d5f7655b906a76066c8421bef" + "captureGroups": 13, + "digest": "8b21a9220a2e735b637576e2d4dd351490859dd07a52beba097c3ee340f04817" }, "php-method-enrichment/src/Models/Dog.php": { "captureGroups": 9, @@ -312,16 +312,16 @@ "digest": "ebfa35aa3eb065977f922ae72f353d5bf31dd49956c3e9aaef349718e555b7a7" }, "php-mro-arity-mismatch/app/Models/ChildModel.php": { - "captureGroups": 16, - "digest": "eabbdb94047d99d03b7e5cafcde4b0f89666695ca4c3b97ea7e68148999c1c00" + "captureGroups": 19, + "digest": "6e194ffcc5049d0654c60a182b27245171f1685e295f6aa2e15368f39139dd1a" }, "php-mro-arity-mismatch/app/Models/Orphan.php": { - "captureGroups": 9, - "digest": "29d2e5732698a9951194a6be3d788e337a142c5ea174c5356d375cf9c9e9ff58" + "captureGroups": 11, + "digest": "418e7f982d402169e624986bacaa084e62774482e0ca53af7e0d689f1473f1d7" }, "php-mro-arity-mismatch/app/Models/ParentModel.php": { - "captureGroups": 12, - "digest": "e16afaad3145133dce0f2de89d0f58eec33492b6a67741013eaf27e35ce6aa07" + "captureGroups": 14, + "digest": "d5960861a820159bd8ed99a4032b28a4ecc132f6f2a1c412a84ac3425a34177c" }, "php-mro-arity-mismatch/app/Services/Caller.php": { "captureGroups": 22, @@ -336,12 +336,12 @@ "digest": "7f7a357fb5d758f9d4686b072c4d5df82026701ed28bdce042c5c7950a893c41" }, "php-namespace-fallback-isolation/src/App/Utils/Format.php": { - "captureGroups": 5, - "digest": "dc33b55ffcef13d75972ca695921ce75c88ea21f37960ad138ee04267f3ccba4" + "captureGroups": 6, + "digest": "bf58ab493d904cc45d38539dc270dfcbc75614d8fcdcc400f7c1e1953ef3ee3b" }, "php-namespace-fallback-isolation/src/Vendor/Utils/Format.php": { - "captureGroups": 7, - "digest": "eff9d04520000bfc5fd7ae87721965e3000d1b478786f95b4f51e001ad877faf" + "captureGroups": 11, + "digest": "c372e232faf99874eff46877b71f70b533b496832b762833f2b266ca9939978f" }, "php-nullable-receiver/app/Models/Repo.php": { "captureGroups": 7, @@ -352,16 +352,16 @@ "digest": "cec22d127d637784baa722787a0a5e5402970ca2dc23a20f07e644762492d8bd" }, "php-nullable-receiver/app/Services/AppService.php": { - "captureGroups": 13, - "digest": "52e6db35e8fa4174ce698fec802b9b034dcda8f49fd7ab98cd9e884a6bd9777f" + "captureGroups": 15, + "digest": "63e67fa5d12829c99658ea545620466b1bc071ec4e082d02f8e4f4a080f1d7cd" }, "php-overload-dispatch/src/Services/Formatter.php": { - "captureGroups": 7, - "digest": "5a4979e230bd666c0dc35e8e9611adf67d770858d1897ea113610f0698ccea7d" + "captureGroups": 9, + "digest": "181754c8029dbcffebaee51dd89f7c383b321339fcf57721287ecc4a82c55487" }, "php-overload-dispatch/src/Services/FormatterExtended.php": { - "captureGroups": 9, - "digest": "17dadd44959529ba4533df45561025d00be7ae2f0a2503cbde4a69e9b72614c1" + "captureGroups": 13, + "digest": "8c877481daca4ab21128fbf74e4942a71cb45809e5f2ac8f7fabd3285066068d" }, "php-overload-dispatch/src/app.php": { "captureGroups": 10, @@ -396,20 +396,20 @@ "digest": "8c5cd8fb19d4f3964c68533834db49d85eb5f93e04ba067a617f5a6c655a0046" }, "php-phpdoc-attribute-return-type/Services.php": { - "captureGroups": 37, - "digest": "d857f9bc514191f222a2e0880c67ab958f2689fc1c6ac0a55af9c8e26fd9c0c0" + "captureGroups": 41, + "digest": "fa7b5e2b69d746e024ea60652923e37470f79a5510380f792e48b253e0f4b60c" }, "php-phpdoc-return-type/Models.php": { "captureGroups": 11, "digest": "8c5cd8fb19d4f3964c68533834db49d85eb5f93e04ba067a617f5a6c655a0046" }, "php-phpdoc-return-type/Services.php": { - "captureGroups": 37, - "digest": "17ba0088f086c768d5ab6a07de513669a41787ca5f26c33114e6647ef683c920" + "captureGroups": 41, + "digest": "4a62988be2b037a32584b9093ca667490419a401babe2fc52d84f25565c17731" }, "php-property-promotion/UserService.php": { - "captureGroups": 19, - "digest": "872830776a740cf97c1eafac5e3413aae99e0e951b1b5b46d1aa0df821eac8bc" + "captureGroups": 21, + "digest": "6373f13321137c00923fdef141121e13013a8ef5ed31928d957dc146e95b87d2" }, "php-receiver-resolution/app/Models/Repo.php": { "captureGroups": 7, @@ -420,16 +420,16 @@ "digest": "cec22d127d637784baa722787a0a5e5402970ca2dc23a20f07e644762492d8bd" }, "php-receiver-resolution/app/Services/AppService.php": { - "captureGroups": 13, - "digest": "5e92226af09405c7fc4a02d3aeafcedc8462f3e525b8f67b8509f38091488505" + "captureGroups": 15, + "digest": "29ae6c6b128776d8664e7172b4b5ece311b07fe715fd1fa9d1b9aae43a49b823" }, "php-response-shapes/api/items.php": { - "captureGroups": 11, - "digest": "84e64331d581f16103b3b0c8c95819e41c36ca2e16b6186ccfa73a13729577a1" + "captureGroups": 15, + "digest": "fc39dbb91468db99b833e18aa08f2df2fd8760588e184170f76bfaf32a3f0c15" }, "php-response-shapes/api/submit.php": { - "captureGroups": 17, - "digest": "f5d1af2ad313f4639fbad558eed65d3fa0c218eefc55843ea9e07156a1554883" + "captureGroups": 18, + "digest": "f10a59dd26b7002bcc406bb04aea7ad068b2cb691b3716cc62e681afa000c2c4" }, "php-response-shapes/includes/auth.php": { "captureGroups": 4, @@ -440,12 +440,12 @@ "digest": "f04e8cb824fba439cea5fdab461ade906b49953b7e8cd2777e724f8fa66e9b31" }, "php-return-type/app/Models/User.php": { - "captureGroups": 14, - "digest": "5b41d9519e2d696436cb5e6a1337619def5882c2bc442af74ac15a8570b6e168" + "captureGroups": 16, + "digest": "954642025fa2ccd7e8182c95dbc932dcada58884b627603ca6477d3d12242b8a" }, "php-return-type/app/Services/UserService.php": { - "captureGroups": 17, - "digest": "ac4851815d7a450ab92f7f68ccadabf219d2534b99ef1b65ced0a344188f2a69" + "captureGroups": 18, + "digest": "de72421c29529492bc553e6c049b926222afd7d455c6e43260781af234bd559e" }, "php-self-this-resolution/app/Models/Repo.php": { "captureGroups": 7, @@ -468,16 +468,16 @@ "digest": "56d657425e80ae4e58ffe139f90afab5a8527e191bc4b76662f5ce4cbbca4cbb" }, "php-this-receiver-disambiguation/AdminService.php": { - "captureGroups": 15, - "digest": "db9ba7004ba7b98984307f2b3e1a71154b5a7d60403286d7f308c03ed351b18a" + "captureGroups": 16, + "digest": "c95d2acf2aaf17c610d182e56df28ddafc93adedab0f685f6903065c12a77ff6" }, "php-this-receiver-disambiguation/Models.php": { "captureGroups": 11, "digest": "8c5cd8fb19d4f3964c68533834db49d85eb5f93e04ba067a617f5a6c655a0046" }, "php-this-receiver-disambiguation/UserService.php": { - "captureGroups": 15, - "digest": "147924d3638edc63eef1a009943fae66a87ac85d02ae533428646c644e81c2bb" + "captureGroups": 16, + "digest": "2c6c2005dfc643487d7801d3b73e157327c89732da7922826728c953b07de2d3" }, "php-transitive-traits/app/Models/Consumer.php": { "captureGroups": 18, @@ -496,28 +496,28 @@ "digest": "9d55a120a79ead7605f2e05c80b36615c228acd5cf83db5417a5efe4c9ed9e7f" }, "php-typed-properties/app/Models/UserRepo.php": { - "captureGroups": 11, - "digest": "19089a297be5f83a4a7610a2111a1fb1b84806cda5fa7d6dbb16c9dfc137b6d6" + "captureGroups": 12, + "digest": "42acb30febe31b52028f03d856a9b57a1c518bbe2aeafd1f5c0c3caa9785d64d" }, "php-typed-properties/app/Services/UserService.php": { - "captureGroups": 12, - "digest": "9f0540a4f7f322ee96dad4437c4e41e087f29a6993d2659fc48bd26e3277e963" + "captureGroups": 13, + "digest": "01719eeb52ccd723add0f6132bfce85b93c2b6e53c08c1e1c358d09302355d60" }, "php-typed-property-dedup/app/Models/UserRepo.php": { "captureGroups": 7, "digest": "4b0043ea779b33cabc12f79cb4aef32759284137f20838346e9f5ef82076be7b" }, "php-typed-property-dedup/app/Services/Mixed.php": { - "captureGroups": 14, - "digest": "97d09b46f6e66bef700f4686e5988c98372853203885b9a2d99dafce6fdc8343" + "captureGroups": 15, + "digest": "25262e91a1f84e6b34c794c9bc4f61771f41325bc1c3d028901fbb8334284ca2" }, "php-unresolved-receiver-arity/app/Models/Handler.php": { - "captureGroups": 21, - "digest": "0e45194dc4a1bf420d4325756a780b0574129517e70d83a82c5dd68b921a0f67" + "captureGroups": 28, + "digest": "0ba1e08801515cb80dec911710767f6ac030285080eb94a0c0ceec84ce106e90" }, "php-unresolved-receiver-arity/app/Services/Caller.php": { - "captureGroups": 28, - "digest": "a3d316585e67b65ac85a63991735c1324911c709a6d9714f9c3eb4ca6a95f807" + "captureGroups": 34, + "digest": "474a7bac44ce6026b3cb4489d8d3d8e359959c32f43a10b32e006a1869ff0995" }, "php-use-function-const/app/Config/constants.php": { "captureGroups": 2, @@ -532,35 +532,35 @@ "digest": "b2206861c8550b6d2c7610ff167c42f4236c7249c87584b418e05f687233ad22" }, "php-use-function-const/app/Utils/helpers.php": { - "captureGroups": 6, - "digest": "98ebf5b3427b39b619e78eaf11a46376eba5ce7fef202508ce169afd8c8e979e" + "captureGroups": 8, + "digest": "46dba5667a66e8b91d83738015ef3d708509dbbf7ecd0f8ef4ff5161719b8baf" }, "php-variadic-arity-minimum/app/Services/Caller.php": { "captureGroups": 29, "digest": "4ac7be9ff21c52b76630cdcfde0eb41af43e126c2de5db327e9f82c46c3aabbc" }, "php-variadic-arity-minimum/app/Utils/Logger.php": { - "captureGroups": 18, - "digest": "dbcc7f0bf265a950952c4d192bef14924b29b31326942f4f1600aa8bbe22e9a9" + "captureGroups": 30, + "digest": "a75622e071f4028125b47ebbde4624b8d0cfb2c6ed7210a019b2b1e301137cfb" }, "php-variadic-resolution/app/Services/AppService.php": { "captureGroups": 9, "digest": "6591007d2f4ba85b25a59c11c3ae200452fbad23cacb1ebc04291a074b534dd8" }, "php-variadic-resolution/app/Utils/Logger.php": { - "captureGroups": 7, - "digest": "8e075546630b3057577932dcf8a876b450950a5accd7df3f8a87079cdc401139" + "captureGroups": 9, + "digest": "a1ca177024aaad2d7d60b52efd48b8be0750fad2a645142897a803b21bc2803d" }, "php-write-access/models.php": { "captureGroups": 13, "digest": "b7f00a2f5e9c64d9823d11279d831bf9320c697e69184f368d288348fc9ac39e" }, "php-write-access/service.php": { - "captureGroups": 8, - "digest": "5ef6b1687663e4cd9acb880a570fc32388961e735ad38d8ea9906da173971362" + "captureGroups": 9, + "digest": "bfab872dccd371da947e06ba4114e2b0d92fe779256ee3076672e4190db64670" }, "synthetic:dao-20": { - "captureGroups": 221, - "digest": "e29ee5132bb30bf939508cf3741159856b78881c01447acd290e18b1c5121225" + "captureGroups": 261, + "digest": "67e5362403ba445704d57df6265d928e0701f2bbef7108046d07d1cfaa5d3823" } } diff --git a/gitnexus/test/fixtures/python-captures-golden/expected-captures.json b/gitnexus/test/fixtures/python-captures-golden/expected-captures.json index a290e13ef..254f2bb0d 100644 --- a/gitnexus/test/fixtures/python-captures-golden/expected-captures.json +++ b/gitnexus/test/fixtures/python-captures-golden/expected-captures.json @@ -1,103 +1,103 @@ { "python-abstract-dispatch/app.py": { - "captureGroups": 11, - "digest": "8662c17b0f21fcfa650065abd62f0c9b7e1c65bf8a1f7dce6f2a16bba9df759f" + "captureGroups": 12, + "digest": "dbb94bff45b2e0601e2ba54a4e275c2668b99836e0565243406776b3a2268242" }, "python-abstract-dispatch/base.py": { - "captureGroups": 18, - "digest": "f555f01c16bd9696b9854eccb0225ed6df3d271eebcac19812914ad38004786f" + "captureGroups": 22, + "digest": "0de787f2a48bc8089b0edc376f129b7e16d2b6f1c93677deda4c24dbd3513877" }, "python-abstract-dispatch/impl.py": { - "captureGroups": 15, - "digest": "6c27015f13d32024ce06c1515ab29ca0436df7a4864bbb62188d5f79d685cc31" + "captureGroups": 19, + "digest": "adf88901cfc63fb9018cba28a675ce9b7c576752bd8d89b8664ed405e34347f5" }, "python-alias-imports/app.py": { "captureGroups": 13, "digest": "ae1dc39a6d300a38bade534e58a1fcf5141eee06c6e903645a80dbce175b4949" }, "python-alias-imports/models.py": { - "captureGroups": 11, - "digest": "abf5fcdf7cc473efa5a149319b0fc3d14b3ae91a2e9a14d621243c5ed7fafafa" + "captureGroups": 13, + "digest": "cec30bb7577edfb751bcd4f5026a0c9d77ed235071421a2a46f87580c7bcdf47" }, "python-ambiguous/models/__init__.py": { "captureGroups": 0, "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, "python-ambiguous/models/handler.py": { - "captureGroups": 6, - "digest": "a148084ab160b53bd9a59f12a605447dee85eca1a46abfcca62fc48de56ca3a3" + "captureGroups": 7, + "digest": "65f156895b52d0f2f221edb0ae34294b2a4ded348ed859f47b601aee4d7e516b" }, "python-ambiguous/other/__init__.py": { "captureGroups": 0, "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, "python-ambiguous/other/handler.py": { - "captureGroups": 6, - "digest": "41330f66b51bcedb14cdcd4cbb88601e3426e246d547ab7cf060da7c294bc8de" + "captureGroups": 7, + "digest": "37a41af1b2d5e8b2592e9e72ce2ca224a9a0ab4d9171819a80406931c16b9f86" }, "python-ambiguous/services/__init__.py": { "captureGroups": 0, "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, "python-ambiguous/services/user_handler.py": { - "captureGroups": 8, - "digest": "5a4bb82d0e6a6a53fe012f197e42c572ac1169ff38e84db9c6281399f6739021" + "captureGroups": 9, + "digest": "cc2587ab88c688b5a87ac1500829b445197accdd52aa6a145b7e2dce2b115124" }, "python-ancestor-import/a/b/c/deep.py": { - "captureGroups": 5, - "digest": "633dd357999ff9124b5c1faba69d0156f4a996693f4cb5dcd05e6cf530d897ea" + "captureGroups": 7, + "digest": "f183d93d9f0c70d893ff5062ce0621443e093b6bda8d223b15817dfb37ff3945" }, "python-ancestor-import/a/utils.py": { - "captureGroups": 3, - "digest": "4d85820bac0e64fb70eceee1454424a75bc3db10f9fc9e7d822e3539ef50c8c1" + "captureGroups": 5, + "digest": "02af10663e3387191f6cf8b2c1872afc578c336a0117d539c8ad2295ffef0286" }, "python-ancestor-import/backend/middleware.py": { - "captureGroups": 10, - "digest": "ed9769cc43e5e1b6dcaa5179e1c80f07ef15e75e728e6ae039091454b966fcec" + "captureGroups": 16, + "digest": "a53ebfd3ecd324bd63451675564f4842cd5b21f6e0c73619efd95d0ca83c4662" }, "python-ancestor-import/backend/services/auth.py": { - "captureGroups": 7, - "digest": "73968a5d120b0a97e95e96b7c0c7433575e8178a832b735b42d5be34c72e2ba3" + "captureGroups": 15, + "digest": "6d8279e5a75475669231265317c58fd0ab2dc19c9f3eec168f377ad38e1de407" }, "python-assignment-chain/app.py": { - "captureGroups": 27, - "digest": "26ab1b7aacc902373062b7d4a5c88e41619a3352664cf27e3d3cd92012f6a24a" + "captureGroups": 29, + "digest": "1c0ada0748ce42a77ab26dd1c35ea2246ee1c701158004c65a0944d1b28cae53" }, "python-assignment-chain/repo.py": { - "captureGroups": 6, - "digest": "0d2704cfa14e431395973cc82b582f9fae43a8ce67d5548ab5df62b3fa8d1b7e" + "captureGroups": 7, + "digest": "29bd91eac43a803bb4afd96ee2d733f62a8c7a56c02cbca79e3d5394ff69ecb6" }, "python-assignment-chain/user.py": { - "captureGroups": 6, - "digest": "4dd0a9d0f5797bae1c281a07145ddcc4162eae1ea16208e85edb74cd98e085f9" + "captureGroups": 7, + "digest": "3fa157d0e0576fe87c6abf5ce9a3509c82c04b2ecf1fb6d53290ddc1326af8e4" }, "python-bare-import/models/user.py": { - "captureGroups": 6, - "digest": "3e5160418c4e3b4c0a896bed89e731711aff3a66872d99bb5ebfcbd624d9a3d2" + "captureGroups": 7, + "digest": "e023fa6d98558619733e75005e4c6dc4079967b4d43c2fc2ecd24136a4de6e1d" }, "python-bare-import/services/auth.py": { "captureGroups": 8, "digest": "8b3659bfdc60eedb1a33fd803f5ad23f1458f134884ed600e3fb4a662e19dbf3" }, "python-bare-import/services/user.py": { - "captureGroups": 6, - "digest": "0662b53fd75f4892afa665d4dfe51a8dc963971696a370a8b2c4ba124eeda2af" + "captureGroups": 7, + "digest": "89d7afbcc21096723cbbeaa5f12a5cec106158d9b426668cd73accdc08b337e2" }, "python-call-result-binding/app.py": { "captureGroups": 8, "digest": "338c3922981604e71ddfc60ad61eba4b17f68ca654644e01add942c729b422cf" }, "python-call-result-binding/models.py": { - "captureGroups": 12, - "digest": "a7f11709f76dcc9311554b4fe1dd4e95f5d6434e6232627b4c370f21e194bd69" + "captureGroups": 16, + "digest": "cbbb5168c28123820a70fe24016b0ed26ab02a6339d21ffe40fbccad940c1d70" }, "python-call-result-binding/service.py": { - "captureGroups": 7, - "digest": "81f8d5783bfc95a9f83f02abd15f7bbc0869aaca964c62b40653cc05ab73ebcb" + "captureGroups": 9, + "digest": "40bdb4b59d0540330fcb861dca2f2b3fdb432e529af326ffc692cf69201f0af5" }, "python-calls/one.py": { - "captureGroups": 3, - "digest": "4eee8157b23b84308163ffc2baa55c56aee3bd97c06365fe9b045f3c5066ad56" + "captureGroups": 4, + "digest": "b99c3406b4b8555cc521b27bab98eefed02a121dc2e878dd4f63133088603dc5" }, "python-calls/service.py": { "captureGroups": 6, @@ -112,16 +112,16 @@ "digest": "abc26d2f53c4dc0f091009b120e7869ca74b015bad637c5643fc4fdd69df946e" }, "python-chain-call/models/repo.py": { - "captureGroups": 6, - "digest": "54e050e7cecc288e367e97dc59280fd4febb614c7099894591e122901261a130" + "captureGroups": 7, + "digest": "abc5c9d130a440235eaeea7fcb8fc01fe1f93a1eb548083ccba781a5dd143d16" }, "python-chain-call/models/user.py": { - "captureGroups": 6, - "digest": "3e5160418c4e3b4c0a896bed89e731711aff3a66872d99bb5ebfcbd624d9a3d2" + "captureGroups": 7, + "digest": "e023fa6d98558619733e75005e4c6dc4079967b4d43c2fc2ecd24136a4de6e1d" }, "python-chain-call/service.py": { - "captureGroups": 9, - "digest": "250f907deb4fec54761559db26f0938dc0accee480021cf95bcdd5167d33096f" + "captureGroups": 10, + "digest": "02ab55c19bc43c76da1b54e90e9176b6b0792438fd150dfcc4fd77f8452dd124" }, "python-child-extends-parent/app.py": { "captureGroups": 9, @@ -132,32 +132,32 @@ "digest": "d118691eb76c9432841743efee8556f1e7a1d136e9b403a12fd512f91d73ca61" }, "python-child-extends-parent/parent.py": { - "captureGroups": 7, - "digest": "2f30a106769eeda283491fee883b448cd4bbcb5a8be2a587e7c72e2dca2a03f1" + "captureGroups": 8, + "digest": "8d56bb790ffb79c2c211799a331d343cef557f15e180bc5ed65c3a575280436e" }, "python-class-annotations/repo.py": { - "captureGroups": 8, - "digest": "f53cefad4e2dd9951fe34eea13822f5c521d9a23c3185b559b390ada7c5e1f3d" + "captureGroups": 9, + "digest": "a35d836a4f2de8d78362cb72b81ac4d0918c11b78c0649317b12693b43590644" }, "python-class-annotations/service.py": { "captureGroups": 11, "digest": "0b20f98ba8b035c4e065bcfa66df9c955ba98608ea7aece8b998cc62758e5d85" }, "python-class-annotations/user.py": { - "captureGroups": 8, - "digest": "38e7b4fe8372de734b28227c926d0bc80f66d2539b7f8d0cc22c1d32bf69821b" + "captureGroups": 9, + "digest": "23419a3c41d655554aa7bdf47f9c95749c3c83788ba70b5f83bdc2a93c110d1a" }, "python-class-attr-export-leak/app.py": { "captureGroups": 9, "digest": "e958f924b72ac35d92cb3a959c4bea866fa1020347e96a09e07b7087ce9a231c" }, "python-class-attr-export-leak/mod.py": { - "captureGroups": 11, - "digest": "cf175a5a7b577bbf8e76acc004c5360f496d0a10cc5de0a3e31a212bd413b491" + "captureGroups": 12, + "digest": "1c11a05121d348a540b07761e0bb3c0e56898115bba2d6e64eab9924ed84743b" }, "python-class-body-namespace-import/app.py": { - "captureGroups": 9, - "digest": "58843947b979524df15f256d240b789ac67356499013b9c96bcc9e14108f8eff" + "captureGroups": 10, + "digest": "ed0aa5fd5e211b41d541e28b3ebfce22e49e9fd482184a68ab16db835c36ae3d" }, "python-class-body-namespace-import/mod.py": { "captureGroups": 4, @@ -168,36 +168,36 @@ "digest": "e8621bf951ab14634a8ed38a5953ff59434d3406d59e9c16751b6ab9cc40e7e9" }, "python-constructor-calls/models.py": { - "captureGroups": 11, - "digest": "3508b40afee5ce512fd9057be671629b8a122c88342ab358cbf69ac418a39e34" + "captureGroups": 15, + "digest": "201ce01b83b21d729aca89c6299570df55393a95f1899a2eaacd989873950177" }, "python-constructor-type-inference/models/repo.py": { - "captureGroups": 12, - "digest": "a076029a7a594b3577cbe3017c60cbd8f376ae80bd1e5216b0469b165f1c9f76" + "captureGroups": 16, + "digest": "ad11823ee187cc3e1efab34a67a5013119b4ada87c5701b080921c0b0be09e62" }, "python-constructor-type-inference/models/user.py": { - "captureGroups": 12, - "digest": "a7f11709f76dcc9311554b4fe1dd4e95f5d6434e6232627b4c370f21e194bd69" + "captureGroups": 16, + "digest": "cbbb5168c28123820a70fe24016b0ed26ab02a6339d21ffe40fbccad940c1d70" }, "python-constructor-type-inference/services/app.py": { "captureGroups": 13, "digest": "5306b0960c7b88572e81aa5b203f7e10adbd5d6c8b4c54fc78314624d5864836" }, "python-default-params/app.py": { - "captureGroups": 17, - "digest": "408a601fac98bb34b153210a3513e97a5d6d0537f5b02d01fffb4525d24395a3" + "captureGroups": 21, + "digest": "445664a07fde4405c6ad43cdcbde1ed0a634865c92183908454e2622166a94e2" }, "python-dict-items-loop/app.py": { - "captureGroups": 8, - "digest": "98486424c256def8b89730644deb829ac1c1fc5c46d8f86e586ed2d8ab1a07b8" + "captureGroups": 9, + "digest": "dd51c32d705934b1384991ad2291869f446327752481abc20600d4ad9f553ea3" }, "python-dict-items-loop/repo.py": { - "captureGroups": 11, - "digest": "a1f088dbd281662c7b0ba9797d07edaef8a2e5a82a6715242eb3b70c29db86aa" + "captureGroups": 15, + "digest": "8116cf4cbf4dca377e88f97ca645f40fab648a4e9a8e790b5b3761e4a3e17d7c" }, "python-dict-items-loop/user.py": { - "captureGroups": 11, - "digest": "e9398cffc0a70490a3b5f90e3436292e306df36ee51955741d8ee91aa37b2e73" + "captureGroups": 15, + "digest": "15984fa30be4603f3e47c27342352dd602d104b33ac5224dc911d78a82d87926" }, "python-django-app-imports/accounts/__init__.py": { "captureGroups": 0, @@ -244,8 +244,8 @@ "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, "python-django-app-imports/billing/models.py": { - "captureGroups": 12, - "digest": "eedb2e1992f85a6743555947118784db9c78f3c113d2d598e53dd54cb80a0629" + "captureGroups": 14, + "digest": "9e85e8a35a603ceb7c7e108d0190b1a49fc5fc301a2fb6da2df8a02d4a1833d4" }, "python-django-app-imports/billing/tests.py": { "captureGroups": 2, @@ -264,68 +264,68 @@ "digest": "79729019cda1dd6f41848365b98416fc610aaa846464bed569573c1fde0892a3" }, "python-django-app-imports/config/settings.py": { - "captureGroups": 20, - "digest": "776f9193ac3a1927c8b0900efceb5ac3f16400ddfa7fa5d9617153d0bcf6ea0f" + "captureGroups": 21, + "digest": "78bf737a725c170daaf7d6affc4bfb648600afeedca1d1ecf135952a8b8b3794" }, "python-django-app-imports/config/urls.py": { - "captureGroups": 5, - "digest": "fb7fd5c6bac541e180e7f90bed248eeac937bc0cec7c2d8405f7f9bb20e87840" + "captureGroups": 6, + "digest": "3772e1a255d14196be0d4db2052bd34c4f2683b151550e9f3379011a93aa1718" }, "python-django-app-imports/config/wsgi.py": { "captureGroups": 7, "digest": "2c57f45963ef158d2ad6a504c5d09fb4f3aa53d6198b91e91f8f824aae1de452" }, "python-django-app-imports/manage.py": { - "captureGroups": 10, - "digest": "ba770ed72ce287e7739d1b3ec4165053bfd8aa49f9104682d1a7094c724e3039" + "captureGroups": 11, + "digest": "c5d3eb457808eb9c17502c751cd045dc89c469fb8e308498d1c21a6da2fcafa3" }, "python-enumerate-loop/app.py": { - "captureGroups": 23, - "digest": "1f528f2084d1f6da954ec8ce7eef0e45f99a0b3ae58502adc262a6caee5aa661" + "captureGroups": 27, + "digest": "c76272c32fb41cfdc82784dc9d3e3f302cbb25df68e7d18cc7240c8e1c8c7b49" }, "python-enumerate-loop/repo.py": { - "captureGroups": 7, - "digest": "1d6eb1cdc661f2463d8e1a499eaa5bfcd9367324f6090c44fe4d59ed02151215" + "captureGroups": 8, + "digest": "d1e23831dcae38034b278bfefa2b8c4e21ca722ab2c79bfb3126744338a2a401" }, "python-enumerate-loop/user.py": { - "captureGroups": 12, - "digest": "a7f11709f76dcc9311554b4fe1dd4e95f5d6434e6232627b4c370f21e194bd69" + "captureGroups": 16, + "digest": "cbbb5168c28123820a70fe24016b0ed26ab02a6339d21ffe40fbccad940c1d70" }, "python-field-type-disambig/address.py": { - "captureGroups": 8, - "digest": "03076501a92927ff778f3dbcb555aa59e40fc21b1fe367147fd50cc330637a92" + "captureGroups": 9, + "digest": "ea626af0637cfe9dbbf908917128a31375734743aab1e8b1b098cf0748aa7d1c" }, "python-field-type-disambig/service.py": { - "captureGroups": 6, - "digest": "caabe7eb61e0393151c362f3ec18a38b1d23a77176c67814c41ed6a2cfa54005" + "captureGroups": 7, + "digest": "ebe4ba05a566f775df15b9eff96980b496068c8b5504036b41de29b203aeb755" }, "python-field-type-disambig/user.py": { - "captureGroups": 11, - "digest": "d71983e74eb3c6ea73b4c8aa6457a6e6d6383538d5e5757aea9fcf18bcbe560b" + "captureGroups": 12, + "digest": "6faddc1fcc2f2ea3a78282fc3419ba8e5203f646555df335861f160ffc2c6d53" }, "python-field-types/models.py": { - "captureGroups": 18, - "digest": "d1cf0a8e91042726967b6671ea720f2ddb57c5cfeb917cd942096ed0cb8bb0e8" + "captureGroups": 20, + "digest": "9b82907071315cb07aa6d4c97df8613d51f7371292afa124aa6338f90c204787" }, "python-field-types/service.py": { - "captureGroups": 6, - "digest": "ad1c6a054f610dc609f3ec963746d49be87567d1bec643c811563bdcedc4469d" + "captureGroups": 7, + "digest": "03cf02243ec15b64694c0f5b33b80daf1689049123f91ade18ae9557aff1b028" }, "python-for-call-expr/main.py": { "captureGroups": 15, "digest": "5c290b3b34f3f5e9dcdd6ee3ae72ba4223337cd64592330f9a8c6c6f13b2fd2d" }, "python-for-call-expr/models.py": { - "captureGroups": 31, - "digest": "f809317e086279962b81038b7ef2053bd3bc96d35bb1dd84a1ad543bc8dfe068" + "captureGroups": 39, + "digest": "133a14c0543a41412d9d4fd5485d6270e1f4b0cd247f0ec0d71974850e4918c1" }, "python-function-local-import-chain/app.py": { "captureGroups": 9, "digest": "ad4d45976ca10c3fc3b7bf498ee23397797368a5d2817311d0e95e943af89199" }, "python-function-local-import-chain/svc.py": { - "captureGroups": 11, - "digest": "955591b75260b260c00d4ab4918c16ac344d23b5fffa9c47fbbacdc56c67ae47" + "captureGroups": 12, + "digest": "5463c3e1d3159af3d37f1e0341ea0a429c6f54cf99e117a466b210df05fe94ec" }, "python-function-local-namespace-import/app.py": { "captureGroups": 9, @@ -344,8 +344,8 @@ "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, "python-grandparent-resolution/models/a.py": { - "captureGroups": 9, - "digest": "5879a42d6655248623c9aee193fedcae51daa1887d8e597fdd944ad93af053a4" + "captureGroups": 10, + "digest": "c2a76b06792f8594c3734bf521cc192ecf18648eff96191ba78df6434915adf0" }, "python-grandparent-resolution/models/b.py": { "captureGroups": 5, @@ -356,28 +356,28 @@ "digest": "2ad9124422ca018e59855c55a054d5c37d15b448a90027842fc56dd6f61e4593" }, "python-grandparent-resolution/models/greeting.py": { - "captureGroups": 7, - "digest": "90a9349f89eb7e7c76123c6b235d510a9f151c9cd7e0db07bd8ae9b99f38ee93" + "captureGroups": 8, + "digest": "6ce8f7f1b5ccd7951765eee06a538d2fe791f7ca955230fc8041b2cf46b9ca8d" }, "python-local-shadow/app.py": { - "captureGroups": 8, - "digest": "b71ed90838b5898310a80843486871ebeb6e1f63d24e0565d4be947c7bb66dc4" + "captureGroups": 9, + "digest": "68c3ec5904bb2e2613c0b008a85e4bc598168a53fe7ae409bfc187d1f06373eb" }, "python-local-shadow/utils.py": { - "captureGroups": 4, - "digest": "c11f7883ea696f30d188f8b8316cfd28746a10b4c7b898807ae21c67a8644ee5" + "captureGroups": 5, + "digest": "287289abcacd9dd0f3763632fe57579567df01b73ea4b48e68c59e2f8aded3da" }, "python-match-case/app.py": { - "captureGroups": 7, - "digest": "9c1f82b03516fbb9e111f1abcc56fd329bbfe290d3e9c0f31e9c8a84d715a031" + "captureGroups": 8, + "digest": "d4155b08b28018e2d290923f93b197290f72ef4f2d026d11ff4d5b35d682c2a3" }, "python-match-case/models/repo.py": { - "captureGroups": 6, - "digest": "54e050e7cecc288e367e97dc59280fd4febb614c7099894591e122901261a130" + "captureGroups": 7, + "digest": "abc5c9d130a440235eaeea7fcb8fc01fe1f93a1eb548083ccba781a5dd143d16" }, "python-match-case/models/user.py": { - "captureGroups": 6, - "digest": "3e5160418c4e3b4c0a896bed89e731711aff3a66872d99bb5ebfcbd624d9a3d2" + "captureGroups": 7, + "digest": "e023fa6d98558619733e75005e4c6dc4079967b4d43c2fc2ecd24136a4de6e1d" }, "python-mcp-tools/helper1.py": { "captureGroups": 3, @@ -436,64 +436,64 @@ "digest": "25270485caa12492922ede0e4a2ed1e898f7d7862641e89cbc25ff0d31f77f18" }, "python-mcp-tools/server.py": { - "captureGroups": 36, - "digest": "b65b1821625ca03577a06e5c90204a1513706f841386f8806310cd41eb67b9c1" + "captureGroups": 46, + "digest": "d37ba082866e1ba64ee27fd04ea3c6a7e6ac63b77900b1744caed57f9c31556b" }, "python-member-access-for-loop/app.py": { - "captureGroups": 22, - "digest": "ec36378bb69d54cbe0d0a130e90f4122b7bc80191ec09e1af4437abca2d43ee6" + "captureGroups": 26, + "digest": "fe44c7af9a0e711f1d8cd448262f96e499a3feb759d20b148470c1f0ce62c4d4" }, "python-member-access-for-loop/models/repo.py": { - "captureGroups": 6, - "digest": "54e050e7cecc288e367e97dc59280fd4febb614c7099894591e122901261a130" + "captureGroups": 7, + "digest": "abc5c9d130a440235eaeea7fcb8fc01fe1f93a1eb548083ccba781a5dd143d16" }, "python-member-access-for-loop/models/user.py": { - "captureGroups": 6, - "digest": "3e5160418c4e3b4c0a896bed89e731711aff3a66872d99bb5ebfcbd624d9a3d2" + "captureGroups": 7, + "digest": "e023fa6d98558619733e75005e4c6dc4079967b4d43c2fc2ecd24136a4de6e1d" }, "python-member-calls/app.py": { "captureGroups": 8, "digest": "3d5282754e81dc800cf1bf11b414e7986b0133ae6545f242030002ececab4840" }, "python-member-calls/user.py": { - "captureGroups": 9, - "digest": "485f993536f52252ad9b06971e99adb391fa95d005217c3563e41c48359fdf56" + "captureGroups": 12, + "digest": "19f6f21b1d5d25dc3e1653537e986f65fdd3697efd48affa9325fe2ecffe188d" }, "python-method-chain-binding/app.py": { "captureGroups": 19, "digest": "01fe4805f59723a5f163d26b7be3ed3e456eb3a093e3df3a8f034cecee22ebb9" }, "python-method-chain-binding/models.py": { - "captureGroups": 34, - "digest": "062be597e84f6949477e56194ea5e875a6e1b8870e1af993d8c549d6f3de2d39" + "captureGroups": 45, + "digest": "ea3f745514a330d86447796734faff29f0c88ea49a7ef8781087c537426d29bf" }, "python-method-enrichment/app.py": { "captureGroups": 13, "digest": "88dfd417951b8083184f83da8c1e0c2b19700cfbf1f1ff203a904ebc00f50b30" }, "python-method-enrichment/models.py": { - "captureGroups": 27, - "digest": "c9f6e1678d8976665d7c7c3b1c649ff60ea5a4f397cf2fef2a559ccf4b5f08e3" + "captureGroups": 31, + "digest": "0645e71e2b604c65e2621ea00e19068ab4d3718f65e4f2900fcac187ae877b2c" }, "python-module-export-vs-method-collision/app.py": { "captureGroups": 14, "digest": "d37707fc868b7b086fd2534356cef6f96d42f0ea4691847858d7a5ad03eabe34" }, "python-module-export-vs-method-collision/mod.py": { - "captureGroups": 11, - "digest": "75158050a37ac0f4aa7427f87bfa1bb4d85933b92d023c9448d1fd21a787a3c3" + "captureGroups": 13, + "digest": "d3032a370833cf67e68672de0ec85b199756f43984488f93a37de81501cac1c1" }, "python-module-import/app.py": { "captureGroups": 15, "digest": "29560a379fc78162c760c142019d480ddbdd56e807da5831bc39d2c559136f3e" }, "python-module-import/auth.py": { - "captureGroups": 11, - "digest": "0b99a5f6add0359f530c27c82f14ceb884849b24461870b8b517ab38a2714ae1" + "captureGroups": 13, + "digest": "21f44ba75e592c14a94cd7820cfb04d2ccba7a9c97c40a778c6d727f286f8665" }, "python-module-import/models.py": { - "captureGroups": 6, - "digest": "3e5160418c4e3b4c0a896bed89e731711aff3a66872d99bb5ebfcbd624d9a3d2" + "captureGroups": 7, + "digest": "e023fa6d98558619733e75005e4c6dc4079967b4d43c2fc2ecd24136a4de6e1d" }, "python-multi-level-mro/app.py": { "captureGroups": 9, @@ -504,16 +504,16 @@ "digest": "d118691eb76c9432841743efee8556f1e7a1d136e9b403a12fd512f91d73ca61" }, "python-multi-level-mro/grandparent.py": { - "captureGroups": 7, - "digest": "f9f81d3a37c55b3e23bec3774405920afa29c5793c46860a98a06c5d0c7f0980" + "captureGroups": 8, + "digest": "fdb504c509c49a276524145bd2f5a9946f2e8ef8ea07be166abe5a3c15b748a0" }, "python-multi-level-mro/parent.py": { "captureGroups": 5, "digest": "b68bfb8fdedb8f725c609264e604ccb674a5a775c0d87c008a9990234c70bde3" }, "python-multi-segment-ancestor-import/backend/auth_utils.py": { - "captureGroups": 6, - "digest": "45f881e9deb28e29d912924a6d108378cc7f5046ed84b0302d9e4b36c5085f8c" + "captureGroups": 8, + "digest": "cb8c5013202cea31bd75d05907e2d45087060fa9b00c0983127701012c3dc6d2" }, "python-multi-segment-ancestor-import/backend/routers/__init__.py": { "captureGroups": 0, @@ -532,112 +532,112 @@ "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, "python-multi-segment-ancestor-import/backend/services/alerts.py": { - "captureGroups": 3, - "digest": "bcbe9f5923dda8b54c1cc1a07c38439ce93d5ce13101775c20ff6e4de9afb404" + "captureGroups": 4, + "digest": "a0c37af99021c9f0f08c7f80c89b8126581a78f3595a9dd54fe9b0de9e95723c" }, "python-multi-segment-ancestor-import/backend/services/sync.py": { - "captureGroups": 5, - "digest": "3c8bb78fafe4c0797d9d1ca162911d96f7b6fd3a5a037c26952b7f0287610150" + "captureGroups": 7, + "digest": "c247e0961ca943badd8407134d12f9d0760020336bcd14b462500bcbbe2ca078" }, "python-named-imports/app.py": { "captureGroups": 5, "digest": "179248a29a48abba318bece3fe6eeca436db9544126684e203ad27983dc41695" }, "python-named-imports/format_prefix.py": { - "captureGroups": 3, - "digest": "199776174b6aa80ea46969db4b427dbd4d6be0d4d6a96d62f05d984556a2b09f" + "captureGroups": 5, + "digest": "7f321a84964a7f3c1c3612150331b94e635f40bac021d4b5e407be36b4740188" }, "python-named-imports/format_upper.py": { - "captureGroups": 4, - "digest": "3f0a4801427bc126dff1d700a91d9133f3e59d3db4db26eccfb3276258cb4fea" + "captureGroups": 5, + "digest": "df334f13bf7c81ba01c861c2b9a4ddc969645ccecbd536088c3556a2a2fb1f2b" }, "python-nullable-chain/app.py": { - "captureGroups": 31, - "digest": "d5a8af8929a21808c3570cdc175b8412a5925b41cdace728ffc0179cdb2bc484" + "captureGroups": 33, + "digest": "b27470a2f2dc030240c2f48010e489278e95859db80e8003745334ab32f0d455" }, "python-nullable-chain/repo.py": { - "captureGroups": 7, - "digest": "8c79e31ea4557abf29e4e1ec087db258e48afa3f9cc1e637291d0d5d49914980" + "captureGroups": 8, + "digest": "12ae0c4f238838d13501844676855952c54c47a0fe5eca937355e918e1f041b1" }, "python-nullable-chain/user.py": { - "captureGroups": 7, - "digest": "967ff5732d066c516ad0bfddd87696598ab9c2c4b55c4463641db14af4cdfb78" + "captureGroups": 8, + "digest": "72f02563ba72a1a1a595623a81341c694124054698afeb606e65662915b66a22" }, "python-nullable-receiver/app.py": { "captureGroups": 23, "digest": "7f22953242a4402b31ebcd6219d4976fd706c2775ef60d85f0911900e08cd647" }, "python-nullable-receiver/repo.py": { - "captureGroups": 6, - "digest": "0d2704cfa14e431395973cc82b582f9fae43a8ce67d5548ab5df62b3fa8d1b7e" + "captureGroups": 7, + "digest": "29bd91eac43a803bb4afd96ee2d733f62a8c7a56c02cbca79e3d5394ff69ecb6" }, "python-nullable-receiver/user.py": { - "captureGroups": 6, - "digest": "4dd0a9d0f5797bae1c281a07145ddcc4162eae1ea16208e85edb74cd98e085f9" + "captureGroups": 7, + "digest": "3fa157d0e0576fe87c6abf5ce9a3509c82c04b2ecf1fb6d53290ddc1326af8e4" }, "python-overload-dispatch/app.py": { "captureGroups": 21, "digest": "37fa5251fc1914d95fab2c6aae6d97b460a47f46c5b74415a86c955f4ed78626" }, "python-overload-dispatch/service.py": { - "captureGroups": 28, - "digest": "a873c2cebeb1092fe21b75b2109052fbb66335a378dd5468b32a57a4644ad14b" + "captureGroups": 37, + "digest": "b5bbb6744e86850bbea1afe2e7b38f4e14e01b27bcf68777695dffd8065623bb" }, "python-parent-resolution/models/__init__.py": { "captureGroups": 0, "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, "python-parent-resolution/models/base.py": { - "captureGroups": 7, - "digest": "f0384bd6ecb7d1a9ad2306358917b7295c71ea8f1bcea818fd39c56d2c28c7e9" + "captureGroups": 8, + "digest": "3e885e626c46c7c4b9779867991a939f136f3a5669f092094c3ef33542ddf7f1" }, "python-parent-resolution/models/user.py": { - "captureGroups": 9, - "digest": "b06a66a108097eec9427a028dec38bbab284918ac39344e86e58bba89533c530" + "captureGroups": 10, + "digest": "4c239f6abff4d91c4db07436c0d23a6012dd38f2b900c184e06da38237a3e776" }, "python-parsing-coverage/heritage.py": { "captureGroups": 21, "digest": "dc7df1006ed20c6f7fd17799eb090ce2ebdd4d064384949d612835f5f05a9158" }, "python-pkg/models/base.py": { - "captureGroups": 9, - "digest": "4984ee01b7a9fefe622195f0e4925823c0e62a1714ca2dda5cd8250e5e45fa7c" + "captureGroups": 11, + "digest": "53dd613c8d7cd9e20ec44919455741b0cccea5e32efc907680c8078b16c7bf2d" }, "python-pkg/models/user.py": { - "captureGroups": 8, - "digest": "9ee707b36f42a635fdb867ce20359b5f51ac4e13550cde801359dc8314e01a77" + "captureGroups": 9, + "digest": "d708868d257b88964e4b44646f0870363000985725f86a3ecd2c2b063b42b07c" }, "python-pkg/services/auth.py": { - "captureGroups": 9, - "digest": "3c9e44e7870643e475a67f562c15a42d58ade2b47a6c43ad665ef73e53e765ff" + "captureGroups": 11, + "digest": "6fc0ba60b6b2b9ad5825ab0109fb0de979937d06316983b30226e1b788d5bcc5" }, "python-pkg/utils/helpers.py": { - "captureGroups": 7, - "digest": "9a12c0b8c2523d4dfd5919e04cfe2bf4e9f6b84fe1a83a146f58996b7d453bb5" + "captureGroups": 8, + "digest": "c3b1fa72e2d2bb8b8e2398b0d79c4b0a660949efbde7a1303bf5d2811a57f1e6" }, "python-plain-import-alias/app.py": { "captureGroups": 17, "digest": "a419c716b0d13787ce1f3cc2adb5ab2ab77604cb8348a9b27aa56443984ab0b2" }, "python-plain-import-alias/auth.py": { - "captureGroups": 6, - "digest": "51a9f92e1dae910b08b86f506c174ad2f60dfc1d2d5f66d22838978d7d424b48" + "captureGroups": 7, + "digest": "d8be5670ece6958c45dc48149f1bf0a826adf285262ab93745e34e61fe80e60b" }, "python-plain-import-alias/models.py": { - "captureGroups": 11, - "digest": "abf5fcdf7cc473efa5a149319b0fc3d14b3ae91a2e9a14d621243c5ed7fafafa" + "captureGroups": 13, + "digest": "cec30bb7577edfb751bcd4f5026a0c9d77ed235071421a2a46f87580c7bcdf47" }, "python-qualified-base/a/__init__.py": { "captureGroups": 0, "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, "python-qualified-base/a/b.py": { - "captureGroups": 7, - "digest": "725adf1074b272c8d2ce664549106b07286a92cfb08ab4d605068102317f8baa" + "captureGroups": 8, + "digest": "2c3fdd9f12bcb1dfc9165b65d4dc4768519a26a1bcfa7ec8cacc102d7ba1e069" }, "python-qualified-base/base_mod.py": { - "captureGroups": 12, - "digest": "7fc34dae23f54cdee030a2d4a1d80c0bf226ea3b37335ecf48551c4707d32e20" + "captureGroups": 14, + "digest": "e3033d124fafff6e4bf7f62307359f2070c4fc876d3a986a8476397975aa98ba" }, "python-qualified-base/service.py": { "captureGroups": 16, @@ -648,20 +648,20 @@ "digest": "9e4d51c06d75721e3c07898124ef85139d52d9f20d6df6bae557aff98a3ed207" }, "python-qualified-constructor/models.py": { - "captureGroups": 13, - "digest": "94e738bd516d6e1fa1ca130fc231157071c04aa3b2ad74db6dabbacd7b7842a7" + "captureGroups": 18, + "digest": "6b350aa1579d58e036053a16e8e94b91d8a8f6906f78d4f44ef675887a48bf5d" }, "python-receiver-resolution/app.py": { "captureGroups": 15, "digest": "806934204693a06760f3c74e8c68145eda5cf19d7903130646e152ea14fc7a98" }, "python-receiver-resolution/repo.py": { - "captureGroups": 6, - "digest": "0d2704cfa14e431395973cc82b582f9fae43a8ce67d5548ab5df62b3fa8d1b7e" + "captureGroups": 7, + "digest": "29bd91eac43a803bb4afd96ee2d733f62a8c7a56c02cbca79e3d5394ff69ecb6" }, "python-receiver-resolution/user.py": { - "captureGroups": 6, - "digest": "4dd0a9d0f5797bae1c281a07145ddcc4162eae1ea16208e85edb74cd98e085f9" + "captureGroups": 7, + "digest": "3fa157d0e0576fe87c6abf5ce9a3509c82c04b2ecf1fb6d53290ddc1326af8e4" }, "python-reexport-chain/app.py": { "captureGroups": 13, @@ -672,107 +672,107 @@ "digest": "ee0d8506fbaaf9cbf3a2f78bd34b7e688eb01aed590408cc2d6c59a4ee3bb4d8" }, "python-reexport-chain/models/base.py": { - "captureGroups": 13, - "digest": "4ea6fa3504e37231c8d395259009df5d4adda31e3ea0bfc28cd3c3eabdd68280" + "captureGroups": 15, + "digest": "e1e9f98ffb690ffabecd7287bf7f640e3fa53ecdda37dba41692b6914a37c591" }, "python-return-type-inference/app.py": { "captureGroups": 8, "digest": "741f690b6330491303b9b58cb31027a33600973265b59428facfefbabf0cf7e1" }, "python-return-type-inference/models.py": { - "captureGroups": 12, - "digest": "a7f11709f76dcc9311554b4fe1dd4e95f5d6434e6232627b4c370f21e194bd69" + "captureGroups": 16, + "digest": "cbbb5168c28123820a70fe24016b0ed26ab02a6339d21ffe40fbccad940c1d70" }, "python-return-type-inference/service.py": { - "captureGroups": 7, - "digest": "81f8d5783bfc95a9f83f02abd15f7bbc0869aaca964c62b40653cc05ab73ebcb" + "captureGroups": 9, + "digest": "40bdb4b59d0540330fcb861dca2f2b3fdb432e529af326ffc692cf69201f0af5" }, "python-same-file-method-collision/app.py": { "captureGroups": 17, "digest": "cd1e6bd1cea7de20c9317fa650bb0b3492df81d736d24142a0a3f082834c507a" }, "python-same-file-method-collision/models.py": { - "captureGroups": 21, - "digest": "3c5ef9ae13442f9c77a61c4fc6b76ebc580cdd5857810cc484f6cb768fa9d727" + "captureGroups": 25, + "digest": "b892bec2982f22ac15e5db0decc934be3a59464961fdde657fb1a80110df074e" }, "python-same-name-collision/metrics.py": { - "captureGroups": 7, - "digest": "57482d976bc3d4f232064b55f809507a9e00ec4f75ee519691034227c666312e" + "captureGroups": 8, + "digest": "5bede48bac9564a854a5e6073b9b099c54da7ae4ab44e33ce8b337125326fd67" }, "python-same-name-collision/router.py": { "captureGroups": 10, "digest": "e9248113f2dbae22dff443551daa131136c69f00ea0fb912a6419fcb1c1e7833" }, "python-self-this-resolution/models/repo.py": { - "captureGroups": 7, - "digest": "1d6eb1cdc661f2463d8e1a499eaa5bfcd9367324f6090c44fe4d59ed02151215" + "captureGroups": 8, + "digest": "d1e23831dcae38034b278bfefa2b8c4e21ca722ab2c79bfb3126744338a2a401" }, "python-self-this-resolution/models/user.py": { - "captureGroups": 12, - "digest": "b23ad4122f7fdb5b3f9b047582229a9ea001a29452035974112d4f425ef86cf4" + "captureGroups": 14, + "digest": "c7ed155a14a8ca915ec5db84e5eed21d783a3a8ef8ed6a74a456cfe7456eac09" }, "python-static-class-methods/app.py": { "captureGroups": 14, "digest": "b397708c05d101d0d3978313f65184c9f181a179b803d242b1e585ac2605fe4a" }, "python-static-class-methods/service.py": { - "captureGroups": 32, - "digest": "91730faa59b93ee5a06c8f7050831d32f39ad89c624cb665a4eeb38449aa2c3e" + "captureGroups": 39, + "digest": "0c734dc7f683469f9c1a9eb97fd8be2af68e29aedd20cd8c6f0c86c17f7ce13e" }, "python-super-resolution/models/__init__.py": { "captureGroups": 0, "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, "python-super-resolution/models/base.py": { - "captureGroups": 7, - "digest": "f0384bd6ecb7d1a9ad2306358917b7295c71ea8f1bcea818fd39c56d2c28c7e9" + "captureGroups": 8, + "digest": "3e885e626c46c7c4b9779867991a939f136f3a5669f092094c3ef33542ddf7f1" }, "python-super-resolution/models/repo.py": { - "captureGroups": 7, - "digest": "1d6eb1cdc661f2463d8e1a499eaa5bfcd9367324f6090c44fe4d59ed02151215" + "captureGroups": 8, + "digest": "d1e23831dcae38034b278bfefa2b8c4e21ca722ab2c79bfb3126744338a2a401" }, "python-super-resolution/models/user.py": { - "captureGroups": 11, - "digest": "8a66f8962fcf960b66c1106d1d67da21f7f6bac323b961e5fc0623b3c93dbc38" + "captureGroups": 12, + "digest": "fbd079ff1c9d18b88c3ab32ff58c3ffefecb40e75fbb6fd5e7a21049490dfe5c" }, "python-variadic-resolution/app.py": { "captureGroups": 5, "digest": "62b16647599de4db600ece073131025de23bf692d1585d8b7f1bfc3fae2f3dcb" }, "python-variadic-resolution/logger.py": { - "captureGroups": 5, - "digest": "5995f1a01f8354c3d854d4f4dc51fb4a2919bdcb22e90eb0a64431eb03e5804c" + "captureGroups": 7, + "digest": "7d460bb9d28620ce178db652e15427a7cf7839cdf9ef651a9c30209ed3d22d82" }, "python-walrus-chain/app.py": { - "captureGroups": 33, - "digest": "07590672df6cd4d1069ec346033fa29c116d42c841ec68da8d22afd44effb938" + "captureGroups": 37, + "digest": "6453e4257122110a3efc9a868dcdc6c08dbe614370f59cd66bd2356fb9220bf9" }, "python-walrus-chain/repo.py": { - "captureGroups": 7, - "digest": "8c79e31ea4557abf29e4e1ec087db258e48afa3f9cc1e637291d0d5d49914980" + "captureGroups": 8, + "digest": "12ae0c4f238838d13501844676855952c54c47a0fe5eca937355e918e1f041b1" }, "python-walrus-chain/user.py": { - "captureGroups": 7, - "digest": "967ff5732d066c516ad0bfddd87696598ab9c2c4b55c4463641db14af4cdfb78" + "captureGroups": 8, + "digest": "72f02563ba72a1a1a595623a81341c694124054698afeb606e65662915b66a22" }, "python-walrus-operator/main.py": { "captureGroups": 7, "digest": "4df7ea089c43552ca4ea5a51f8e985d11d351b86949efb2f7ebefdf6a9ffd689" }, "python-walrus-operator/models.py": { - "captureGroups": 16, - "digest": "ccd7c8c00bc2fe3bd2bad667e2448591c3508e9eb91a0005a9148a2d3734ac98" + "captureGroups": 21, + "digest": "cf014d1bad66ea61e327c146fd1a52159a96faaf264555bb164230a5218e6948" }, "python-write-access/models.py": { "captureGroups": 11, "digest": "d1b714cb6654f83df3bb40830ced44f2950a122270c9e379f732943d57688151" }, "python-write-access/service.py": { - "captureGroups": 9, - "digest": "0aa940e3428c35b1324544e9bcc75c166732b63b3e65e6faf460bc3d3a9571ec" + "captureGroups": 10, + "digest": "6e3690ec68d8de54f376bb6f5f7a29da829a8a6a24001eabae9ec66a327f3409" }, "synthetic:dao-20": { - "captureGroups": 493, - "digest": "3ef7932339801c2cfa24609b741b472bed2f1037b413edafaadf8b8c2edd8f7c" + "captureGroups": 733, + "digest": "c540f2143882137e6c6f7996bf9b87a0117f41915b1d0989d3d6bb9db5a5a1ab" } } diff --git a/gitnexus/test/fixtures/ruby-captures-golden/expected-captures.json b/gitnexus/test/fixtures/ruby-captures-golden/expected-captures.json index cf587fbd1..da6ecd5a1 100644 --- a/gitnexus/test/fixtures/ruby-captures-golden/expected-captures.json +++ b/gitnexus/test/fixtures/ruby-captures-golden/expected-captures.json @@ -36,20 +36,20 @@ "digest": "6567c827276575ef3a087757b5725e381fbedb421ffecfe6c5904d7d62f6bcd4" }, "ruby-call-result-binding/app.rb": { - "captureGroups": 17, - "digest": "42a4ffb39fa090608cb0f60e8c31d2c51c7515a4ee6ae038e9accbac0d2e5488" + "captureGroups": 18, + "digest": "194c1ca21a7d5d8d85cf9aecfdc4edd881c5d3447de1ad574b56cff5a411bab1" }, "ruby-calls/lib/one_arg.rb": { - "captureGroups": 7, - "digest": "a04d2c55b5738a824bfa4139925cade1d437bf0a1a91ebf044c5098e2e5f9b58" + "captureGroups": 8, + "digest": "61300083e7a2e6d9a116c3975ddd3b78a593a9a0e397c252bedb160d481c20b7" }, "ruby-calls/lib/service.rb": { "captureGroups": 11, "digest": "7a4d964547d3ac645274c65fcbceba3f6852286537af2452c87d75bbde3495ab" }, "ruby-calls/lib/two_args.rb": { - "captureGroups": 7, - "digest": "533578cb1111c137d4cc660d1667ba43f649799a9fbd6139a20ac389d16477bf" + "captureGroups": 9, + "digest": "0b781a930116eed742de7bfbc0852ecdf4056d5ccc4f49c2b779da49c5ca04e9" }, "ruby-chain-call/lib/app.rb": { "captureGroups": 13, @@ -112,16 +112,16 @@ "digest": "afa655d9a3c7ebea02b529d33904575692a53531a619c3fd34634f0bb43855e4" }, "ruby-default-params/app.rb": { - "captureGroups": 7, - "digest": "84779fa164cbc83863f6ae1552ca8ed19f21a18cb410b26ea581132c21d88a6a" + "captureGroups": 9, + "digest": "5724292fa04411256cc8c43c0bd3b3a58be5b5e457fdead417da379199a18e31" }, "ruby-field-type-disambig/address.rb": { "captureGroups": 8, "digest": "cfb08d28049718393f211454b0c0148aaa82d7093915f0d8650a224d8ff687e3" }, "ruby-field-type-disambig/service.rb": { - "captureGroups": 8, - "digest": "bb2449fe1f87d23fca7db7507987d32daae36cbee8166fe9990006934b9d05a5" + "captureGroups": 9, + "digest": "beafde5670e8b6f2f2f7966bcfe71c9d3adec92ec0fa188f8b47587e2d559a4a" }, "ruby-field-type-disambig/user.rb": { "captureGroups": 13, @@ -132,20 +132,20 @@ "digest": "6673d1c2b299edee80c5d7ff0dddb8741e6b03070fbc119a5c1f14598ed8b005" }, "ruby-field-types/service.rb": { - "captureGroups": 8, - "digest": "9dcde083c602b85bf41561623dc7e6af16718cd1c8bacf00ac4b8d917a1557cd" + "captureGroups": 9, + "digest": "209a8f57b1f5a55c293ce3617c3040fe55d348168bc56e96a416e8ed8220cf58" }, "ruby-for-in-loop/app.rb": { - "captureGroups": 8, - "digest": "b584394fac8f939f3d4d1170c373da4332bfeffa641e64e997d108e85a4c5ec3" + "captureGroups": 9, + "digest": "63e0c857c40f59580432d18045a2e07bc628edb5464850fd277bb4af2bbb1996" }, "ruby-for-in-loop/repo.rb": { - "captureGroups": 9, - "digest": "fb4c12f847eca414066c5219a68c2dd48167a174d28788da118814ee8931ccfd" + "captureGroups": 11, + "digest": "06ccf6d3f7c370efe1aab899e20eaeae0552c2d068bb2fbeee3ae88a55b0bc60" }, "ruby-for-in-loop/user.rb": { - "captureGroups": 9, - "digest": "b6f4c241293057f3d4505cfec9d63376d1f598599dc6bf0a38ec0bef242dda5a" + "captureGroups": 11, + "digest": "5ee42d0becb14846a96ff4bd83832c30cf29bbff31713cc1f3fee484cff721b8" }, "ruby-grandparent-resolution/lib/app.rb": { "captureGroups": 10, @@ -188,8 +188,8 @@ "digest": "54fc82a9a0a67ccff1d5ca3055c93044e79e6cd2972d943f9c2e90f1f2198716" }, "ruby-method-enrichment/lib/animal.rb": { - "captureGroups": 28, - "digest": "f694a63268d14ab07db08332809823351abc680a7dc53f4797651f0e7b167384" + "captureGroups": 32, + "digest": "692d351da11c65998ebce97ab2c494df6b7aaf50af759caeefbb19d8f2726475" }, "ruby-method-enrichment/lib/app.rb": { "captureGroups": 14, @@ -200,8 +200,8 @@ "digest": "8f6555980417465c870a16742eb2d49fcb8a31e26777c11b926180674aa93e97" }, "ruby-namespaced-constructor/models/user_service.rb": { - "captureGroups": 12, - "digest": "c10f36dbbbe2be16fc3fccbebb7ee79668ec7a77b75adbd5281ded31893d49de" + "captureGroups": 13, + "digest": "5e0334b33bf3971198e58d8d7637bab148381744ce3b44bb7f17846c863b690d" }, "ruby-namespaced/namespaced.rb": { "captureGroups": 16, @@ -224,8 +224,8 @@ "digest": "288d5386cf37fb76b52a94bc7da6bf8e7843830ebbb01fcd8100d1590c0e3f72" }, "ruby-overload-dispatch/lib/formatter.rb": { - "captureGroups": 11, - "digest": "3e210571081b6977ea7d8b23f1678b57bedb2414cf5f6730466bf7b56829857c" + "captureGroups": 14, + "digest": "48a0c401c3716da69fce6beb17f9be49c7994074a2fb44599aa0fce2a8c6c2b1" }, "ruby-parent-resolution/lib/models/base_model.rb": { "captureGroups": 6, @@ -264,12 +264,12 @@ "digest": "0db7356580a8ac5addec69a3259e5a78028e01b44f5e36316a6f76172929f829" }, "ruby-return-type/models.rb": { - "captureGroups": 14, - "digest": "b731080e81701e48bbe0eff87dcb0286d266626037dc1e422cc268ef1a644722" + "captureGroups": 18, + "digest": "e176965a226cc1c84bfd10a2d22083facea402184e9907c6d7858fcd9bc459e5" }, "ruby-return-type/repo.rb": { - "captureGroups": 14, - "digest": "70875215143713279e9595e89b8c5d7d2c642d0660365772b64daf5b8b487b4f" + "captureGroups": 18, + "digest": "c23aa57b5f96ea094dc201daa0b2d02d7a7438b53488977eadc6ab2ebe63a68b" }, "ruby-self-this-resolution/lib/models/repo.rb": { "captureGroups": 6, @@ -288,8 +288,8 @@ "digest": "f56ff0b87abb7463dcf5fdbe18c1cf74c11595853ff04ff8d929cc8d5cb71ca5" }, "ruby-sequential-mixin/lib/logger_mixin.rb": { - "captureGroups": 7, - "digest": "c6edbab52ae7a58d89bd11da498d99a7cf0eaaeab531c56774b1a3fee1c39d1e" + "captureGroups": 9, + "digest": "b37cb0302afa4e380fb02828fcd4219edfd6aabbe65c74e4a5f5de9a65b33003" }, "ruby-sequential-mixin/lib/prepended_override.rb": { "captureGroups": 9, @@ -320,27 +320,27 @@ "digest": "106fe23a380801055a2ef642f0b92d4552ce074321ebe831601f2cb89a8d8529" }, "ruby-write-access/service.rb": { - "captureGroups": 18, - "digest": "b39aef2dd2721626e0c1718ea150455bc54522022ba45ce0d235f2e33e861be9" + "captureGroups": 21, + "digest": "444cd08dd9f29508da3298f7a14a91dd6edab354ab4ff6345b3744afa65136d7" }, "ruby-yard-annotations/models.rb": { - "captureGroups": 14, - "digest": "1906ddedab8b6f352468a13cefd3d018889492b8dc983f854ccbe7a2b84a938f" + "captureGroups": 15, + "digest": "47b1dc604ac3070fcb24575b06e452f9c6ba95424dd8cdc98c65a25b2a635251" }, "ruby-yard-annotations/service.rb": { - "captureGroups": 13, - "digest": "f0864dc29de46bc9ae3c15cae5714490aac6536d8da0704a353f2300df75bb63" + "captureGroups": 15, + "digest": "1778fe41038f4a05ecca406ad5cffb0e6b33e69b81265702611a6d498aa68b06" }, "ruby-yard-generics/models.rb": { "captureGroups": 17, "digest": "3a0722a48ac72ad10a492d10c4728a805990d07789e5b20a239a6c95a914d23f" }, "ruby-yard-generics/service.rb": { - "captureGroups": 18, - "digest": "040e4c3a4091238c8b59089f4b4e92d59879dbe5053131395a5e1ac6ad5f1d09" + "captureGroups": 21, + "digest": "f2452a20687ba8adb2ac86a23d4c6e3af51df62f65c28212dfbb0f1244643cb3" }, "synthetic:dao-20": { - "captureGroups": 161, - "digest": "99da729084d5a08425f04078fb633b35b7256e861b67bce095af59cb42e151fe" + "captureGroups": 201, + "digest": "62db9b6265ac37ece2271596da05196463ae9bbb7d9af40798f55bfe1af1768e" } } diff --git a/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json b/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json index e0326b484..d977d99bf 100644 --- a/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json +++ b/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json @@ -1,11 +1,11 @@ { "rust-abstract-dispatch/src/lib.rs": { - "captureGroups": 28, - "digest": "f1fad77028347c5102a39dbe6522786fa7863e5d4fcaf675294dffc23555d23b" + "captureGroups": 30, + "digest": "88309004d1ab00054f81bc55c1d058fc4ca25781162d6b94b7e7ce631a5d61b2" }, "rust-abstract-dispatch/src/main.rs": { - "captureGroups": 19, - "digest": "f0483e7bed3772013ed48efea6071e4959603e5ceb9b3786e4d334c7a1ccf76a" + "captureGroups": 21, + "digest": "9e37c9806579acc6ad6c43e98b7c1320d411020fb0c0e88f5b2c8bbed2053d7d" }, "rust-alias-imports/src/main.rs": { "captureGroups": 19, @@ -40,8 +40,8 @@ "digest": "225b1a2de0f98873a99c669275ecce6a7f6f0d04f2b50f6a15f7546c06f2e295" }, "rust-assignment-chain/src/main.rs": { - "captureGroups": 35, - "digest": "ba2d21b5126b59c0cfe9d99427ee6726eab0e3f2d19022e4b946bddf11730b42" + "captureGroups": 37, + "digest": "d9dc8c233465fb4d160c8547380fa05a0e2d1cdc4dae1205a33f6e96506f0df6" }, "rust-assignment-chain/src/repo.rs": { "captureGroups": 10, @@ -68,24 +68,24 @@ "digest": "db75d1b522e5d3574037b692e51423f5f71b6bba011aa120e33d0c6fbc6c1033" }, "rust-call-result-binding/src/models.rs": { - "captureGroups": 19, - "digest": "550f189e1374dc52599482fb63c8363a7839b4a7514aa6e911c354b3a92926a9" + "captureGroups": 20, + "digest": "768d02914dc9e5b495b091ae3f6a27f90ac7b043eeb54e1401c07db4126a5e36" }, "rust-calls/src/main.rs": { "captureGroups": 9, "digest": "2ec004ce1d77e3aee80ae1332bc0ab64fb366deffbac84f84ff1861286edc4e5" }, "rust-calls/src/onearg/mod.rs": { - "captureGroups": 6, - "digest": "5a2b4645b0dcebfb9eada1a5b168b99830316859854d1f9c55654a3ab0211f0c" + "captureGroups": 7, + "digest": "32c1951a3ae761c4451f78883ee1e520d1e3fb3e11100c9052668547eca20e69" }, "rust-calls/src/zeroarg/mod.rs": { "captureGroups": 5, "digest": "aae68fe5023fa93180b05ce8034654bfa1d2d95bc7eea67625e2e3a3cff6ebee" }, "rust-chain-call/src/main.rs": { - "captureGroups": 23, - "digest": "c99d5d1f0aaf873eec47268f852fc265e4f1e2d2be08b76be653e49f43ffc5df" + "captureGroups": 24, + "digest": "eae71a411927bfc015e03800fda779c065c2513e6a6aa130211216d9bee61a35" }, "rust-chain-call/src/models/mod.rs": { "captureGroups": 3, @@ -104,8 +104,8 @@ "digest": "4af705aa98a718b54c1e8a2037989c747c3e4e6bb8ba833cdf6672a1f8983cec" }, "rust-child-extends-parent/src/main.rs": { - "captureGroups": 17, - "digest": "21698d0650c872cb7a2f0849aef47d266ffb6a851d01286174d76f74a25acd1e" + "captureGroups": 18, + "digest": "0c87c741f9b05ff5e01eaf6a27c285ba6ee76b753d6609fb43f8d78e5f66fc0a" }, "rust-child-extends-parent/src/parent.rs": { "captureGroups": 7, @@ -128,8 +128,8 @@ "digest": "ff4606a898325894a9ef71fa5d16446fba7f24976bb88d7fcf5e6e62f76f99d3" }, "rust-coverage/patterns.rs": { - "captureGroups": 8, - "digest": "107045c7d648c89b825214f56bdaffee80fccbc063b69a126b95d5aca27618b6" + "captureGroups": 9, + "digest": "b551630a14753534a8f1fe3ea3bffbcfe077569440c2dcc9016c85489dc53806" }, "rust-coverage/union.rs": { "captureGroups": 5, @@ -156,8 +156,8 @@ "digest": "fe28a5861492fc4b20d911e44abc246ec33e4f3305fe4bd7e95c952972f17564" }, "rust-deep-field-chain/service.rs": { - "captureGroups": 15, - "digest": "143fcbb0bda81b945ecb1b3a692fb35ccc1e7b2ee1eea64f20ec548b432a53fb" + "captureGroups": 16, + "digest": "49d6a6bff4bfde6d29d8e63c2899f49688bee98273c9f743d9189dbfdf19ce2c" }, "rust-default-constructor/src/main.rs": { "captureGroups": 34, @@ -176,8 +176,8 @@ "digest": "798c8e01c6e54792ba69e845248efc8abf0cba38fa3d16fb8e0d1f6dd2ad2b7e" }, "rust-err-unwrap/src/main.rs": { - "captureGroups": 24, - "digest": "e4c452ce79e6e09f24d77acb9537f38b46afb4c59661192d0a4bc496a2b753bb" + "captureGroups": 26, + "digest": "d7da37b0af98c116dba470de505610c4fbb06f152078b684c0094b1e81aff7e4" }, "rust-err-unwrap/src/repo.rs": { "captureGroups": 9, @@ -192,8 +192,8 @@ "digest": "f6c64476fd1b30699d0c35cf7b64cd4210b3ae2158f29ad0faae614b1b218c83" }, "rust-field-types/service.rs": { - "captureGroups": 10, - "digest": "03f3b9175fc383450ab97e7e82a3e0feff4ffebdd78f4804717c8f64045c080c" + "captureGroups": 11, + "digest": "5bba21d0f792eb98025ce0531c575dac215492027cd877df09b19877e80da4ad" }, "rust-for-call-expr/src/main.rs": { "captureGroups": 24, @@ -208,8 +208,8 @@ "digest": "7331591b986aa2a9b0ee57020c1f0285ad0da10d5044c326b4beaaeb26019aa8" }, "rust-for-loop/src/main.rs": { - "captureGroups": 24, - "digest": "9622a9b47737283123a93511231529524ec82a54c5a97c17dc81dcc6e8f4912c" + "captureGroups": 26, + "digest": "1f80bd8d194a6e0e843e0a2a76e910d25bab0712bd9208f69c045b6be25cbaed" }, "rust-for-loop/src/repo.rs": { "captureGroups": 9, @@ -224,8 +224,8 @@ "digest": "494ee20e8d5ebff07a8358938b706e15a48c9a9fb21e0ce361c2a8c391080352" }, "rust-grouped-imports/src/helpers/mod.rs": { - "captureGroups": 14, - "digest": "32bc4f57a1dc0ffe8e9cbf0f0d6ce2ac5b1f2f93e2e3de3f217c92636480de63" + "captureGroups": 16, + "digest": "4363614087aacd43e98bef6c90e33a49edf7a4c4bee2a20a9e1dd8301c84310a" }, "rust-grouped-imports/src/main.rs": { "captureGroups": 14, @@ -244,8 +244,8 @@ "digest": "abf2026b72dacf36eca93895d2601c900424f30ff7f59f904f0dcf1fa377829c" }, "rust-if-let-unwrap/src/main.rs": { - "captureGroups": 15, - "digest": "3a45c99bbfc3ba2117d333127b3031697623bb044a1f04ce5381ff1366fe7468" + "captureGroups": 16, + "digest": "fa486e6573e6f8372fdff4ffbce6c726e354f1961f27e4f91be5aa0ab49b0496" }, "rust-if-let-unwrap/src/repo.rs": { "captureGroups": 9, @@ -264,8 +264,8 @@ "digest": "d8c1eb57431b915dd5c9055d8451054a454e340c4842628e38e4d46f69471abd" }, "rust-iter-for-loop/src/main.rs": { - "captureGroups": 28, - "digest": "7443b6c9231e54de5afd5f6fb52fd0a9709299a650615119a5be68de43287a88" + "captureGroups": 30, + "digest": "529fda7f9f188814ce6044e2c99d6b005ec4a9b98835fe9609530897998b9f32" }, "rust-iter-for-loop/src/repo.rs": { "captureGroups": 9, @@ -276,20 +276,20 @@ "digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e" }, "rust-local-shadow/src/main.rs": { - "captureGroups": 16, - "digest": "66d3a2678fb33301bbaa419684d208a0dc5c914d6e3bcf20e1c99e9666ed6fa2" + "captureGroups": 17, + "digest": "fa630d884c95e92a2d9c9b372f88e752a24aa4feb8089c3e43726793f3d12188" }, "rust-local-shadow/src/utils.rs": { - "captureGroups": 6, - "digest": "493dbeab554e28bed4a03cb62b951cad4f43bce5b5b522a192cd04159b3b2d9c" + "captureGroups": 7, + "digest": "bd341f6eb3527abdd63b3ff6a337a502858c72d75623f78a4e46646ba51d5915" }, "rust-macro/lib.rs": { "captureGroups": 11, "digest": "19b650be99256aa211356edc5a3dde83aff21e5d763e2c079322a24045bf69c9" }, "rust-match-unwrap/src/main.rs": { - "captureGroups": 24, - "digest": "1d9ead4663799e035816ed06a17d37e9a98d0fd8090f9dea256ec6f019788648" + "captureGroups": 26, + "digest": "ffec7f4401caefc75df450004d4b0828b053799dcbf7247ff2b431ea6ac5a46b" }, "rust-match-unwrap/src/repo.rs": { "captureGroups": 9, @@ -300,24 +300,24 @@ "digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e" }, "rust-member-calls/src/main.rs": { - "captureGroups": 14, - "digest": "9de5ab8411808df29a924b2e854048c6babb1a09b996279aba5c3f7916d03ae0" + "captureGroups": 15, + "digest": "2ef8b5f8ad187b1476af8ceff54051dcf9aa71236aa2b7485a28b3ff97e58e1e" }, "rust-member-calls/src/user.rs": { "captureGroups": 10, "digest": "a5545efb669e40adf8428af48057cd93bd31c226f7d0952d4dae76e1ee5228f7" }, "rust-method-chain-binding/src/main.rs": { - "captureGroups": 17, - "digest": "81b41ca25c672098dadbd7911c5a981706035b1bdca83eb2ca5f6bdbf759ab6b" + "captureGroups": 18, + "digest": "b4767bb9c5e6856309d04e0a3dff90940d391a64b7a6f7efa9109d419af6891a" }, "rust-method-chain-binding/src/models.rs": { "captureGroups": 34, "digest": "cd836a2a9c15ab240961d2e15f192f7e33d65eb5ebf2e1a8af2f620a47fe66ae" }, "rust-method-enrichment/src/lib.rs": { - "captureGroups": 39, - "digest": "8e60a44f5e18d1e26eea4bb21129494d2e3be697fec373dd4b6044f989176d9e" + "captureGroups": 40, + "digest": "71627a8218e32514b6945e4e310686eb631ce37451c90c9644e3f5336a37820b" }, "rust-method-enrichment/src/main.rs": { "captureGroups": 18, @@ -332,8 +332,8 @@ "digest": "2fc1fe1eb4e8727a89ab283ae34a0ae8df0c421551a7bd5e6e7ffb9d4aa54189" }, "rust-nullable-receiver/src/main.rs": { - "captureGroups": 37, - "digest": "283d8606eb837f2a9e5fdf95a30e3da5b73d4c14d74b94deb03e924dd2b2fde1" + "captureGroups": 39, + "digest": "2fdecb2bce41738daf30b86b8b145b4ca3438e72387ab25e5b3ba7a5af2c1090" }, "rust-nullable-receiver/src/repo.rs": { "captureGroups": 10, @@ -344,8 +344,8 @@ "digest": "a5545efb669e40adf8428af48057cd93bd31c226f7d0952d4dae76e1ee5228f7" }, "rust-option-receiver/src/main.rs": { - "captureGroups": 24, - "digest": "02298528e6fd679850ecaf6ed06017d29b7cac0f55ba2308692c87bd8904f040" + "captureGroups": 27, + "digest": "497145ee37b17cf2eacb3fab98b71b3a04162d011d1e887c588fb9f77d4283be" }, "rust-option-receiver/src/repo.rs": { "captureGroups": 8, @@ -380,8 +380,8 @@ "digest": "ee34385539f7e9398123c056738c6a662a80dac41fc038db96fab0da5c84c8ac" }, "rust-receiver-resolution/src/main.rs": { - "captureGroups": 21, - "digest": "283b9d58dc76d8f48ab2fce4598f5173910f8c59f27c2aa9c1d80a3c86878be8" + "captureGroups": 23, + "digest": "89d03ee72c223fde12b407ff50ae512dedb1a63aa4239e6f5dd9a54949566786" }, "rust-receiver-resolution/src/repo.rs": { "captureGroups": 10, @@ -416,8 +416,8 @@ "digest": "2ab4961fb869cf40db597b422b24c7ba7a7b63d97487cea7013e065a32e497f3" }, "rust-return-type/src/models.rs": { - "captureGroups": 17, - "digest": "0e3826200f2e6f5b948313369e85ee3a08f18bc8d51fbd1f7283b19a8e01aac8" + "captureGroups": 18, + "digest": "c589e22d34215100a8e69f0c0a3a801814203edfb28d46a0342d1b1f813af06b" }, "rust-scoped-impl/lib.rs": { "captureGroups": 17, @@ -432,12 +432,12 @@ "digest": "7346b2cf62e4946b261ed0eb46f2623fb1882f46d832abdc2068012917c7b16a" }, "rust-scoped-multi-file/src/models/repo.rs": { - "captureGroups": 19, - "digest": "25a3d44bc451ccaf8c6875b226fbda22ed1a18dcc884d4c6d9914ed83fc555b0" + "captureGroups": 20, + "digest": "9c41e8dea4bec804dc08ebb2af3ee06f6cbed2d4e72217331f4a42dd227ea6a9" }, "rust-scoped-multi-file/src/models/user.rs": { - "captureGroups": 19, - "digest": "77dc9858229322d8e1abbf209230bfbf6fca00627a5864888e91f1841b1f0cbb" + "captureGroups": 20, + "digest": "ab12913053296035ad20ebd2af9f2f9988f8b89d32af41f3915f8706f00fa600" }, "rust-self-struct-literal/main.rs": { "captureGroups": 11, @@ -456,8 +456,8 @@ "digest": "9354dad2a222a66b46f00ceede6777ba8c0634294c41313f393bc9ebcbbb6aa7" }, "rust-struct-destructuring/main.rs": { - "captureGroups": 15, - "digest": "6d96109296b7f5bd709e2704c14c332bfa0bca12453ae289b38a74da2c38124d" + "captureGroups": 17, + "digest": "79ce4f3fa933214532c96161865231c51c5505773794e0c87d5c995a2684cd4b" }, "rust-struct-destructuring/point.rs": { "captureGroups": 6, @@ -476,16 +476,16 @@ "digest": "dedd93d6214ff00ee0ee267140918403b7f59e0ab010cb9058af40cb3b08bb81" }, "rust-struct-literals/app.rs": { - "captureGroups": 12, - "digest": "46c3ea5d181fc61b6ea891adf439310ca9f941c0846a6a62c3a9ae9ac0c112f1" + "captureGroups": 13, + "digest": "2a1a44788e40cf855c235849c932b4e82f6551a7b5e747e1f6f91b250ead6ec4" }, "rust-struct-literals/user.rs": { "captureGroups": 11, "digest": "60fc4ac44f58ae67d462e243655b0571392a18de85b9e200e9391b50c941a6c9" }, "rust-traits/src/impls/button.rs": { - "captureGroups": 34, - "digest": "80fc76cdf20e3e0594a1ed933ce25682519151632157d18ef011dba60a2245d1" + "captureGroups": 36, + "digest": "30b0e4e92385e7f7eb83ad97e78ed65b0eced81bbcb20bf52a846924c75ab5bb" }, "rust-traits/src/main.rs": { "captureGroups": 11, @@ -508,11 +508,11 @@ "digest": "660f755fd70cd1796f9da02ad7d65f599dea8029665ee45ecd18cd27919741f3" }, "rust-write-access/service.rs": { - "captureGroups": 16, - "digest": "7edc72e15b18ec6cf0b48a592ccb3570f46ac25463640fc6a97d71fa1795b643" + "captureGroups": 17, + "digest": "4c5055277bdbb762af4f58623c3aba323c9618e900ca731ead19171d4a3a9495" }, "synthetic:dao-20": { - "captureGroups": 381, - "digest": "4b0262ff8d728d50f3f22772a1691001ee362936f7dd5395d458539cd6ca3411" + "captureGroups": 421, + "digest": "5ec55a7a5a07266b385669e663d1438c0ad223c5eb521a74f77b5b03840d2892" } } diff --git a/gitnexus/test/fixtures/swift-captures-golden/expected-captures.json b/gitnexus/test/fixtures/swift-captures-golden/expected-captures.json index 1482e4674..7abab94aa 100644 --- a/gitnexus/test/fixtures/swift-captures-golden/expected-captures.json +++ b/gitnexus/test/fixtures/swift-captures-golden/expected-captures.json @@ -1,27 +1,27 @@ { "swift-abstract-dispatch/Sources/App.swift": { - "captureGroups": 9, - "digest": "56b82214cca3ed89312d84d6b04a48ee13cbe748ea6978e9bffa20ce46ac0930" + "captureGroups": 10, + "digest": "0ec4bb27184ce68dca2ef6bef58c23353b363be9aa483d0bbea5321bccf85ae2" }, "swift-abstract-dispatch/Sources/Repository.swift": { - "captureGroups": 24, - "digest": "3a3fa6017b6937de81e5f70bcab727910edb00053c7e5bd30da67ed1fb84c71f" + "captureGroups": 26, + "digest": "36d177980181b97f016d834a9a57b7e559e61558a9f4e1c45809604886b18aa7" }, "swift-await-try/App.swift": { "captureGroups": 13, "digest": "7ba2042b0cb0a6e5c5d459dc460c509ace477b0ce97bec0cc8aaf981f320e69a" }, "swift-await-try/Models.swift": { - "captureGroups": 20, - "digest": "59f0a1aaa2884d12ff60c615cf73b3385894afba7b2d3d0cda74ff8648802d0b" + "captureGroups": 21, + "digest": "eb9e2b04b9dd55d161c58b5004adde0f881a138b790985fd523e110b87035ffd" }, "swift-call-result-binding/App.swift": { "captureGroups": 7, "digest": "979534a46ee420a7a5dfd6b029c9fe5d67ca1b00f3bdc5e8977d72a1c316b75d" }, "swift-call-result-binding/Models.swift": { - "captureGroups": 14, - "digest": "177c25e87bb0507385cb1da50ebb08305c83c62af637311fad53928ba1d491bc" + "captureGroups": 15, + "digest": "7f068c35624ff48635c1f51ec79e871511200a044eb4920bedd6673c4f690911" }, "swift-child-extends-parent/Sources/App.swift": { "captureGroups": 10, @@ -36,8 +36,8 @@ "digest": "ee1086e1a0cbfc57f381401b81b2e150f422a64cd3b937f083a1fbbd85e5a27e" }, "swift-class-func-receiver/Service.swift": { - "captureGroups": 24, - "digest": "04431e0287afed4c37779fb8fbf1d8cbe00ff917d6512fefcec5d7da1db69de8" + "captureGroups": 27, + "digest": "6e9c6bd2ebb65624a5079a1ce18b5b8c6a73af5e3216580bb558f0e22a0ec689" }, "swift-constructor-fallback/App.swift": { "captureGroups": 7, @@ -48,12 +48,12 @@ "digest": "685538adb8aede40f7969572a4c8ba3d491413de22798e109800a121d8d88223" }, "swift-constructor-type-inference/Models/Repo.swift": { - "captureGroups": 14, - "digest": "a1e1b75aa5a637bdf1e47ffd5cea61b2f6b139078db766d4926d0d99086abe34" + "captureGroups": 15, + "digest": "9f932b70d43c83b403a456b5854586f29238cfb573dff7e1183aea20d7e7f18d" }, "swift-constructor-type-inference/Models/User.swift": { - "captureGroups": 14, - "digest": "107563b39540473845aeafc30aae96191eae60ba56bed8d8b13dd98aaef72f0a" + "captureGroups": 15, + "digest": "1fcd2298bb1b7d0d6ff42248cedd66d31a2e3e0de8645162ee3eb31924ac9987" }, "swift-constructor-type-inference/Services/App.swift": { "captureGroups": 12, @@ -76,16 +76,16 @@ "digest": "f7062b53f33ec548a2ba5836ef5507b4c662d74ac4935dc654e6f95081f6fcbd" }, "swift-extension-dedup/Product.swift": { - "captureGroups": 14, - "digest": "86c0c65ace3809f2f13cca1dabe54328c354923cde0e29e526542a9291a66522" + "captureGroups": 15, + "digest": "81ef7e833c46fdedfd8f632fe0918b768e88d4490e886a763760f26549346009" }, "swift-extension-dedup/ProductExtensions.swift": { "captureGroups": 8, "digest": "1e072eb427bb17ea1a225bcc85e0defe74297e3e0ae05c7dc9c04a7e870b0508" }, "swift-field-types/App.swift": { - "captureGroups": 6, - "digest": "2cb023f94129e0bd07e1116892bad2597eb0f3aa02079deb029d5e3604311999" + "captureGroups": 7, + "digest": "651c5cbbf9afdc0bf4d96217bc1a01cf0bf07a0190ca12a3a6786bb08a67b3ad" }, "swift-field-types/Models.swift": { "captureGroups": 20, @@ -116,24 +116,24 @@ "digest": "9d0a738f1fdd31c1e204b20f1ccc76428f9f300eed82191da63f70799ff18f09" }, "swift-init-cross-file/User.swift": { - "captureGroups": 17, - "digest": "238af425526a62a088a1c4b3ba75c9249e375ffbeedb991c1b7481fa20e11543" + "captureGroups": 18, + "digest": "4e7df3b46d39126ab8d6a21a6a4103453f38e1b8a2fe159447abaa85381b6295" }, "swift-init-cross-file/main.swift": { "captureGroups": 8, "digest": "3007e4849cf77cc6cb360a1dbf70abec414a408076020f4730d01b8a80c1ddd3" }, "swift-member-write-access/App.swift": { - "captureGroups": 11, - "digest": "43c147d979761375f88bccd980f9db2274a937a3157ecaf93ca337fa54d0ae39" + "captureGroups": 14, + "digest": "f3c28a41c270cea237598bd20c688ba6ab791e3701e85c66bfe3aff7ecfb1ec8" }, "swift-member-write-access/Models.swift": { - "captureGroups": 23, - "digest": "e830e3dca7d6c6260181b78d4af5bc61e031bdce3a451588989f17223a5eac1c" + "captureGroups": 26, + "digest": "25be33df18e7eb8fcb9005c8a963205a3d93f37d0950ca187e9caf569fc5dfa4" }, "swift-method-enrichment/Sources/Animal.swift": { - "captureGroups": 23, - "digest": "999c14cd327ad7e1cc90bae25e9c7e79a8ade5dd274d834d87a61846c4c91259" + "captureGroups": 24, + "digest": "a49732a1d0fdf5d33b1f0c321a0413163d5af3a353b83510dae5d801109788c5" }, "swift-method-enrichment/Sources/App.swift": { "captureGroups": 10, @@ -188,8 +188,8 @@ "digest": "b5ed80965325806c715d1c3a65163b014e0bbba5ca7e2df9d7c81b084fada15e" }, "swift-overload-dispatch/SqlRepository.swift": { - "captureGroups": 23, - "digest": "e5abcf7249afe3fb58fad54ec17da29195a95a05e6f1275375b9dc395030d9dc" + "captureGroups": 28, + "digest": "40caf38a1bbe958294ffaacb921f1ef08131445ed959f549fe9be86ea0cdb289" }, "swift-parent-resolution/Sources/Models/BaseModel.swift": { "captureGroups": 7, @@ -220,16 +220,16 @@ "digest": "ec0a14090e0240bdd52a955cf7c95c2bc994085ca1482911dd882c5cdaa17c81" }, "swift-return-type-inference/Models.swift": { - "captureGroups": 27, - "digest": "3d1a5a4fe1deb91dc2ba74f71e7bc0b42dbe1dde3b374891fbb3fcf8d265f3a8" + "captureGroups": 29, + "digest": "fd0c6c7970e237bed4a305a84ee33d00dbc7c06d97c2e076911ae6ca741cad98" }, "swift-return-type/App.swift": { "captureGroups": 7, "digest": "979534a46ee420a7a5dfd6b029c9fe5d67ca1b00f3bdc5e8977d72a1c316b75d" }, "swift-return-type/Models.swift": { - "captureGroups": 18, - "digest": "dcb5562e378f8672c85ade10d2dc38b499af2cc02efeb4c0f80ac8ce83ce7d02" + "captureGroups": 21, + "digest": "d92ce7a1ec19c86a1848fa1d72d151c8059714c9129b70e8d260135a6a4a4993" }, "swift-self-this-resolution/Sources/Models/Repo.swift": { "captureGroups": 7, @@ -240,7 +240,7 @@ "digest": "c0c8e583896b1d9c889b43682d53b0d6b8194f54a27ae16a4d89a5ad79354fdc" }, "synthetic:dao-20": { - "captureGroups": 321, - "digest": "abd793dd5a0196a9795ff62cee1b1b0905e69ea45092b6efd0decdc9bc84270c" + "captureGroups": 361, + "digest": "d38aec3ca6fad59b943b3515691adc169dc4b40b70711139e9f0301be844e6fa" } } diff --git a/gitnexus/test/integration/callable-capture-options-literal-gate.test.ts b/gitnexus/test/integration/callable-capture-options-literal-gate.test.ts new file mode 100644 index 000000000..f70a3d07b --- /dev/null +++ b/gitnexus/test/integration/callable-capture-options-literal-gate.test.ts @@ -0,0 +1,74 @@ +/** + * Literal-validation gate for `*_CALLABLE_CAPTURE_OPTIONS` blocks (#2522 + * review): the #1920 gate validates tree-sitter QUERY literals and exported + * config objects, but the callable-flow option Sets are module-private + * object-literal properties consumed via the shared synthesizer — a typo'd + * node type there silently captures nothing (PHP shipped a dead + * 'optional_parameter'). This gate extracts every `NodeTypes: new + * Set([...])` literal from each language's captures.ts and validates it + * against that language's grammar. + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync, existsSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { SupportedLanguages } from '../../src/config/supported-languages.js'; +import { loadGrammarModel, validateNodeType } from '../helpers/grammar-introspection.js'; + +const LANGUAGES_DIR = join( + dirname(fileURLToPath(import.meta.url)), + '..', + '..', + 'src', + 'core', + 'ingestion', + 'languages', +); + +interface OptionLiteral { + readonly language: SupportedLanguages; + readonly key: string; + readonly literal: string; +} + +function collectOptionLiterals(): OptionLiteral[] { + const out: OptionLiteral[] = []; + for (const language of Object.values(SupportedLanguages)) { + const file = join(LANGUAGES_DIR, language, 'captures.ts'); + if (!existsSync(file)) continue; + const source = readFileSync(file, 'utf8'); + const block = source.match(/_CALLABLE_CAPTURE_OPTIONS\s*=\s*\{([\s\S]*?)\n\}\s*as const/); + if (block === null) continue; + // Only keys whose name says they hold tree-sitter NODE TYPES — + // callableProtocolMethods (method names) and memberPointerOperators + // (operator tokens) are deliberately outside this contract. + const setRegex = /(\w+NodeTypes)\??:\s*new Set(?:)?\(\[([\s\S]*?)\]\)/g; + for (const match of block[1]!.matchAll(setRegex)) { + for (const entry of match[2]!.matchAll(/'([^']+)'/g)) { + out.push({ language, key: match[1]!, literal: entry[1]! }); + } + } + } + return out; +} + +describe('callable-capture-options literal gate (#2522)', () => { + it('collects a plausible literal surface', () => { + const literals = collectOptionLiterals(); + expect(literals.length).toBeGreaterThan(50); + expect(literals.map((l) => l.language)).toContain(SupportedLanguages.JavaScript); + }); + + it('every node-type literal in a *_CALLABLE_CAPTURE_OPTIONS Set exists in its grammar', () => { + const literals = collectOptionLiterals(); + const models = new Map>(); + const dead: string[] = []; + for (const { language, key, literal } of literals) { + if (!models.has(language)) models.set(language, loadGrammarModel(language)); + const verdict = validateNodeType(language, models.get(language) ?? null, literal); + if (verdict === 'dead') dead.push(`${language}: ${key} → '${literal}'`); + } + expect(dead).toEqual([]); + }); +}); diff --git a/gitnexus/test/integration/cfg/worker-roundtrip.test.ts b/gitnexus/test/integration/cfg/worker-roundtrip.test.ts index 92f8408da..b31a00f60 100644 --- a/gitnexus/test/integration/cfg/worker-roundtrip.test.ts +++ b/gitnexus/test/integration/cfg/worker-roundtrip.test.ts @@ -8,6 +8,7 @@ import { getProvider } from '../../../src/core/ingestion/languages/index.js'; import { SupportedLanguages } from '../../../src/config/supported-languages.js'; import type { CfgVisitor } from '../../../src/core/ingestion/cfg/types.js'; import type { SyntaxNode } from '../../../src/core/ingestion/utils/ast-helpers.js'; +import { extractParsedFile } from '../../../src/core/ingestion/scope-extractor-bridge.js'; // U3 — the worker→main boundary + cache coherence for the CFG side-channel. // These pin the contracts that make the disk-store + warm/durable parse cache @@ -148,6 +149,48 @@ describe('U3 — CFG side-channel JSON round-trip (no AST leakage, no field loss }); }); +describe('callable-flow ParsedFile round-trip', () => { + it('preserves discriminants, scopes, ranges, indexes, modes, and invocation positions', () => { + const provider = getProvider(SupportedLanguages.TypeScript); + const parsed = extractParsedFile( + provider, + ` + function target(): void {} + function invoke(cb: () => void): void { cb(); } + const first = target; + invoke(first); + `, + 'callable.ts', + () => {}, + ); + expect(parsed.callableFlowSites?.map((site) => site.kind)).toEqual([ + 'formal', + 'invoke', + 'seed', + 'argument', + ]); + + const round = JSON.parse(JSON.stringify(parsed, mapReplacer), mapReviver); + expect(round.callableFlowSites).toEqual(parsed.callableFlowSites); + expect(round.callableFlowSites[0]).toMatchObject({ + kind: 'formal', + parameterIndex: 0, + passingMode: 'value', + binding: { name: 'cb', addressOf: false, indirection: 0 }, + }); + expect(round.callableFlowSites[1]).toMatchObject({ + kind: 'invoke', + callee: { name: 'cb' }, + callSite: { startLine: 3 }, + }); + expect(round.callableFlowSites[3]).toMatchObject({ + kind: 'argument', + directCalleeName: 'invoke', + parameterIndex: 0, + }); + }); +}); + describe('U3 — parse-cache key folds the --pdg flag (R4, #2038-class guard)', () => { const entries = [ { filePath: 'b.ts', contentHash: 'h2' }, diff --git a/gitnexus/test/integration/resolvers/callable-value-flow.test.ts b/gitnexus/test/integration/resolvers/callable-value-flow.test.ts new file mode 100644 index 000000000..a16a63d51 --- /dev/null +++ b/gitnexus/test/integration/resolvers/callable-value-flow.test.ts @@ -0,0 +1,1437 @@ +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { SupportedLanguages } from 'gitnexus-shared'; +import { providers } from '../../../src/core/ingestion/languages/index.js'; +import { getRelationships, runPipelineFromRepo, type PipelineOptions } from './helpers.js'; +import { + loadParseCache, + PARSE_CACHE_VERSION, + saveParseCache, + type ParseCache, +} from '../../../src/storage/parse-cache.js'; +import { + getDurableParsedFileDir, + pruneAndSaveDurableParsedFileStore, +} from '../../../src/storage/parsedfile-store.js'; + +const CALLABLE_FLOW_PROVIDER_COVERAGE = { + [SupportedLanguages.JavaScript]: 'matrix', + [SupportedLanguages.TypeScript]: 'dedicated', + [SupportedLanguages.Python]: 'matrix', + [SupportedLanguages.Java]: 'matrix', + [SupportedLanguages.C]: 'dedicated', + [SupportedLanguages.CPlusPlus]: 'dedicated', + [SupportedLanguages.CSharp]: 'matrix', + [SupportedLanguages.Go]: 'matrix', + [SupportedLanguages.Ruby]: 'matrix', + [SupportedLanguages.Rust]: 'matrix', + [SupportedLanguages.PHP]: 'matrix', + [SupportedLanguages.Kotlin]: 'matrix', + [SupportedLanguages.Swift]: 'matrix', + [SupportedLanguages.Dart]: 'matrix', + [SupportedLanguages.Vue]: 'matrix', + [SupportedLanguages.Cobol]: 'matrix', +} as const satisfies Record; + +const PROVIDER_FLOW_CASES = [ + { + language: SupportedLanguages.JavaScript, + extension: 'js', + caller: 'invoke', + target: 'target', + source: ` +function target() {} +function invoke(callback) { callback(); } +const first = target; +const second = first; +invoke(second); +`, + }, + { + language: SupportedLanguages.Python, + extension: 'py', + caller: 'invoke', + target: 'target', + source: ` +def target(): + pass +def invoke(callback): + callback() +first = target +second = first +invoke(second) +`, + }, + { + language: SupportedLanguages.Java, + extension: 'java', + caller: 'invoke', + target: 'target', + source: ` +class Demo { + static void target() {} + static void invoke(Runnable callback) { callback.run(); } + static void main(String[] args) { + Runnable first = Demo::target; + Runnable second = first; + invoke(second); + } +} +`, + }, + { + language: SupportedLanguages.Kotlin, + extension: 'kt', + caller: 'invoke', + target: 'target', + source: ` +fun target() {} +fun invoke(callback: () -> Unit) { callback() } +fun main() { + val first = ::target + val second = first + invoke(second) +} +`, + }, + { + language: SupportedLanguages.Go, + extension: 'go', + caller: 'invoke', + target: 'target', + source: ` +package main +func target() {} +func invoke(callback func()) { callback() } +func main() { + first := target + second := first + invoke(second) +} +`, + }, + { + language: SupportedLanguages.Rust, + extension: 'rs', + caller: 'invoke', + target: 'target', + source: ` +fn target() {} +fn invoke(callback: fn()) { callback(); } +fn main() { + let first = target; + let second = first; + invoke(second); +} +`, + }, + { + language: SupportedLanguages.CSharp, + extension: 'cs', + caller: 'Invoke', + target: 'Target', + source: ` +using System; +class Demo { + static void Target() {} + static void Invoke(Action callback) { callback(); } + static void Main() { + Action first = Target; + Action second = first; + Invoke(second); + } +} +`, + }, + { + language: SupportedLanguages.PHP, + extension: 'php', + caller: 'invoke', + target: 'target', + source: ` Void) { callback() } +func main() { + let first = target + let second = first + invoke(second) +} +`, + }, + { + language: SupportedLanguages.Dart, + extension: 'dart', + caller: 'invoke', + target: 'target', + source: ` +void target() {} +void invoke(void Function() callback) { callback(); } +void main() { + final first = target; + final second = first; + invoke(second); +} +`, + }, + { + language: SupportedLanguages.Vue, + extension: 'vue', + caller: 'invoke', + target: 'target', + source: ` + +`, + }, + { + language: SupportedLanguages.Cobol, + extension: 'cbl', + caller: 'INVOKE', + target: 'TARGET', + source: ` +>>SOURCE FORMAT FREE +IDENTIFICATION DIVISION. +PROGRAM-ID. MAIN. +DATA DIVISION. +WORKING-STORAGE SECTION. +01 CALLBACK USAGE PROCEDURE-POINTER. +PROCEDURE DIVISION. + SET CALLBACK TO ENTRY "TARGET". + CALL "INVOKE" USING CALLBACK. + STOP RUN. +END PROGRAM MAIN. + +IDENTIFICATION DIVISION. +PROGRAM-ID. INVOKE. +DATA DIVISION. +LINKAGE SECTION. +01 CB USAGE PROCEDURE-POINTER. +PROCEDURE DIVISION USING CB. + CALL CB. + GOBACK. +END PROGRAM INVOKE. + +IDENTIFICATION DIVISION. +PROGRAM-ID. TARGET. +PROCEDURE DIVISION. + GOBACK. +END PROGRAM TARGET. +`, + }, +] as const; + +async function runSource(extension: string, source: string, options: PipelineOptions = {}) { + return runSources({ [`main.${extension}`]: source }, options); +} + +async function runSources(files: Record, options: PipelineOptions = {}) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-callable-flow-')); + try { + for (const [filePath, source] of Object.entries(files)) { + const fullPath = path.join(root, filePath); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, source, 'utf8'); + } + return await runPipelineFromRepo(root, () => {}, { skipGraphPhases: true, ...options }); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +} + +function callableCallSiteLines( + result: Awaited>, + source: string, + target: string, +): number[] { + return getRelationships(result, 'CALLS') + .filter( + (edge) => + edge.source === source && + edge.target === target && + edge.rel.reason === 'callable-value-flow', + ) + .map((edge) => Number(edge.rel.id.match(/:(\d+):(\d+)$/)?.[1])) + .sort((left, right) => left - right); +} + +function callsFrom( + result: Awaited>, + source: string, +): Array<{ target: string; reason: string }> { + return getRelationships(result, 'CALLS') + .filter((edge) => edge.source === source) + .map((edge) => ({ target: edge.target, reason: edge.rel.reason ?? '' })); +} + +function callableTargetQualifiedNames( + result: Awaited>, + source: string, +): string[] { + return getRelationships(result, 'CALLS') + .filter((edge) => edge.source === source && edge.rel.reason === 'callable-value-flow') + .map((edge) => result.graph.getNode(edge.rel.targetId)?.properties.qualifiedName ?? edge.target) + .sort(); +} + +function callableTargetIds( + result: Awaited>, + source: string, +): string[] { + return getRelationships(result, 'CALLS') + .filter((edge) => edge.source === source && edge.rel.reason === 'callable-value-flow') + .map((edge) => edge.rel.targetId) + .sort(); +} + +describe('callable value flow', () => { + it('governs every registered language provider', () => { + expect(Object.keys(CALLABLE_FLOW_PROVIDER_COVERAGE).sort()).toEqual( + Object.keys(providers).sort(), + ); + }); + + it.each(PROVIDER_FLOW_CASES)( + 'resolves assign → copy → argument → invoke for $language', + async ({ extension, source, caller, target }) => { + const result = await runSource(extension, source); + expect(callsFrom(result, caller)).toContainEqual({ + target, + reason: 'callable-value-flow', + }); + }, + 90_000, + ); + + it('resolves C function pointers, pointer copies, pointer-to-pointer loads, and two wrappers', async () => { + const result = await runSource( + 'c', + ` +void target(void) {} +void callback(void) {} +void installed_target(void) {} +void invoke(void (*callback)(void)) { callback(); } +void outer(void (*cb)(void)) { invoke(cb); } +void install(void (**slot)(void)) { *slot = &installed_target; } + +int entry(void) { + void (*fp)(void) = ⌖ + void (*fp2)(void) = fp; + void (**slot)(void) = &fp2; + fp(); + (*fp2)(); + (*slot)(); + (**slot)(); + invoke(*slot); + outer(target); + void (*installed)(void) = &callback; + install(&installed); + installed(); + return 0; +} +`, + ); + + expect(callsFrom(result, 'entry')).toEqual( + expect.arrayContaining([ + { target: 'target', reason: 'callable-value-flow' }, + { target: 'installed_target', reason: 'callable-value-flow' }, + expect.objectContaining({ target: 'invoke' }), + expect.objectContaining({ target: 'outer' }), + expect.objectContaining({ target: 'install' }), + ]), + ); + expect(callsFrom(result, 'invoke')).toContainEqual({ + target: 'target', + reason: 'callable-value-flow', + }); + expect(callsFrom(result, 'invoke').some((edge) => edge.target === 'callback')).toBe(false); + expect(callableCallSiteLines(result, 'entry', 'target')).toEqual([13, 14, 15, 16]); + expect(callableCallSiteLines(result, 'entry', 'installed_target')).toEqual([21]); + expect(callableCallSiteLines(result, 'invoke', 'target')).toEqual([5]); + }, 60_000); + + it('joins C prototypes to cross-file definitions without leaking a static decoy', async () => { + const result = await runSources({ + 'target.c': 'void target(void) {}\n', + 'decoy.c': 'static void target(void) {}\n', + 'wrapper.c': 'void invoke(void (*callback)(void)) { callback(); }\n', + 'main.c': ` +void target(void); +void invoke(void (*callback)(void)); +int main(void) { + void (*assigned)(void) = target; + invoke(assigned); + return 0; +} +`, + }); + + const callbackEdges = getRelationships(result, 'CALLS').filter( + (edge) => + edge.source === 'invoke' && + edge.target === 'target' && + edge.rel.reason === 'callable-value-flow', + ); + expect(callbackEdges).toHaveLength(1); + expect(callbackEdges[0]!.sourceFilePath).toBe('wrapper.c'); + expect(callbackEdges[0]!.targetFilePath).toBe('target.c'); + }, 60_000); + + it('uses a C++ function-pointer signature to select one overload and suppresses untyped overload sets', async () => { + const result = await runSource( + 'cpp', + ` +void target(int) {} +void target(double) {} + +int entry() { + void (*typed)(int) = ⌖ + auto unresolved = target; + typed(1); + unresolved(1); + return 0; +} +`, + ); + + expect(callableTargetQualifiedNames(result, 'entry')).toEqual(['target']); + expect(callableCallSiteLines(result, 'entry', 'target')).toEqual([8]); + const targetEdge = getRelationships(result, 'CALLS').find( + (edge) => + edge.source === 'entry' && + edge.target === 'target' && + edge.rel.reason === 'callable-value-flow', + ); + expect(result.graph.getNode(targetEdge!.rel.targetId)?.properties.parameterTypes).toEqual([ + 'int', + ]); + }, 60_000); + + it('retains C++ pointer and member-pointer signatures across split declaration and assignment', async () => { + const result = await runSource( + 'cpp', + ` +void target(int) {} +void target(double) {} +struct Base { + virtual void run() const {} + virtual void run() {} +}; +struct Derived : Base { + void run() const override {} + void run() override {} +}; +void invoke(void (*callback)(int), Derived& value, void (Base::*member)() const) { + callback(1); + (value.*member)(); +} +int entry() { + void (*callback)(int); + callback = ⌖ + void (Base::*member)() const; + member = &Base::run; + Derived value; + invoke(callback, value, member); + return 0; +} +`, + { skipGraphPhases: false }, + ); + + const targets = getRelationships(result, 'CALLS') + .filter((edge) => edge.source === 'invoke' && edge.rel.reason === 'callable-value-flow') + .map((edge) => ({ + id: edge.rel.targetId, + properties: result.graph.getNode(edge.rel.targetId)?.properties, + })); + expect(targets).toHaveLength(2); + expect( + targets.some( + ({ id, properties }) => + id.includes('target') && + JSON.stringify(properties?.parameterTypes) === JSON.stringify(['int']), + ), + ).toBe(true); + expect( + targets.some( + ({ id, properties }) => id.includes('Derived.run') && properties?.isConst === true, + ), + ).toBe(true); + expect( + targets.some( + ({ id, properties }) => + JSON.stringify(properties?.parameterTypes) === JSON.stringify(['double']) || + (id.includes('Derived.run') && properties?.isConst !== true), + ), + ).toBe(false); + }, 90_000); + + it('resolves an imported C++ function assigned through an auto pointer', async () => { + const result = await runSources({ + 'target.hpp': 'void target();\n', + 'target.cpp': '#include "target.hpp"\nvoid target() {}\n', + 'main.cpp': '#include "target.hpp"\nint entry() { auto callback = ⌖ callback(); }\n', + }); + + const targetEdges = getRelationships(result, 'CALLS').filter( + (edge) => + edge.source === 'entry' && + edge.target === 'target' && + edge.rel.reason === 'callable-value-flow', + ); + expect(targetEdges).toHaveLength(1); + expect(result.graph.getNode(targetEdges[0]!.rel.targetId)?.properties.filePath).toBe( + 'target.cpp', + ); + }, 90_000); + + it('dispatches C++ member pointers through object/reference and pointer receivers to the virtual override', async () => { + const result = await runSource( + 'cpp', + ` +struct Base { + virtual void run() {} +}; +struct Derived : Base { + void run() override {} +}; + +void invoke(Derived& value, void (Base::*member)()) { + (value.*member)(); + Derived* pointer = &value; + (pointer->*member)(); +} + +int entry() { + Derived value; + auto member = &Base::run; + invoke(value, member); + return 0; +} +`, + { skipGraphPhases: false }, + ); + + const targets = callableTargetIds(result, 'invoke'); + expect(targets).toHaveLength(2); + expect(new Set(targets)).toEqual(new Set(['Method:main.cpp:Derived.run#0'])); + }, 90_000); + + it('resolves (obj->*ptr)() regardless of tree-sitter ERROR-recovery grouping (#2522 review)', async () => { + const result = await runSource( + 'cpp', + ` +struct Base { + virtual void run() {} +}; +struct Derived : Base { + void run() override {} +}; + +void invoke(Derived& v, void (Base::*ptr)()) { + Derived* obj = &v; + (obj->*ptr)(); +} + +int entry() { + Derived v; + auto ptr = &Base::run; + invoke(v, ptr); + return 0; +} +`, + { skipGraphPhases: false }, + ); + + const targets = callableTargetIds(result, 'invoke'); + expect(targets).toHaveLength(1); + expect(new Set(targets)).toEqual(new Set(['Method:main.cpp:Derived.run#0'])); + }, 90_000); + + it('keeps non-virtual C++ member pointers exact and dispatches virtual overload/cv signatures precisely', async () => { + const result = await runSource( + 'cpp', + ` +struct Base { + virtual void run(int) {} + virtual void run(double) {} + void fixed() {} + virtual void cv() const {} + virtual void cv() {} +}; +struct Derived : Base { + void run(int) override {} + void run(double) override {} + void fixed() {} + void cv() const override {} + void cv() override {} +}; + +void invoke( + Derived& value, + void (Base::*runMember)(int), + void (Base::*fixedMember)(), + void (Base::*constMember)() const, + void (Base::*mutableMember)() +) { + (value.*runMember)(1); + (value.*fixedMember)(); + (value.*constMember)(); + (value.*mutableMember)(); +} + +int entry() { + Derived value; + void (Base::*runMember)(int) = &Base::run; + void (Base::*fixedMember)() = &Base::fixed; + void (Base::*constMember)() const = &Base::cv; + void (Base::*mutableMember)() = &Base::cv; + invoke(value, runMember, fixedMember, constMember, mutableMember); + return 0; +} +`, + { skipGraphPhases: false }, + ); + + const targets = getRelationships(result, 'CALLS') + .filter((edge) => edge.source === 'invoke' && edge.rel.reason === 'callable-value-flow') + .map((edge) => ({ + id: edge.rel.targetId, + properties: result.graph.getNode(edge.rel.targetId)?.properties, + })); + expect(targets).toHaveLength(4); + expect( + targets.some( + ({ id, properties }) => + id.includes('Derived.run') && + JSON.stringify(properties?.parameterTypes) === JSON.stringify(['int']), + ), + ).toBe(true); + expect( + targets.some( + ({ id, properties }) => + id.includes('Derived.run') && + JSON.stringify(properties?.parameterTypes) === JSON.stringify(['double']), + ), + ).toBe(false); + expect(targets.some(({ id }) => id.includes('Base.fixed'))).toBe(true); + expect(targets.some(({ id }) => id.includes('Derived.fixed'))).toBe(false); + expect( + targets.some( + ({ id, properties }) => id.includes('Derived.cv') && properties?.isConst === true, + ), + ).toBe(true); + expect( + targets.some( + ({ id, properties }) => id.includes('Derived.cv') && properties?.isConst !== true, + ), + ).toBe(true); + }, 90_000); + + it('resolves C++ function references and references to pointer variables', async () => { + const result = await runSource( + 'cpp', + ` +void target() {} +void invoke(void (&cb)()) { cb(); } + +int entry() { + void (*fp)(void) = ⌖ + void (&fr)(void) = target; + void (*&fpr)(void) = fp; + fr(); + fpr(); + invoke(fr); + return 0; +} +`, + ); + + expect(callsFrom(result, 'entry')).toEqual( + expect.arrayContaining([ + { target: 'target', reason: 'callable-value-flow' }, + expect.objectContaining({ target: 'invoke' }), + ]), + ); + expect(callsFrom(result, 'invoke')).toContainEqual({ + target: 'target', + reason: 'callable-value-flow', + }); + expect(callableCallSiteLines(result, 'entry', 'target')).toEqual([9, 10]); + expect(callableCallSiteLines(result, 'invoke', 'target')).toEqual([3]); + }, 60_000); + + it('propagates a C++ function reference through a cross-file prototype and formal', async () => { + const result = await runSources({ + 'target.cpp': 'void target() {}\n', + 'decoy.cpp': 'static void target() {}\n', + 'wrapper.cpp': ` +using Callback = void (*)(); +void invoke(Callback callback) { callback(); } +`, + 'main.cpp': ` +using Callback = void (*)(); +void target(); +void invoke(Callback callback); +int main() { + void (&reference)() = target; + invoke(reference); + return 0; +} +`, + }); + + const callbackEdges = getRelationships(result, 'CALLS').filter( + (edge) => + edge.source === 'invoke' && + edge.target === 'target' && + edge.rel.reason === 'callable-value-flow', + ); + expect(callbackEdges).toHaveLength(1); + expect(callbackEdges[0]!.sourceFilePath).toBe('wrapper.cpp'); + expect(callbackEdges[0]!.targetFilePath).toBe('target.cpp'); + }, 60_000); + + it('resolves TypeScript callable assignment, copy, and actual-to-formal invocation', async () => { + const result = await runSource( + 'ts', + ` +function target(): void {} +function invoke(callback: () => void): void { callback(); } +function outer(cb: () => void): void { invoke(cb); } + +const first = target; +const second = first; +second(); +outer(second); +`, + ); + + expect(callsFrom(result, 'main.ts')).toEqual( + expect.arrayContaining([ + { target: 'target', reason: 'callable-value-flow' }, + expect.objectContaining({ target: 'outer' }), + ]), + ); + expect(callsFrom(result, 'invoke')).toContainEqual({ + target: 'target', + reason: 'callable-value-flow', + }); + }, 60_000); + + it('propagates an imported callable through a cross-file wrapper without selecting a decoy', async () => { + const result = await runSources({ + 'target.ts': `export function target(): void {}`, + 'decoy.ts': `export function target(): void {}`, + 'wrapper.ts': ` +export function invoke(callback: () => void): void { + callback(); +} +`, + 'main.ts': ` +import { target } from './target'; +import { invoke } from './wrapper'; +const assigned = target; +invoke(assigned); +`, + }); + + const edge = getRelationships(result, 'CALLS').find( + (candidate) => + candidate.source === 'invoke' && + candidate.target === 'target' && + candidate.rel.reason === 'callable-value-flow', + ); + expect(edge).toBeDefined(); + expect(edge!.sourceFilePath).toBe('wrapper.ts'); + expect(edge!.targetFilePath).toBe('target.ts'); + expect( + getRelationships(result, 'CALLS').some( + (candidate) => candidate.source === 'invoke' && candidate.targetFilePath === 'decoy.ts', + ), + ).toBe(false); + }, 60_000); + + it('resolves a bound method assigned to a variable and passed into a wrapper', async () => { + const result = await runSource( + 'ts', + ` +class Worker { + run(): void {} +} +class Decoy { + run(): void {} +} +function invoke(callback: () => void): void { callback(); } +const worker = new Worker(); +const assigned = worker.run; +invoke(assigned); +`, + ); + + const edge = getRelationships(result, 'CALLS').find( + (candidate) => + candidate.source === 'invoke' && + candidate.target === 'run' && + candidate.rel.reason === 'callable-value-flow', + ); + expect(edge).toBeDefined(); + expect(edge!.targetLabel).toBe('Method'); + expect(edge!.rel.targetId).toContain('Worker.run'); + }, 60_000); + + it('does not reinterpret call results passed as arguments as callable designators', async () => { + const result = await runSource( + 'ts', + ` +function target(): void {} +function factory(): () => void { return target; } +function invoke(callback: () => void): void { callback(); } +invoke(factory()); +`, + ); + + expect(callableCallSiteLines(result, 'invoke', 'target')).toEqual([]); + expect(callableCallSiteLines(result, 'invoke', 'factory')).toEqual([]); + }, 60_000); + + it('resolves COBOL procedure pointers in sequence-numbered fixed format (#2522 review)', async () => { + const result = await runSource( + 'cbl', + ` +000100 IDENTIFICATION DIVISION. +000200 PROGRAM-ID. MAIN. +000300 DATA DIVISION. +000400 WORKING-STORAGE SECTION. +000500 01 CALLBACK USAGE IS PROCEDURE-POINTER. +000600 PROCEDURE DIVISION. +000700 SET CALLBACK TO ENTRY "TARGET". +000800 CALL "INVOKE" USING CALLBACK. +000900 STOP RUN. +001000 END PROGRAM MAIN. +001100 IDENTIFICATION DIVISION. +001200 PROGRAM-ID. INVOKE. +001300 DATA DIVISION. +001400 LINKAGE SECTION. +001500 01 CB USAGE IS PROCEDURE-POINTER. +001600 PROCEDURE DIVISION USING CB. +001700 CALL CB. +001800 GOBACK. +001900 END PROGRAM INVOKE. +002000 IDENTIFICATION DIVISION. +002100 PROGRAM-ID. TARGET. +002200 PROCEDURE DIVISION. +002300 GOBACK. +002400 END PROGRAM TARGET. +`, + ); + + expect(callableCallSiteLines(result, 'INVOKE', 'TARGET')).toEqual([18]); + }, 60_000); + + it('propagates COBOL procedure pointers through SET x TO y copies (#2522 review)', async () => { + const result = await runSource( + 'cbl', + ` +>>SOURCE FORMAT FREE +IDENTIFICATION DIVISION. +PROGRAM-ID. MAIN. +DATA DIVISION. +WORKING-STORAGE SECTION. +01 P1 USAGE PROCEDURE-POINTER. +01 P2 USAGE PROCEDURE-POINTER. +PROCEDURE DIVISION. + SET P1 TO ENTRY "TARGET". + SET P2 TO P1. + CALL P2. + STOP RUN. +END PROGRAM MAIN. + +IDENTIFICATION DIVISION. +PROGRAM-ID. TARGET. +PROCEDURE DIVISION. + GOBACK. +END PROGRAM TARGET. +`, + ); + + expect(callableCallSiteLines(result, 'MAIN', 'TARGET')).toEqual([12]); + }, 60_000); + + it('ignores commented-out COBOL SET statements (#2522 review)', async () => { + const result = await runSource( + 'cbl', + ` +000100 IDENTIFICATION DIVISION. +000200 PROGRAM-ID. MAIN. +000300 DATA DIVISION. +000400 WORKING-STORAGE SECTION. +000500 01 CALLBACK USAGE IS PROCEDURE-POINTER. +000600 PROCEDURE DIVISION. +000700 SET CALLBACK TO ENTRY "TARGET". +000750* SET CALLBACK TO ENTRY "MAIN". +000800 CALL "INVOKE" USING CALLBACK. +000900 STOP RUN. +001000 END PROGRAM MAIN. +001100 IDENTIFICATION DIVISION. +001200 PROGRAM-ID. INVOKE. +001300 DATA DIVISION. +001400 LINKAGE SECTION. +001500 01 CB USAGE IS PROCEDURE-POINTER. +001600 PROCEDURE DIVISION USING CB. +001700 CALL CB. +001800 GOBACK. +001900 END PROGRAM INVOKE. +002000 IDENTIFICATION DIVISION. +002100 PROGRAM-ID. TARGET. +002200 PROCEDURE DIVISION. +002300 GOBACK. +002400 END PROGRAM TARGET. +`, + ); + + // The dead SET in the comment must not add MAIN to the callee set. + expect(callableCallSiteLines(result, 'INVOKE', 'MAIN')).toEqual([]); + expect(callableCallSiteLines(result, 'INVOKE', 'TARGET')).toEqual([19]); + }, 60_000); + + it('does not resolve a Rust unit enum variant seeded as a callable value (#2522 review)', async () => { + const result = await runSource( + 'rs', + ` +enum Shape { + Square, +} + +fn dispatch(cb: fn()) { + cb(); +} + +fn entry() { + let x = Shape::Square; + let _ = x; +} + +fn main() { + entry(); + dispatch(main); +} +`, + ); + + // Shape::Square is a value, not a callable — the qualified-name guard + // must keep the over-captured seed edge-free. + expect( + getRelationships(result, 'CALLS').filter( + (edge) => edge.rel.reason === 'callable-value-flow' && edge.target === 'Square', + ), + ).toEqual([]); + }, 60_000); + + it('pairs Go multi-value := positionally instead of cross-wiring (#2522 review)', async () => { + const result = await runSource( + 'go', + `package main + +func targetFunc() {} +func otherFunc() {} + +func entry() { + a, b := targetFunc, otherFunc + a() + b() +} +`, + ); + + expect(callableCallSiteLines(result, 'entry', 'targetFunc')).toEqual([8]); + expect(callableCallSiteLines(result, 'entry', 'otherFunc')).toEqual([9]); + }, 60_000); + + it('does not treat a bare Ruby identifier as a callable reference — it is a call (#2522 review)', async () => { + const result = await runSource( + 'rb', + ` +def process + 'done' +end + +def dispatch(cb) + cb.call +end + +action = process +dispatch(action) +`, + ); + + // `action` holds process's RETURN VALUE (a String); a CALLS edge from + // dispatch to process here is over-linking. + expect(callableCallSiteLines(result, 'dispatch', 'process')).toEqual([]); + expect( + getRelationships(result, 'CALLS').filter( + (edge) => edge.rel.reason === 'callable-value-flow' && edge.target === 'process', + ), + ).toEqual([]); + }, 60_000); + + it('resolves the C ops-vtable pattern: struct-field pointer stored then called (#2522 review)', async () => { + const result = await runSource( + 'c', + ` +void handler(int x) {} +struct ops { void (*run)(int); }; +int entry(struct ops *o) { + o->run = handler; + o->run(1); + return 0; +} +`, + ); + + expect(callableCallSiteLines(result, 'entry', 'handler')).toEqual([6]); + }, 60_000); + + it('resolves a file-scope function pointer assigned in one function and called in another (#2522 review)', async () => { + const result = await runSource( + 'c', + ` +void handler(int x) {} +static void (*fp)(int); +void init(void) { fp = handler; } +void run(void) { fp(1); } +`, + ); + + expect(callableCallSiteLines(result, 'run', 'handler')).toEqual([5]); + }, 60_000); + + it('binds variable-indexed function-pointer array cells to the array, not the index (#2522 review)', async () => { + const result = await runSource( + 'c', + ` +void handler(int x) {} +int entry(void) { + void (*tbl[2])(int); + int i = 0; + tbl[i] = handler; + tbl[i](7); + return 0; +} +`, + ); + + expect(callableCallSiteLines(result, 'entry', 'handler')).toEqual([7]); + }, 60_000); + + it('does not store a factory function itself through a C pointer assignment', async () => { + const result = await runSource( + 'c', + ` +void initial(void) {} +void produced(void) {} +void (*factory(void))(void) { return &produced; } +void install(void (**slot)(void)) { *slot = factory(); } +int entry(void) { + void (*callback)(void) = &initial; + install(&callback); + callback(); + return 0; +} +`, + ); + + expect(callableCallSiteLines(result, 'entry', 'factory')).toEqual([]); + expect(callableCallSiteLines(result, 'entry', 'initial')).toEqual([9]); + }, 60_000); + + it('keeps a direct call outside a nested same-name callable binding on ordinary resolution', async () => { + const result = await runSource( + 'ts', + ` +function target(): void {} +function replacement(): void {} +function entry(): void { + target(); + { + const target = replacement; + target(); + } +} +`, + ); + + expect( + getRelationships(result, 'CALLS').some( + (edge) => edge.source === 'entry' && edge.target === 'target', + ), + ).toBe(true); + expect(callableCallSiteLines(result, 'entry', 'replacement')).toEqual([8]); + }, 60_000); + + it('keeps the declaration as a target when a function is reassigned through its own name and the RHS is unresolvable (#2522 review)', async () => { + const result = await runSource( + 'js', + ` +import { fancyGreet } from 'external-pkg'; +function greet() {} +function entry() { + greet = fancyGreet; + greet(); +} +`, + ); + + expect(callableCallSiteLines(result, 'entry', 'greet')).toEqual([6]); + }, 60_000); + + it('unions the declaration with a resolvable reassignment target (inclusion semantics)', async () => { + const result = await runSource( + 'js', + ` +function greet() {} +function fancy() {} +function entry() { + greet = fancy; + greet(); +} +`, + ); + + expect(callableCallSiteLines(result, 'entry', 'greet')).toEqual([6]); + expect(callableCallSiteLines(result, 'entry', 'fancy')).toEqual([6]); + }, 60_000); + + it.each([ + { + language: 'Python', + extension: 'py', + source: ` +def target(): + pass + +def entry(enabled): + if enabled: + callback = target + callback() +`, + }, + { + language: 'PHP', + extension: 'php', + source: ` Unit) { callback() } +fun entry(enabled: Boolean) { + var chosen = ::other + if (enabled) { + chosen = ::target + } + invoke(chosen) +} +`, + }, + { + language: 'C#', + extension: 'cs', + source: ` +class Demo { + static void target() {} + static void entry(bool enabled) { + System.Action callback = () => {}; + if (enabled) { + callback = target; + } + callback(); + } +} +`, + }, + { + language: 'Swift', + extension: 'swift', + caller: 'invoke', + source: ` +func target() {} +func other() {} +func invoke(_ callback: () -> Void) { callback() } +func entry(enabled: Bool) { + var chosen = other + if enabled { + chosen = target + } + invoke(chosen) +} +`, + }, + { + language: 'Dart', + extension: 'dart', + source: ` +void target() {} + +void entry(bool enabled) { + var callback = () {}; + if (enabled) { + callback = target; + } + callback(); +} +`, + }, + ])( + 'keeps function-scoped callable assignments visible outside nested $language blocks', + async ({ extension, source, caller = 'entry' }) => { + const result = await runSource(extension, source); + expect(callsFrom(result, caller)).toContainEqual({ + target: 'target', + reason: 'callable-value-flow', + }); + }, + 60_000, + ); + + it('does not leak actuals between distinct C++ wrapper overloads', async () => { + const result = await runSource( + 'cpp', + ` +void target() {} +void invoke(void (*callback)()) { callback(); } +void invoke(void (*callback)(), int) { callback(); } +int entry() { + invoke(target); + return 0; +} +`, + ); + + const sourceParameterCounts = getRelationships(result, 'CALLS') + .filter((edge) => edge.target === 'target' && edge.rel.reason === 'callable-value-flow') + .map((edge) => result.graph.getNode(edge.rel.sourceId)?.properties.parameterCount) + .sort(); + expect(sourceParameterCounts).toEqual([1]); + }, 90_000); + + it('terminates copy cycles and preserves the reachable callable target', async () => { + const result = await runSource( + 'ts', + ` +function target(): void {} +let first = target; +let second = first; +first = second; +second = first; +second(); +`, + ); + + expect(callsFrom(result, 'main.ts')).toContainEqual({ + target: 'target', + reason: 'callable-value-flow', + }); + }, 60_000); + + it('suppresses rather than partially emitting an over-cap candidate set', async () => { + const targets = Array.from({ length: 33 }, (_, index) => `target${index}`); + const result = await runSource( + 'js', + [ + ...targets.map((name) => `function ${name}() {}`), + `let callback = ${targets[0]};`, + ...targets.slice(1).map((name) => `callback = ${name};`), + 'callback();', + ].join('\n'), + ); + + expect(callableTargetIds(result, 'main.js')).toEqual([]); + }, 60_000); + + it('does not steal an ordinary Java method whose name matches a callable protocol', async () => { + const result = await runSource( + 'java', + ` +class Worker { + void run() {} +} +class Demo { + static void entry() { + Worker worker = new Worker(); + worker.run(); + } +} +`, + ); + + expect(callsFrom(result, 'entry')).toContainEqual(expect.objectContaining({ target: 'run' })); + expect( + callsFrom(result, 'entry').some( + (edge) => edge.target === 'Worker' && edge.reason === 'callable-value-flow', + ), + ).toBe(false); + }, 60_000); + + it('does not treat a callable invocation result as the callable itself', async () => { + const result = await runSource( + 'ts', + ` +function target(): number { return 1; } +const value = target(); +value(); +`, + ); + + expect(callsFrom(result, 'main.ts')).not.toContainEqual({ + target: 'target', + reason: 'callable-value-flow', + }); + }, 60_000); + + it('replays identical callable-flow semantics from the durable warm parse cache', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-callable-warm-repo-')); + const storage = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-callable-warm-store-')); + try { + fs.writeFileSync( + path.join(root, 'main.ts'), + ` +function target(): void {} +function invoke(callback: () => void): void { callback(); } +const assigned = target; +invoke(assigned); +`, + 'utf8', + ); + const coldCache: ParseCache = { + version: PARSE_CACHE_VERSION, + entries: new Map(), + usedKeys: new Set(), + storagePath: storage, + onDiskKeys: new Set(), + }; + const cold = await runPipelineFromRepo(root, () => {}, { + skipGraphPhases: true, + parseCache: coldCache, + }); + const savedKeys = await saveParseCache(storage, coldCache); + await pruneAndSaveDurableParsedFileStore( + getDurableParsedFileDir(storage), + PARSE_CACHE_VERSION, + new Set(savedKeys), + ); + const warmCache = await loadParseCache(storage); + const warm = await runPipelineFromRepo(root, () => {}, { + skipGraphPhases: true, + parseCache: warmCache, + }); + const project = (result: Awaited>) => + getRelationships(result, 'CALLS') + .filter((edge) => edge.rel.reason === 'callable-value-flow') + .map( + (edge) => + `${edge.sourceFilePath}:${edge.source}->${edge.targetFilePath}:${edge.target}`, + ) + .sort(); + expect(project(warm)).toEqual(project(cold)); + expect(project(warm)).toEqual(['main.ts:invoke->main.ts:target']); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(storage, { recursive: true, force: true }); + } + }, 120_000); + + it('keeps normal/PDG targets identical and stamps calleeIds at the indirect invocation', async () => { + const source = ` +function target(): void {} +function invoke(callback: () => void): void { callback(); } +const assigned = target; +invoke(assigned); +`; + const normal = await runSource('ts', source); + const pdg = await runSource('ts', source, { pdg: true }); + const project = (result: Awaited>) => + callsFrom(result, 'invoke') + .filter((edge) => edge.reason === 'callable-value-flow') + .map((edge) => edge.target) + .sort(); + expect(project(pdg)).toEqual(project(normal)); + expect(project(pdg)).toEqual(['target']); + + const matchingBlocks: Array> = []; + pdg.graph.forEachNode((node) => { + if ( + node.label === 'BasicBlock' && + typeof node.properties.text === 'string' && + node.properties.text.includes('callback()') + ) { + matchingBlocks.push(node.properties); + } + }); + expect(matchingBlocks).not.toHaveLength(0); + expect( + matchingBlocks.some( + (properties) => + typeof properties.calleeIds === 'string' && properties.calleeIds.includes('target'), + ), + ).toBe(true); + }, 90_000); +}); diff --git a/gitnexus/test/integration/resolvers/typescript-value-refs.test.ts b/gitnexus/test/integration/resolvers/typescript-value-refs.test.ts new file mode 100644 index 000000000..50affb44c --- /dev/null +++ b/gitnexus/test/integration/resolvers/typescript-value-refs.test.ts @@ -0,0 +1,359 @@ +/** + * TypeScript/JavaScript: value-position function references (#2437). + * + * Registration (`{ emitScopeCaptures: emitHook }`, shorthand `{ emitHook }`) + * emits a reference-class USES edge — a registration is not an invocation. + * Invocation is recovered by the field-based property-dispatch pass: + * member-call sites `x.emitScopeCaptures()` gain synthesized CALLS edges + * (reason `property-dispatch`, discounted confidence) to every function + * registered under that property name. Non-callable values emit nothing + * (resolution is MethodRegistry-gated), and promiscuous keys past the + * fan-out cap are dropped entirely. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import path from 'path'; +import fs from 'node:fs'; +import os from 'node:os'; +import { + getRelationships, + runPipelineFromRepo, + type PipelineResult, + type RelEdge, +} from './helpers.js'; +import { + MAX_PROPERTY_DISPATCH_FANOUT, + PROPERTY_DISPATCH_CONFIDENCE, +} from '../../../src/core/ingestion/scope-resolution/passes/property-dispatch.js'; +import { _captureLogger, type PinoLogRecord } from '../../../src/core/logger.js'; + +function writeFixtureRepo(root: string, files: Record): void { + for (const [relPath, content] of Object.entries(files)) { + const fullPath = path.join(root, relPath); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, content, 'utf8'); + } +} + +function edgesFrom(edges: RelEdge[], sourceFile: string): RelEdge[] { + return edges.filter((c) => c.sourceFilePath === sourceFile); +} + +const VALUE_REF_REASON = 'scope-resolution: value-ref'; +const DISPATCH_REASON = 'property-dispatch'; + +describe('value-position function references (#2437)', () => { + let repoDir: string; + let result: PipelineResult; + let calls: RelEdge[]; + let uses: RelEdge[]; + let logRecords: PinoLogRecord[]; + + beforeAll(async () => { + repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-ts-value-refs-')); + const cappedFunctions = Array.from( + { length: MAX_PROPERTY_DISPATCH_FANOUT + 1 }, + (_, i) => + `export function capped${i}(): void {}\nexport const holder${i} = { common: capped${i} };`, + ).join('\n'); + writeFixtureRepo(repoDir, { + // Same-file provider object: hook value, plain value, factory call. + 'src/provider.ts': ` +export function emitHook(): void {} +export const DEFAULT_PORT = 8080; +export function createVisitor(): () => void { + return () => {}; +} +export function targetCallback(): void {} +export function invokeCallback(callback: () => void): void { callback(); } +export const provider = { + emitScopeCaptures: emitHook, + runWithCallback: invokeCallback, + port: DEFAULT_PORT, + cfgVisitor: createVisitor(), +}; +`, + // Dispatch through the property — the scope-extractor-bridge shape. + 'src/bridge.ts': ` +import { provider, targetCallback } from './provider'; +export function runBridge(): void { + provider.emitScopeCaptures(); + provider.runWithCallback(targetCallback); + provider.toString(); +} +`, + // Cross-file registration: the c-cpp.ts shape. + 'src/impl.ts': ` +export function emitCrossHook(): void {} +`, + 'src/registry.ts': ` +import { emitCrossHook } from './impl'; +export const registry = { emitScopeCaptures: emitCrossHook }; +`, + // Aliased import as pair value. + 'src/alias-registry.ts': ` +import { emitCrossHook as hookImpl } from './impl'; +export const aliasRegistry = { emitScopeCaptures: hookImpl }; +`, + // Shorthand registration + dispatch through it. + 'src/shorthand.ts': ` +import { emitCrossHook } from './impl'; +export const shorthandRegistry = { emitCrossHook }; +`, + 'src/dispatch-shorthand.ts': ` +import { shorthandRegistry } from './shorthand'; +export function runShorthand(): void { + shorthandRegistry.emitCrossHook(); +} +`, + // Destructuring shorthand must NOT produce a value-ref. + 'src/destructure.ts': ` +export function pickHook(): void {} +const holder = { pickHook }; +const { pickHook: picked } = holder; +export const keep = picked; +`, + // Fan-out cap: one key registered by MAX+1 distinct functions. + 'src/capped.ts': ` +${cappedFunctions} +`, + 'src/dispatch-capped.ts': ` +import { holder0 } from './capped'; +export function runCap(): void { + holder0.common(); +} +`, + // JavaScript twins (separate query file, same patterns). + 'src/js/provider.js': ` +export function emitJsHook() {} +export const DEFAULT_LIMIT = 5; +export const jsProvider = { emitScopeCaptures: emitJsHook, limit: DEFAULT_LIMIT }; +`, + 'src/js/shorthand.js': ` +import { emitJsHook } from './provider'; +export const jsShorthand = { emitJsHook }; +`, + 'src/js/bridge.js': ` +import { jsProvider } from './provider'; +export function runJsBridge() { + jsProvider.emitScopeCaptures(); +} +`, + }); + const loggerCapture = _captureLogger(); + try { + result = await runPipelineFromRepo(repoDir, () => {}, {}); + logRecords = loggerCapture.records(); + } finally { + loggerCapture.restore(); + } + calls = getRelationships(result, 'CALLS'); + uses = getRelationships(result, 'USES'); + }, 120000); + + afterAll(() => { + if (repoDir !== undefined) fs.rmSync(repoDir, { recursive: true, force: true }); + }); + + // ── Registration → USES ────────────────────────────────────────────────── + + it('emits USES from the provider file to a same-file longhand hook value', () => { + const edge = edgesFrom(uses, 'src/provider.ts').find((c) => c.target === 'emitHook'); + expect(edge).toBeDefined(); + expect(edge!.sourceLabel).toBe('File'); + expect(edge!.rel.reason).toBe(VALUE_REF_REASON); + }); + + it('does not emit CALLS at the registration site', () => { + expect( + edgesFrom(calls, 'src/provider.ts').find( + (c) => c.target === 'emitHook' && c.rel.reason === VALUE_REF_REASON, + ), + ).toBe(undefined); + }); + + it('emits nothing for a non-callable pair value', () => { + expect(edgesFrom(calls, 'src/provider.ts').find((c) => c.target === 'DEFAULT_PORT')).toBe( + undefined, + ); + expect( + edgesFrom(uses, 'src/provider.ts').find( + (c) => c.target === 'DEFAULT_PORT' && c.rel.reason === VALUE_REF_REASON, + ), + ).toBe(undefined); + }); + + it('keeps a factory-call pair value as a single call-site CALLS edge', () => { + const factory = edgesFrom(calls, 'src/provider.ts').filter((c) => c.target === 'createVisitor'); + expect(factory).toHaveLength(1); + expect(factory[0]!.rel.reason).not.toBe(DISPATCH_REASON); + }); + + it('emits USES across files for an imported hook (the c-cpp.ts shape)', () => { + const edge = edgesFrom(uses, 'src/registry.ts').find((c) => c.target === 'emitCrossHook'); + expect(edge).toBeDefined(); + expect(edge!.targetFilePath).toBe('src/impl.ts'); + expect(edge!.rel.reason).toBe(VALUE_REF_REASON); + }); + + it('resolves an aliased import pair value to the original definition', () => { + const edge = edgesFrom(uses, 'src/alias-registry.ts').find((c) => c.target === 'emitCrossHook'); + expect(edge).toBeDefined(); + expect(edge!.targetFilePath).toBe('src/impl.ts'); + }); + + it('emits USES for a shorthand property referencing an imported hook', () => { + const edge = edgesFrom(uses, 'src/shorthand.ts').find((c) => c.target === 'emitCrossHook'); + expect(edge).toBeDefined(); + expect(edge!.targetFilePath).toBe('src/impl.ts'); + }); + + it('emits exactly one value-ref USES per site (object shorthand), none for destructuring', () => { + const destructure = edgesFrom(uses, 'src/destructure.ts').filter( + (c) => c.target === 'pickHook' && c.rel.reason === VALUE_REF_REASON, + ); + expect(destructure).toHaveLength(1); + }); + + // ── Dispatch → synthesized CALLS ───────────────────────────────────────── + + it('synthesizes CALLS from the dispatch site to the registered hook', () => { + const edge = calls.find( + (c) => + c.source === 'runBridge' && c.target === 'emitHook' && c.rel.reason === DISPATCH_REASON, + ); + expect(edge).toBeDefined(); + expect(edge!.targetFilePath).toBe('src/provider.ts'); + expect(edge!.rel.confidence).toBe(PROPERTY_DISPATCH_CONFIDENCE); + }); + + it('propagates a callback through a property-dispatched wrapper before indirect resolution', () => { + const edge = calls.find( + (candidate) => + candidate.source === 'invokeCallback' && + candidate.target === 'targetCallback' && + candidate.rel.reason === 'callable-value-flow', + ); + expect(edge).toBeDefined(); + expect(edge!.sourceFilePath).toBe('src/provider.ts'); + expect(edge!.targetFilePath).toBe('src/provider.ts'); + }); + + it('dispatch fans out to every same-language registration of the property name', () => { + // `emitScopeCaptures` is registered in TypeScript by emitHook + // (provider.ts) and emitCrossHook (registry.ts + alias-registry.ts, + // deduped by def) — a TS call through the key reaches both. emitJsHook + // is NOT reached: scope resolution runs per language provider, so JS + // registrations are visible only to JS dispatch sites (see runJsBridge). + const targets = calls + .filter((c) => c.source === 'runBridge' && c.rel.reason === DISPATCH_REASON) + .map((c) => c.target) + .sort(); + expect(targets).toEqual(['emitCrossHook', 'emitHook', 'invokeCallback']); + }); + + it('dispatches through a shorthand-registered key', () => { + const edge = calls.find( + (c) => + c.source === 'runShorthand' && + c.target === 'emitCrossHook' && + c.rel.reason === DISPATCH_REASON, + ); + expect(edge).toBeDefined(); + }); + + it('does not synthesize CALLS for unregistered member calls', () => { + expect(calls.find((c) => c.target === 'toString' && c.rel.reason === DISPATCH_REASON)).toBe( + undefined, + ); + }); + + it('drops keys past the fan-out cap entirely', () => { + const cappedDispatch = calls.filter( + (c) => c.rel.reason === DISPATCH_REASON && c.target.startsWith('capped'), + ); + expect(cappedDispatch).toEqual([]); + + const capWarnings = logRecords.filter( + (record) => + record.msg === + 'property-dispatch: keys over the fan-out cap were dropped (no CALLS synthesized for them)', + ); + expect(capWarnings).toHaveLength(1); + expect(capWarnings[0]).toMatchObject({ + level: 40, + lang: 'typescript', + skippedKeys: 1, + fanoutCap: MAX_PROPERTY_DISPATCH_FANOUT, + }); + }); + + it('every property-dispatch edge targets a registered hook', () => { + const dispatchTargets = new Set( + calls.filter((c) => c.rel.reason === DISPATCH_REASON).map((c) => c.target), + ); + expect([...dispatchTargets].sort()).toEqual([ + 'emitCrossHook', + 'emitHook', + 'emitJsHook', + 'invokeCallback', + ]); + }); + + // ── JavaScript twins ───────────────────────────────────────────────────── + + it('emits USES for JavaScript longhand and shorthand hook values', () => { + const longhand = edgesFrom(uses, 'src/js/provider.js').find((c) => c.target === 'emitJsHook'); + expect(longhand).toBeDefined(); + expect(longhand!.rel.reason).toBe(VALUE_REF_REASON); + const shorthand = edgesFrom(uses, 'src/js/shorthand.js').find((c) => c.target === 'emitJsHook'); + expect(shorthand).toBeDefined(); + }); + + it('synthesizes CALLS from a JavaScript dispatch site', () => { + const edge = calls.find( + (c) => + c.source === 'runJsBridge' && c.target === 'emitJsHook' && c.rel.reason === DISPATCH_REASON, + ); + expect(edge).toBeDefined(); + }); + + it('emits nothing for a non-callable JavaScript pair value', () => { + expect(edgesFrom(calls, 'src/js/provider.js').find((c) => c.target === 'DEFAULT_LIMIT')).toBe( + undefined, + ); + }); +}); + +// --------------------------------------------------------------------------- +// Declarator-named arrows: `const handler = () => {}` referenced as a pair +// value. Pins whether the arrow def is callable-labeled (USES emitted) — +// kept as its own suite so the pinned behavior is visible in isolation. +// --------------------------------------------------------------------------- + +describe('value-position reference to a const-arrow handler (#2437)', () => { + let repoDir: string; + let result: PipelineResult; + + beforeAll(async () => { + repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-ts-value-refs-arrow-')); + writeFixtureRepo(repoDir, { + 'src/arrow.ts': ` +const handler = (): void => {}; +export const arrowRegistry = { onEvent: handler }; +`, + }); + result = await runPipelineFromRepo(repoDir, () => {}, {}); + }, 120000); + + afterAll(() => { + if (repoDir !== undefined) fs.rmSync(repoDir, { recursive: true, force: true }); + }); + + it('emits USES to the arrow function definition', () => { + const edge = getRelationships(result, 'USES').find( + (c) => c.sourceFilePath === 'src/arrow.ts' && c.target === 'handler', + ); + expect(edge).toBeDefined(); + expect(edge!.rel.reason).toBe(VALUE_REF_REASON); + }); +}); diff --git a/gitnexus/test/unit/call-summary-schema-version.test.ts b/gitnexus/test/unit/call-summary-schema-version.test.ts index 723cf3439..5d6c8674e 100644 --- a/gitnexus/test/unit/call-summary-schema-version.test.ts +++ b/gitnexus/test/unit/call-summary-schema-version.test.ts @@ -73,8 +73,8 @@ describe('CALL_SUMMARY relation-type exclusion (U-C1)', () => { }); describe('CALL_SUMMARY incremental reuse gate (U-C5)', () => { - it('INCREMENTAL_SCHEMA_VERSION is bumped to 6 (uniform 0-based line storage re-index window)', () => { - expect(INCREMENTAL_SCHEMA_VERSION).toBe(6); + it('INCREMENTAL_SCHEMA_VERSION is bumped to 7 (callable-value-flow edges re-index window)', () => { + expect(INCREMENTAL_SCHEMA_VERSION).toBe(7); }); it('a pre-current stamp fails the `=== INCREMENTAL_SCHEMA_VERSION` reuse gate → forces full re-analyze', () => { @@ -95,7 +95,11 @@ describe('CALL_SUMMARY incremental reuse gate (U-C5)', () => { // COBOL/JCL/markdown/scope rows are still 1-based, so an incremental top-up // would mix bases → must NOT reuse. expect(passesReuseGate(5)).toBe(false); + // A pre-v7 (v6) index predates the callable-value-flow edges (#2437/#2522) + // — new edges between unchanged files would never enter the incremental + // write set → must NOT reuse. + expect(passesReuseGate(6)).toBe(false); // A current-version stamp passes the gate (incremental top-up eligible). - expect(passesReuseGate(6)).toBe(true); + expect(passesReuseGate(7)).toBe(true); }); }); diff --git a/gitnexus/test/unit/parsedfile-store.test.ts b/gitnexus/test/unit/parsedfile-store.test.ts index 8f410d8a0..724423619 100644 --- a/gitnexus/test/unit/parsedfile-store.test.ts +++ b/gitnexus/test/unit/parsedfile-store.test.ts @@ -40,6 +40,19 @@ const makeParsedFile = (filePath: string): ParsedFile => ], }) as unknown as ParsedFile; +/** + * Store payload with arbitrary (possibly corrupt) field overrides. The one + * controlled escape hatch for building malformed serialization-boundary + * fixtures lives HERE instead of double-casts scattered through the tests + * (#2522 review). + */ +function makeStoreEntry(filePath: string, overrides: Record): ParsedFile { + return { + ...(makeParsedFile(filePath) as unknown as Record), + ...overrides, + } as unknown as ParsedFile; +} + describe('parsedfile-store', () => { it('round-trips ParsedFiles (incl. Scope Maps) and filters by requested paths', async () => { const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-')); @@ -102,6 +115,122 @@ describe('parsedfile-store', () => { } }); + it('round-trips validated callable-flow operand and signature metadata', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-')); + try { + const pf = makeStoreEntry('flow.cpp', { + callableFlowSites: [ + { + kind: 'seed', + destination: { + name: 'member', + inScope: 'scope:entry', + atRange: { startLine: 3, startCol: 2, endLine: 3, endCol: 8 }, + indirection: 0, + addressOf: false, + expressionKind: 'binding', + }, + targetName: 'run', + targetQualifiedName: 'Base.run', + targetRange: { startLine: 3, startCol: 12, endLine: 3, endCol: 21 }, + expectedSignature: { + parameterCount: 1, + parameterTypes: ['int'], + isConst: true, + }, + }, + ], + }); + await persistParsedFileChunk(dir, 'flow', [pf]); + + const loaded = await loadParsedFilesForPaths(dir, new Set(['flow.cpp'])); + expect(loaded.get('flow.cpp')?.callableFlowSites).toEqual(pf.callableFlowSites); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + it('drops a malformed callable-flow site but retains the file and its other sites (per-site sanitation, #2522)', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-')); + try { + const operand = { + name: 'callback', + inScope: 'scope:entry', + atRange: { startLine: 2, startCol: 2, endLine: 2, endCol: 10 }, + indirection: 17, + addressOf: false, + expressionKind: 'binding', + }; + const invalid = makeStoreEntry('invalid.c', { + callableFlowSites: [ + { + kind: 'invoke', + callSite: { startLine: 2, startCol: 2, endLine: 2, endCol: 12 }, + inScope: 'scope:entry', + callee: operand, + invocationKind: 'indirect', + arity: 0, + }, + ], + }); + await persistParsedFileChunk(dir, 'invalid', [invalid, makeParsedFile('valid.c')]); + + const loaded = await loadParsedFilesForPaths(dir, new Set(['invalid.c', 'valid.c'])); + // The file survives with the offending site dropped — a per-file + // rejection here caused a permanent, silent warm-cache reparse loop. + expect(loaded.get('invalid.c')?.callableFlowSites).toEqual([]); + expect(loaded.has('valid.c')).toBe(true); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + it('accepts empty-string parameterTypes entries ("" = unknown type, real C++ extractor output)', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-')); + try { + const pf = makeStoreEntry('cv.cpp', { + callableFlowSites: [ + { + kind: 'seed', + destination: { + name: 'fp', + inScope: 'scope:entry', + atRange: { startLine: 1, startCol: 0, endLine: 1, endCol: 8 }, + indirection: 0, + addressOf: false, + expressionKind: 'binding', + }, + targetName: 'handler', + targetRange: { startLine: 1, startCol: 12, endLine: 1, endCol: 19 }, + expectedSignature: { parameterCount: 2, parameterTypes: ['int', ''] }, + }, + ], + }); + await persistParsedFileChunk(dir, 'cv', [pf]); + + const loaded = await loadParsedFilesForPaths(dir, new Set(['cv.cpp'])); + expect(loaded.get('cv.cpp')?.callableFlowSites).toEqual(pf.callableFlowSites); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + it('rejects the whole file only when callableFlowSites is non-array garbage', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-')); + try { + const garbage = makeStoreEntry('garbage.c', { + callableFlowSites: 'not-an-array', + }); + await persistParsedFileChunk(dir, 'garbage', [garbage, makeParsedFile('ok.c')]); + + const loaded = await loadParsedFilesForPaths(dir, new Set(['garbage.c', 'ok.c'])); + expect(loaded.has('garbage.c')).toBe(false); + expect(loaded.has('ok.c')).toBe(true); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + // #1983 parallel serialization: the sync worker writer and the async writer // share one serialization core and MUST produce byte-identical shards (the // loader's deep-equals masks byte drift, so assert raw bytes). @@ -178,10 +307,9 @@ describe('parsedfile-store', () => { dependentPackBaseClasses: ['Mix'], }, }; - const pf = { - ...(makeParsedFile('app.cpp') as unknown as Record), + const pf = makeStoreEntry('app.cpp', { captureSideChannel: sideChannel, - } as unknown as ParsedFile; + }); persistParsedFileShardSync(dir, 'w1-0', [pf]); const loaded = await loadParsedFilesForPaths(dir, new Set(['app.cpp'])); @@ -204,10 +332,9 @@ describe('parsedfile-store', () => { kind: 'kotlin', companionScopes: ['scope:Logger.companion', 'scope:Animal.companion'], }; - const pf = { - ...(makeParsedFile('App.kt') as unknown as Record), + const pf = makeStoreEntry('App.kt', { captureSideChannel: sideChannel, - } as unknown as ParsedFile; + }); persistParsedFileShardSync(dir, 'w1-0', [pf]); const loaded = await loadParsedFilesForPaths(dir, new Set(['App.kt'])); @@ -228,10 +355,9 @@ describe('parsedfile-store', () => { const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-')); try { const sideChannel = { kind: 'c', staticNames: ['compute', 'helper'] }; - const pf = { - ...(makeParsedFile('local.c') as unknown as Record), + const pf = makeStoreEntry('local.c', { captureSideChannel: sideChannel, - } as unknown as ParsedFile; + }); persistParsedFileShardSync(dir, 'w1-0', [pf]); const loaded = await loadParsedFilesForPaths(dir, new Set(['local.c'])); diff --git a/gitnexus/test/unit/pdg-callee-id-capture.test.ts b/gitnexus/test/unit/pdg-callee-id-capture.test.ts index 6e769ecb0..ef2804f72 100644 --- a/gitnexus/test/unit/pdg-callee-id-capture.test.ts +++ b/gitnexus/test/unit/pdg-callee-id-capture.test.ts @@ -622,3 +622,17 @@ describe('callee-id accumulator — delete (R6 per-file release)', () => { expect(snapshot(acc, 'b.ts')).toEqual({ [calleeIdPosKey(5, 0)]: ['fn:b'] }); }); }); + +describe('callee-id accumulator — selective callable-flow capture', () => { + it('retains only positions accepted by the capture filter', () => { + const acc = createCalleeIdAccumulator( + (filePath, line, col) => filePath === 'wanted.ts' && line === 7 && col === 3, + ); + acc.add('wanted.ts', 7, 3, 'Function:wanted'); + acc.add('wanted.ts', 8, 3, 'Function:other-line'); + acc.add('other.ts', 7, 3, 'Function:other-file'); + + expect(snapshot(acc, 'wanted.ts')).toEqual({ '7:3': ['Function:wanted'] }); + expect(acc.get('other.ts')).toBeUndefined(); + }); +}); diff --git a/gitnexus/test/unit/scope-resolution/c/c-captures.test.ts b/gitnexus/test/unit/scope-resolution/c/c-captures.test.ts index 170620d5e..467f1503d 100644 --- a/gitnexus/test/unit/scope-resolution/c/c-captures.test.ts +++ b/gitnexus/test/unit/scope-resolution/c/c-captures.test.ts @@ -374,3 +374,15 @@ describe('emitCScopeCaptures — static storage class', () => { expect(isStaticName('a.c', 'get_buffer')).toBe(true); }); }); + +describe('emitCScopeCaptures — callable-flow signatures', () => { + it('variadic function-pointer signatures carry the "..." sentinel and no fixed arity (#2522)', () => { + const match = findMatch( + 'int vlog(const char *fmt, ...) { return 0; }\nint entry(void) { int (*emit)(const char *, ...) = vlog; return 0; }', + (tags) => tags.includes('@callable-flow.seed'), + ); + expect(match).toBeDefined(); + expect(match?.['@callable-flow.expected-types']?.text).toBe('["char","..."]'); + expect(match?.['@callable-flow.expected-arity']).toBeUndefined(); + }); +}); diff --git a/gitnexus/test/unit/scope-resolution/callable-flow-captures.test.ts b/gitnexus/test/unit/scope-resolution/callable-flow-captures.test.ts new file mode 100644 index 000000000..f63ea10c3 --- /dev/null +++ b/gitnexus/test/unit/scope-resolution/callable-flow-captures.test.ts @@ -0,0 +1,117 @@ +/** + * Direct unit tests for `synthesizeCallableFlowCaptures` — the shared + * AST-walking capture synthesizer behind every language's + * `*_CALLABLE_CAPTURE_OPTIONS` block (#2522 review: the 1,100-line producer + * had no test naming it; only downstream consumers were covered). + * + * Runs over the JavaScript grammar with a minimal options object so the + * assertions pin the SYNTHESIZER's semantics (assignment forms, member + * paths, subscripts, formals, produced-value guards), not any language's + * option tuning. + */ + +import { describe, it, expect } from 'vitest'; +import { synthesizeCallableFlowCaptures } from '../../../src/core/ingestion/utils/callable-flow-captures.js'; +import { getJsParser } from '../../../src/core/ingestion/languages/javascript/query.js'; + +const OPTIONS = { + functionNodeTypes: new Set(['function_declaration', 'arrow_function', 'function_expression']), + callNodeTypes: new Set(['call_expression']), + parameterListNodeTypes: new Set(['formal_parameters', 'arguments']), + parameterNodeTypes: new Set(['identifier', 'rest_pattern', 'assignment_pattern']), + bindingNodeTypes: new Set(['variable_declarator']), + assignmentNodeTypes: new Set(['assignment_expression']), + identifierNodeTypes: new Set(['identifier', 'property_identifier']), +} as const; + +function factsFor(src: string): Array> { + const tree = getJsParser().parse(src); + if (tree === null) throw new Error('parse failed'); + return synthesizeCallableFlowCaptures(tree.rootNode, OPTIONS).map((match) => { + const out: Record = {}; + for (const [tag, cap] of Object.entries(match)) { + if (cap !== undefined) out[tag] = cap.text; + } + return out; + }); +} + +function byTag(facts: Array>, tag: string): Array> { + return facts.filter((fact) => fact[tag] !== undefined); +} + +describe('synthesizeCallableFlowCaptures (shared synthesizer, #2522)', () => { + it('emits a seed for a declared-function initializer and an invoke at the call through it', () => { + const facts = factsFor('function target() {}\nconst h = target;\nh(1);\n'); + expect(byTag(facts, '@callable-flow.seed')).toMatchObject([ + { + '@callable-flow.destination': 'h', + '@callable-flow.target-name': 'target', + }, + ]); + expect(byTag(facts, '@callable-flow.invoke')).toMatchObject([ + { + '@callable-flow.callee': 'h', + '@callable-flow.invocation-kind': 'indirect', + '@callable-flow.arity': '1', + }, + ]); + }); + + it('emits one formal fact per parameter with its index', () => { + const facts = factsFor('function take(a, b) {}\n'); + expect(byTag(facts, '@callable-flow.formal')).toMatchObject([ + { '@callable-flow.binding': 'a', '@callable-flow.parameter-index': '0' }, + { '@callable-flow.binding': 'b', '@callable-flow.parameter-index': '1' }, + ]); + }); + + it('emits an argument fact for a function passed by name', () => { + const facts = factsFor('function target() {}\nfunction wire(cb) { cb(); }\nwire(target);\n'); + expect(byTag(facts, '@callable-flow.argument')).toMatchObject([ + { '@callable-flow.source': 'target', '@callable-flow.parameter-index': '0' }, + ]); + }); + + it('binds subscripted destinations and callees to the container, not the index (#2522 fix)', () => { + const facts = factsFor( + 'function target() {}\nfunction entry(i) {\n const tbl = [];\n tbl[i] = target;\n tbl[i]();\n}\n', + ); + expect(byTag(facts, '@callable-flow.seed')).toMatchObject([ + { '@callable-flow.destination': 'tbl', '@callable-flow.target-name': 'target' }, + ]); + expect(byTag(facts, '@callable-flow.invoke')).toMatchObject([ + { '@callable-flow.callee': 'tbl' }, + ]); + }); + + it('emits a member-call invoke only when the member cell has a visible store (#2522 fix)', () => { + const stored = factsFor('function target() {}\nfunction go(o) { o.run = target; o.run(); }\n'); + expect(byTag(stored, '@callable-flow.invoke')).toMatchObject([ + { '@callable-flow.callee': 'run' }, + ]); + + const unstored = factsFor('function go(map) { map.get("x"); }\n'); + expect(byTag(unstored, '@callable-flow.invoke')).toEqual([]); + }); + + it('treats call results as produced values, not callable designators', () => { + const facts = factsFor('function make() {}\nconst h = make();\n'); + expect(byTag(facts, '@callable-flow.seed')).toEqual([]); + expect(byTag(facts, '@callable-flow.copy')).toEqual([]); + }); + + it('suppresses bare-name sources entirely under bareNamesAreCalls (#2522 Ruby fix)', () => { + const src = 'function target() {}\nconst h = target;\n'; + const withDefault = factsFor(src); + expect(byTag(withDefault, '@callable-flow.seed')).toHaveLength(1); + + const tree = getJsParser().parse(src); + if (tree === null) throw new Error('parse failed'); + const suppressed = synthesizeCallableFlowCaptures(tree.rootNode, { + ...OPTIONS, + bareNamesAreCalls: true, + }); + expect(suppressed.filter((match) => match['@callable-flow.seed'] !== undefined)).toEqual([]); + }); +}); diff --git a/gitnexus/test/unit/scope-resolution/callable-value-flow-worklist.test.ts b/gitnexus/test/unit/scope-resolution/callable-value-flow-worklist.test.ts new file mode 100644 index 000000000..3a22dac63 --- /dev/null +++ b/gitnexus/test/unit/scope-resolution/callable-value-flow-worklist.test.ts @@ -0,0 +1,202 @@ +import { describe, expect, it } from 'vitest'; +import { + buildDefIndex, + buildMethodDispatchIndex, + buildModuleScopeIndex, + buildQualifiedNameIndex, + buildScopeTree, + type BindingRef, + type CallableFlowOperand, + type ParsedFile, + type Range, + type Scope, + type ScopeId, + type SymbolDefinition, +} from 'gitnexus-shared'; +import { createKnowledgeGraph } from '../../../src/core/graph/graph.js'; +import type { ScopeResolutionIndexes } from '../../../src/core/ingestion/model/scope-resolution-indexes.js'; +import { buildGraphNodeLookup } from '../../../src/core/ingestion/scope-resolution/graph-bridge/node-lookup.js'; +import { createCalleeIdAccumulator } from '../../../src/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.js'; +import { emitCallableValueFlow } from '../../../src/core/ingestion/scope-resolution/passes/callable-value-flow.js'; + +const FILE = 'chain.ts'; +const MODULE = 'scope:module' as ScopeId; +const ENTRY_SCOPE = 'scope:entry' as ScopeId; + +const range = (line: number): Range => ({ + startLine: line, + startCol: 0, + endLine: line, + endCol: 8, +}); + +const targetDef: SymbolDefinition = { + nodeId: 'def:chain.ts#1:0:Function:target', + filePath: FILE, + type: 'Function', + qualifiedName: 'target', + parameterCount: 0, +}; + +const entryDef: SymbolDefinition = { + nodeId: 'def:chain.ts#2:0:Function:entry', + filePath: FILE, + type: 'Function', + qualifiedName: 'entry', + parameterCount: 0, +}; + +function scope( + id: ScopeId, + parent: ScopeId | null, + kind: Scope['kind'], + ownedDefs: readonly SymbolDefinition[], + bindings: ReadonlyMap = new Map(), +): Scope { + return { + id, + parent, + kind, + range: range(1), + filePath: FILE, + bindings, + ownedDefs, + imports: [], + typeBindings: new Map(), + }; +} + +function indexes(scopes: readonly Scope[]): ScopeResolutionIndexes { + const defs = [targetDef, entryDef]; + return { + scopeTree: buildScopeTree([...scopes]), + defs: buildDefIndex(defs), + qualifiedNames: buildQualifiedNameIndex(defs), + moduleScopes: buildModuleScopeIndex([{ filePath: FILE, moduleScopeId: MODULE }]), + methodDispatch: buildMethodDispatchIndex({ + owners: [], + computeMro: () => [], + implementsOf: () => [], + }), + imports: new Map(), + bindings: new Map(), + bindingAugmentations: new Map(), + workspaceFqnBindings: new Map(), + workspaceTypeBindings: new Map(), + namespaceFqnBindings: new Map(), + namespaceTypeBindings: new Map(), + accessibleNamespacesByScope: new Map(), + referenceSites: [], + sccs: [], + stats: { + totalFiles: 1, + totalEdges: 0, + linkedEdges: 0, + unresolvedEdges: 0, + sccCount: 1, + largestSccSize: 1, + }, + }; +} + +function operand(name: string, line: number): CallableFlowOperand { + return { + name, + inScope: ENTRY_SCOPE, + atRange: range(line), + indirection: 0, + addressOf: false, + expressionKind: 'binding', + }; +} + +function runReverseChain(length: number) { + const moduleBindings = new Map([ + ['target', [{ def: targetDef }]], + ['entry', [{ def: entryDef }]], + ]); + const scopes = [ + scope(MODULE, null, 'Module', [targetDef, entryDef], moduleBindings), + scope(ENTRY_SCOPE, MODULE, 'Function', [entryDef]), + ]; + const callSite = range(length + 10); + const copies = Array.from({ length }, (_, index) => index) + .reverse() + .map((index) => ({ + kind: 'copy' as const, + source: operand(`value${index}`, index + 3), + destination: operand(`value${index + 1}`, index + 3), + })); + const parsed: ParsedFile = { + filePath: FILE, + moduleScope: MODULE, + scopes, + parsedImports: [], + localDefs: [targetDef, entryDef], + referenceSites: [ + { + name: `value${length}`, + atRange: callSite, + inScope: ENTRY_SCOPE, + kind: 'call', + callForm: 'free', + }, + ], + callableFlowSites: [ + { + kind: 'seed', + destination: operand('value0', 3), + targetName: 'target', + targetRange: range(1), + }, + ...copies, + { + kind: 'invoke', + callSite, + inScope: ENTRY_SCOPE, + callee: operand(`value${length}`, callSite.startLine), + invocationKind: 'indirect', + arity: 0, + }, + ], + }; + const graph = createKnowledgeGraph(); + graph.addNode({ + id: 'Function:chain.ts:target', + label: 'Function', + properties: { name: 'target', qualifiedName: 'target', filePath: FILE }, + }); + graph.addNode({ + id: 'Function:chain.ts:entry', + label: 'Function', + properties: { name: 'entry', qualifiedName: 'entry', filePath: FILE }, + }); + const result = emitCallableValueFlow({ + graph, + scopes: indexes(scopes), + parsedFiles: [parsed], + nodeLookup: buildGraphNodeLookup(graph), + calleeIds: createCalleeIdAccumulator(), + language: 'typescript', + }); + return { graph, result }; +} + +describe('callable-value-flow dependency worklist', () => { + it('scales linearly for reverse-ordered chains and resolves the terminal call', () => { + const n = runReverseChain(128); + const twoN = runReverseChain(256); + + expect(n.result.resolvedInvokes).toBe(1); + expect(twoN.result.resolvedInvokes).toBe(1); + expect(n.result.emitted).toBe(1); + expect(twoN.result.emitted).toBe(1); + expect(twoN.result.iterations).toBeLessThanOrEqual(n.result.iterations * 2 + 4); + expect(twoN.result.iterations).toBeLessThan(1_024); + expect( + [...twoN.graph.iterRelationshipsByType('CALLS')].map( + (relationship) => twoN.graph.getNode(relationship.targetId)?.properties.name, + ), + ).toEqual(['target']); + }); +}); diff --git a/gitnexus/test/unit/scope-resolution/cpp/cpp-captures.test.ts b/gitnexus/test/unit/scope-resolution/cpp/cpp-captures.test.ts index 29b224d27..d40fe2c62 100644 --- a/gitnexus/test/unit/scope-resolution/cpp/cpp-captures.test.ts +++ b/gitnexus/test/unit/scope-resolution/cpp/cpp-captures.test.ts @@ -527,3 +527,36 @@ describe('emitCppScopeCaptures — file-local linkage', () => { expect(isFileLocal('test.cpp', 'helper')).toBe(false); }); }); + +describe('emitCppScopeCaptures — callable-flow passing modes (#2522 review, M5)', () => { + function modesFor(src: string): Record { + const out: Record = {}; + for (const m of emitCppScopeCaptures(src, 'modes.cpp')) { + const binding = m['@callable-flow.binding']?.text; + if (m['@callable-flow.formal'] === undefined || binding === undefined) continue; + out[`${m['@callable-flow.owner']?.text}.${binding}`] = m['@callable-flow.passing-mode']?.text; + } + return out; + } + + it('classifies by the outermost declarator chain, never nested parameter lists', () => { + const modes = modesFor( + [ + 'void reg(void (*cb)(int& out)) {}', + 'void take(int& v) {}', + 'void raw(int* p) {}', + 'void val(int v) {}', + 'void refptr(void (*&cb)(int)) {}', + ].join('\n'), + ); + expect(modes).toMatchObject({ + // by-value pointer copy — the nested `int&` must not make it an alias + 'reg.cb': 'pointer', + 'take.v': 'reference', + 'raw.p': 'pointer', + 'val.v': 'value', + // reference-to-pointer IS an alias + 'refptr.cb': 'reference', + }); + }); +}); diff --git a/gitnexus/test/unit/scope-resolution/cpp/cpp-file-local-linkage.test.ts b/gitnexus/test/unit/scope-resolution/cpp/cpp-file-local-linkage.test.ts new file mode 100644 index 000000000..48d32eeac --- /dev/null +++ b/gitnexus/test/unit/scope-resolution/cpp/cpp-file-local-linkage.test.ts @@ -0,0 +1,56 @@ +/** + * Pins the `hasFileLocalCallableLinkage` contract (#2522 review, M2): the + * hook must answer language-level internal linkage ONLY. The name-keyed + * file-local set is populated from every `static` declaration in a file, so + * a class member sharing a name with a static free function was over-marked + * — but class members have EXTERNAL linkage even when declared `static` + * (in-class `static` means "no instance"), and refusing their cross-file + * declaration/definition join under-links member-pointer seeds. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import type { SymbolDefinition } from 'gitnexus-shared'; +import { cppScopeResolver } from '../../../../src/core/ingestion/languages/cpp/scope-resolver.js'; +import { + clearFileLocalNames, + markFileLocal, +} from '../../../../src/core/ingestion/languages/cpp/file-local-linkage.js'; + +describe('cppScopeResolver.hasFileLocalCallableLinkage (#2522 review, M2)', () => { + beforeEach(() => { + clearFileLocalNames(); + }); + + it('treats a static namespace-scope free function as file-local', () => { + markFileLocal('w.cpp', 'helper'); + const def: SymbolDefinition = { + nodeId: 'Function:w.cpp:helper#0', + filePath: 'w.cpp', + type: 'Function', + qualifiedName: 'helper', + }; + expect(cppScopeResolver.hasFileLocalCallableLinkage?.(def)).toBe(true); + }); + + it('never treats a class member as file-local, even when a same-named static free function over-marked the file', () => { + markFileLocal('w.cpp', 'helper'); + const method: SymbolDefinition = { + nodeId: 'Method:w.cpp:Widget.helper#0', + filePath: 'w.cpp', + type: 'Method', + qualifiedName: 'Widget.helper', + }; + expect(cppScopeResolver.hasFileLocalCallableLinkage?.(method)).toBe(false); + }); + + it('never treats a constructor as file-local', () => { + markFileLocal('w.cpp', 'Widget'); + const ctor: SymbolDefinition = { + nodeId: 'Constructor:w.cpp:Widget.Widget#0', + filePath: 'w.cpp', + type: 'Constructor', + qualifiedName: 'Widget.Widget', + }; + expect(cppScopeResolver.hasFileLocalCallableLinkage?.(ctor)).toBe(false); + }); +}); diff --git a/gitnexus/test/unit/scope-resolution/java/java-captures.test.ts b/gitnexus/test/unit/scope-resolution/java/java-captures.test.ts index f4d5d0251..36c88daca 100644 --- a/gitnexus/test/unit/scope-resolution/java/java-captures.test.ts +++ b/gitnexus/test/unit/scope-resolution/java/java-captures.test.ts @@ -117,3 +117,32 @@ describe('emitJavaScopeCaptures — explicit constructor invocations (F38 #1928) expect(refs.some((r) => r.name === 'E' && r.arity === '1')).toBe(true); }); }); + +describe('emitJavaScopeCaptures — callable-flow protocol methods (#2522 review)', () => { + function invokeFactsFor(src: string): number { + return emitJavaScopeCaptures(src, 'C.java').filter( + (m) => m['@callable-flow.invoke'] !== undefined, + ).length; + } + + it('does not emit invoke facts for ordinary container accessors', () => { + const src = ` +import java.util.HashMap; +class C { + static Object entry(HashMap map) { + return map.get("x"); + } +} +`; + expect(invokeFactsFor(src)).toBe(0); + }); + + it('still emits invoke facts for functional-interface dispatch', () => { + const src = ` +class C { + static void invoke(Runnable callback) { callback.run(); } +} +`; + expect(invokeFactsFor(src)).toBe(1); + }); +}); diff --git a/gitnexus/test/unit/scope-resolution/javascript/javascript-captures.test.ts b/gitnexus/test/unit/scope-resolution/javascript/javascript-captures.test.ts index cab9a4ef7..12cac2ff2 100644 --- a/gitnexus/test/unit/scope-resolution/javascript/javascript-captures.test.ts +++ b/gitnexus/test/unit/scope-resolution/javascript/javascript-captures.test.ts @@ -29,6 +29,18 @@ function countTag(src: string, tag: string): number { } describe('emitJsScopeCaptures — #1876 array-method-callback narrowing', () => { + it('keeps callable-value bindings lexical across sibling functions', () => { + const invokes = matchesFor( + [ + 'function target() {}', + 'function unrelated(target) { target(); }', + 'function entry() { target(); }', + ].join('\n'), + ).filter((match) => match['@callable-flow.invoke'] !== undefined); + + expect(invokes.map((match) => match['@callable-flow.invoke']!.range.startLine)).toEqual([2]); + }); + it('does not emit @declaration.function for `const x = arr.map(a => …)`', () => { const src = 'const exportData = accountsList.map(account => ({ id: account.id }));'; expect(hasDecl(src, '@declaration.const', 'exportData')).toBe(true); diff --git a/gitnexus/test/unit/scope-resolution/scope-extractor.test.ts b/gitnexus/test/unit/scope-resolution/scope-extractor.test.ts index a3e3bde8c..a8ebf4480 100644 --- a/gitnexus/test/unit/scope-resolution/scope-extractor.test.ts +++ b/gitnexus/test/unit/scope-resolution/scope-extractor.test.ts @@ -213,6 +213,25 @@ describe('Pass 1: scope tree', () => { // ─── §Pass 2: declarations + local bindings ──────────────────────────────── describe('Pass 2: declarations + local bindings', () => { + it('routes one multi-topic match through both scope and declaration passes', () => { + const result = extract( + [ + scopeMatch('module', 1, 0, 100, 0), + { + '@scope.function': cap('@scope.function', 5, 0, 20, 0, 'render'), + '@declaration.function': cap('@declaration.function', 5, 0, 20, 0, 'render'), + '@declaration.name': cap('@declaration.name', 5, 0, 5, 6, 'render'), + }, + ], + 'a.ts', + mockProvider(), + ); + + expect(result.scopes.some((scope) => scope.kind === 'Function')).toBe(true); + expect(result.localDefs).toHaveLength(1); + expect(result.localDefs[0]!.qualifiedName).toBe('render'); + }); + it('attaches a Class declaration to its enclosing Module scope', () => { const result = extract( [ @@ -473,6 +492,172 @@ describe('Pass 5: reference sites', () => { }); }); +// ─── §Pass 6: callable-value-flow facts ─────────────────────────────────── + +describe('Pass 6: callable-value-flow facts', () => { + it('omits callableFlowSites when the provider emits no flow captures', () => { + const result = extract([scopeMatch('module', 1, 0, 100, 0)], 'a.ts', mockProvider()); + expect(result.callableFlowSites).toBeUndefined(); + }); + + it('materializes every normalized fact shape with lexical scopes and JSON-safe metadata', () => { + const matches: CaptureMatch[] = [ + scopeMatch('module', 1, 0, 100, 0), + scopeMatch('function', 10, 0, 60, 0), + { + '@callable-flow.seed': cap('@callable-flow.seed', 20, 2, 20, 20), + '@callable-flow.destination': cap('@callable-flow.destination', 20, 2, 20, 4, 'fp'), + '@callable-flow.target': cap('@callable-flow.target', 20, 8, 20, 14, 'target'), + '@callable-flow.target-name': cap('@callable-flow.target-name', 20, 8, 20, 14, 'target'), + '@callable-flow.target-qualified-name': cap( + '@callable-flow.target-qualified-name', + 20, + 8, + 20, + 14, + 'Ns.target', + ), + '@callable-flow.expected-arity': cap('@callable-flow.expected-arity', 20, 2, 20, 2, '1'), + '@callable-flow.expected-types': cap( + '@callable-flow.expected-types', + 20, + 2, + 20, + 2, + '["int"]', + ), + '@callable-flow.expected-type-classes': cap( + '@callable-flow.expected-type-classes', + 20, + 2, + 20, + 2, + '[{"base":"int","cv":"none","indirection":"value","pointerDepth":0}]', + ), + }, + { + '@callable-flow.copy': cap('@callable-flow.copy', 21, 2, 21, 10), + '@callable-flow.source': cap('@callable-flow.source', 21, 8, 21, 10, 'fp'), + '@callable-flow.destination': cap('@callable-flow.destination', 21, 2, 21, 5, 'fp2'), + }, + { + '@callable-flow.alias': cap('@callable-flow.alias', 22, 2, 22, 10), + '@callable-flow.source': cap('@callable-flow.source', 22, 8, 22, 10, 'fp'), + '@callable-flow.destination': cap('@callable-flow.destination', 22, 2, 22, 5, 'ref'), + }, + { + '@callable-flow.address': cap('@callable-flow.address', 23, 2, 23, 12), + '@callable-flow.source': cap('@callable-flow.source', 23, 9, 23, 11, 'fp'), + '@callable-flow.destination': cap('@callable-flow.destination', 23, 2, 23, 6, 'slot'), + }, + { + '@callable-flow.store': cap('@callable-flow.store', 24, 2, 24, 14), + '@callable-flow.source': cap('@callable-flow.source', 24, 10, 24, 14, 'next'), + '@callable-flow.pointer': cap('@callable-flow.pointer', 24, 3, 24, 7, 'slot'), + '@callable-flow.pointer-indirection': cap( + '@callable-flow.pointer-indirection', + 24, + 3, + 24, + 3, + '1', + ), + }, + { + '@callable-flow.load': cap('@callable-flow.load', 25, 2, 25, 14), + '@callable-flow.pointer': cap('@callable-flow.pointer', 25, 10, 25, 14, 'slot'), + '@callable-flow.destination': cap('@callable-flow.destination', 25, 2, 25, 5, 'out'), + }, + { + '@callable-flow.formal': cap('@callable-flow.formal', 10, 0, 60, 0), + '@callable-flow.owner': cap('@callable-flow.owner', 10, 0, 60, 0, 'invoke'), + '@callable-flow.binding': cap('@callable-flow.binding', 10, 15, 10, 17, 'cb'), + '@callable-flow.parameter-index': cap( + '@callable-flow.parameter-index', + 10, + 15, + 10, + 15, + '0', + ), + '@callable-flow.passing-mode': cap( + '@callable-flow.passing-mode', + 10, + 15, + 10, + 15, + 'reference', + ), + }, + { + '@callable-flow.argument': cap('@callable-flow.argument', 30, 2, 30, 12), + '@callable-flow.source': cap('@callable-flow.source', 30, 9, 30, 11, 'fp'), + '@callable-flow.parameter-index': cap('@callable-flow.parameter-index', 30, 9, 30, 9, '0'), + '@callable-flow.direct-callee-name': cap( + '@callable-flow.direct-callee-name', + 30, + 2, + 30, + 8, + 'invoke', + ), + }, + { + '@callable-flow.invoke': cap('@callable-flow.invoke', 40, 2, 40, 14), + '@callable-flow.callee': cap('@callable-flow.callee', 40, 8, 40, 10, 'cb'), + '@callable-flow.receiver': cap('@callable-flow.receiver', 40, 3, 40, 6, 'obj'), + '@callable-flow.invocation-kind': cap( + '@callable-flow.invocation-kind', + 40, + 2, + 40, + 2, + 'member-pointer', + ), + '@callable-flow.arity': cap('@callable-flow.arity', 40, 2, 40, 2, '0'), + }, + // Malformed facts are ignored defensively. + { '@callable-flow.seed': cap('@callable-flow.seed', 50, 2, 50, 8) }, + ]; + + const result = extract(matches, 'a.ts', mockProvider()); + const sites = result.callableFlowSites!; + expect(sites.map((site) => site.kind)).toEqual([ + 'seed', + 'copy', + 'alias', + 'address', + 'store', + 'load', + 'formal', + 'argument', + 'invoke', + ]); + const fnScope = result.scopes.find((scope) => scope.kind === 'Function')!; + expect(sites[0]).toMatchObject({ + destination: { name: 'fp', inScope: fnScope.id, indirection: 0 }, + targetName: 'target', + targetQualifiedName: 'Ns.target', + expectedSignature: { parameterCount: 1, parameterTypes: ['int'] }, + }); + expect(sites[4]).toMatchObject({ pointer: { name: 'slot', indirection: 1 } }); + expect(sites[6]).toMatchObject({ + ownerName: 'invoke', + parameterIndex: 0, + passingMode: 'reference', + binding: { name: 'cb', inScope: fnScope.id }, + }); + expect(sites[7]).toMatchObject({ directCalleeName: 'invoke' }); + expect(sites[8]).toMatchObject({ + invocationKind: 'member-pointer', + arity: 0, + callee: { name: 'cb' }, + receiver: { name: 'obj' }, + }); + expect(JSON.parse(JSON.stringify(sites))).toEqual(sites); + }); +}); + // ─── §End-to-end fixture ────────────────────────────────────────────────── describe('end-to-end fixture (all 5 passes together)', () => { diff --git a/gitnexus/test/unit/scope-resolution/typescript/typescript-captures.test.ts b/gitnexus/test/unit/scope-resolution/typescript/typescript-captures.test.ts index 3ea48fb1e..c97d41e19 100644 --- a/gitnexus/test/unit/scope-resolution/typescript/typescript-captures.test.ts +++ b/gitnexus/test/unit/scope-resolution/typescript/typescript-captures.test.ts @@ -688,3 +688,42 @@ describe('emitTsScopeCaptures — #1876 array-method-callback narrowing', () => ).toBe(true); }); }); + +describe('emitTsScopeCaptures — value-position references (#2437)', () => { + it('captures a longhand pair value identifier as @reference.value-ref with its key', () => { + const match = findMatch( + 'function emitHook(): void {}\nexport const provider = { emitScopeCaptures: emitHook };', + (t) => t.includes('@reference.value-ref'), + ); + expect(match).toBeDefined(); + expect(match!['@reference.name'].text).toBe('emitHook'); + expect(match!['@reference.property-key'].text).toBe('emitScopeCaptures'); + }); + + it('captures a shorthand property as @reference.value-ref with key = name', () => { + const match = findMatch( + 'function emitHook(): void {}\nexport const provider = { emitHook };', + (t) => t.includes('@reference.value-ref'), + ); + expect(match).toBeDefined(); + expect(match!['@reference.name'].text).toBe('emitHook'); + expect(match!['@reference.property-key'].text).toBe('emitHook'); + }); + + it('does not fire on destructuring shorthand', () => { + expect( + countMatches('const o = { a: 1 };\nconst { a } = o;', (t) => + t.includes('@reference.value-ref'), + ), + ).toBe(0); + }); + + it('does not fire on a pair whose value is a call expression', () => { + expect( + countMatches( + 'function make(): number { return 1; }\nexport const provider = { cfgVisitor: make() };', + (t) => t.includes('@reference.value-ref'), + ), + ).toBe(0); + }); +}); From 4d7a0a69edcc87ee2b2046aba7fbe8040c0de3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Sat, 18 Jul 2026 08:44:09 +0100 Subject: [PATCH 17/24] fix(analyze): degrade FTS search instead of aborting analyze on index-build failure (#2548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(analyze): degrade FTS search instead of aborting analyze on index-build failure createSearchFTSIndexes re-tokenizes every stored row on every analyze run (full or incremental). A native LadybugDB tokenizer error on a single pre-existing row (e.g. "Failed calling LOWER: Invalid UTF-8") previously propagated uncaught out of run-analyze.ts's main FTS phase, discarding an otherwise-successful run's graph/embeddings work every time analyze ran thereafter. Add buildSearchIndexesOrDegrade(), which catches build/verify failures and lets analyze finish with keyword search degraded for that run instead — mirroring the existing sibling degrade path for a missing FTS extension. The dedicated --repair-fts path is untouched and still fails loudly. Fixes #2544, #2546. * fix(analyze): keep capabilities.fts/ftsSkipped honest when index build degrades ftsSkipped and capabilities.fts.status were keyed only on ftsAvailable (extension loaded), which the new degrade path leaves true even when the index build itself failed. Track that outcome in ftsReady and use it for both, and update run-analyze-fts-repair.test.ts's coverage of this path from asserting the old throw to asserting the new degrade contract (ftsSkipped, log message, meta.json capabilities.fts.status). --- gitnexus/src/core/run-analyze.ts | 30 +++++++++---- gitnexus/src/core/search/fts-indexes.ts | 31 +++++++++++++ gitnexus/test/unit/fts-indexes.test.ts | 44 ++++++++++++++++++- .../test/unit/run-analyze-fts-repair.test.ts | 40 +++++++++++------ 4 files changed, 120 insertions(+), 25 deletions(-) diff --git a/gitnexus/src/core/run-analyze.ts b/gitnexus/src/core/run-analyze.ts index 18f9f37aa..bfbcb152f 100644 --- a/gitnexus/src/core/run-analyze.ts +++ b/gitnexus/src/core/run-analyze.ts @@ -36,6 +36,7 @@ import { } from './lbug/lbug-adapter.js'; import { escapeCypherString } from './lbug/cypher-escape.js'; import { + buildSearchIndexesOrDegrade, createSearchFTSIndexes, initialiseSearchFTSStemmer, verifySearchFTSIndexes, @@ -1623,8 +1624,17 @@ export async function runFullAnalysis( const ftsAvailable = await loadFTSExtension(undefined, { policy: resolveAnalyzeInstallPolicy(), }); + // Tracks whether search indexes actually ended up usable this run — starts + // as ftsAvailable (extension loaded) but flips to false below when the + // build/verify step itself fails, so capabilities.fts.status / ftsSkipped + // stay honest even though that failure no longer aborts the whole analyze. + let ftsReady = ftsAvailable; if (ftsAvailable) { - await createSearchFTSIndexes({ + // Degrade rather than throw: createSearchFTSIndexes re-tokenizes every + // stored row on every run, so a native tokenizer error on a single + // pre-existing row (#2544/#2546) must not discard this run's otherwise- + // successful graph/embeddings work — only keyword search degrades. + const ftsResult = await buildSearchIndexesOrDegrade(executeQuery, { onIndexStart: options.verbose ? (table, indexName) => log(`FTS: creating ${table}.${indexName}`) : undefined, @@ -1632,14 +1642,16 @@ export async function runFullAnalysis( ? (table, indexName) => log(`FTS: ready ${table}.${indexName}`) : undefined, }); - const missingIndexNames = await verifySearchFTSIndexes(executeQuery); - if (missingIndexNames.length > 0) { - throw new Error( - `FTS verification failed - missing indexes after analyze: ${missingIndexNames.join(', ')}. ` + - 'Check FTS extension availability, then retry `gitnexus analyze --force` for a full rebuild.', + if (ftsResult.ok) { + progress('fts', 90, 'Search indexes ready'); + } else { + ftsReady = false; + log( + `FTS index build failed (${ftsResult.error}) — keyword search degraded this run. ` + + 'Graph and embeddings analysis completed successfully. Run `gitnexus analyze --repair-fts` to retry.', ); + progress('fts', 90, 'Search indexes skipped (build failed)'); } - progress('fts', 90, 'Search indexes ready'); } else { // For a missing runtime dependency (#2374) the file is present, so the // generic "install it with network access" tail in FTS_UNAVAILABLE_MESSAGE @@ -2036,7 +2048,7 @@ export async function runFullAnalysis( // meta.json / `gitnexus doctor` honest about degraded search. fts: { provider: 'ladybugdb-fts', - status: ftsAvailable ? runtimeCapabilities.fts : 'unavailable', + status: ftsReady ? runtimeCapabilities.fts : 'unavailable', }, vectorSearch: { provider: effectiveSemanticMode === 'vector-index' ? 'ladybugdb-vector' : 'exact-scan', @@ -2216,7 +2228,7 @@ export async function runFullAnalysis( repoPath, stats: meta.stats, pipelineResult, - ftsSkipped: !ftsAvailable, + ftsSkipped: !ftsReady, isPrimaryBranch: !placement.branch, }; } catch (err) { diff --git a/gitnexus/src/core/search/fts-indexes.ts b/gitnexus/src/core/search/fts-indexes.ts index cf0de50f9..dfc4f2eeb 100644 --- a/gitnexus/src/core/search/fts-indexes.ts +++ b/gitnexus/src/core/search/fts-indexes.ts @@ -178,3 +178,34 @@ export async function verifySearchFTSIndexes( } return missing; } + +export interface BuildSearchIndexesResult { + ok: boolean; + error?: string; +} + +/** + * Build + verify FTS indexes, catching any failure instead of letting it + * propagate. `createSearchFTSIndexes` re-tokenizes every stored row on every + * analyze run (see the `ponytail:` comment above) — a native LadybugDB + * tokenizer error on a single pre-existing row (e.g. a "Failed calling + * LOWER: Invalid UTF-8", #2544/#2546) must not discard an otherwise- + * successful analyze's graph/embeddings work. The caller degrades keyword + * search for this run instead, mirroring the existing FTS-extension- + * unavailable degrade path in `run-analyze.ts`. + */ +export async function buildSearchIndexesOrDegrade( + executeQuery: (cypher: string) => Promise, + options?: CreateSearchFTSIndexesOptions, +): Promise { + try { + await createSearchFTSIndexes(options); + const missing = await verifySearchFTSIndexes(executeQuery); + if (missing.length > 0) { + return { ok: false, error: `missing indexes after build: ${missing.join(', ')}` }; + } + return { ok: true }; + } catch (e) { + return { ok: false, error: e instanceof Error ? e.message : String(e) }; + } +} diff --git a/gitnexus/test/unit/fts-indexes.test.ts b/gitnexus/test/unit/fts-indexes.test.ts index c104d742b..259c745c6 100644 --- a/gitnexus/test/unit/fts-indexes.test.ts +++ b/gitnexus/test/unit/fts-indexes.test.ts @@ -15,9 +15,18 @@ vi.mock('../../src/core/lbug/lbug-adapter.js', () => ({ ), })); -const { createSearchFTSIndexes, getSearchFTSStemmer, initialiseSearchFTSStemmer } = - await import('../../src/core/search/fts-indexes.js'); +const { + buildSearchIndexesOrDegrade, + createSearchFTSIndexes, + getSearchFTSStemmer, + initialiseSearchFTSStemmer, +} = await import('../../src/core/search/fts-indexes.js'); const { FTS_INDEXES } = await import('../../src/core/search/fts-schema.js'); +const { createFTSIndex } = await import('../../src/core/lbug/lbug-adapter.js'); + +/** SHOW_INDEXES rows covering every configured FTS index's expected properties. */ +const fullCoverageRows = () => + FTS_INDEXES.map((i) => ({ index_name: i.indexName, property_names: [...i.properties] })); afterEach(() => { calls.length = 0; @@ -65,6 +74,37 @@ describe('createSearchFTSIndexes', () => { }); }); +describe('buildSearchIndexesOrDegrade', () => { + it('returns ok:true when every index builds and verifies (#2544/#2546)', async () => { + const executeQuery = vi.fn(async () => fullCoverageRows()); + + const result = await buildSearchIndexesOrDegrade(executeQuery); + + expect(result).toEqual({ ok: true }); + }); + + it('returns ok:false instead of throwing when a single index build rejects (#2544/#2546)', async () => { + vi.mocked(createFTSIndex).mockRejectedValueOnce( + new Error('Runtime exception: Failed calling LOWER: Invalid UTF-8.'), + ); + const executeQuery = vi.fn(async () => fullCoverageRows()); + + const result = await buildSearchIndexesOrDegrade(executeQuery); + + expect(result.ok).toBe(false); + expect(result.error).toContain('Invalid UTF-8'); + }); + + it('returns ok:false when verification finds a missing index, without throwing', async () => { + const executeQuery = vi.fn(async () => fullCoverageRows().slice(1)); + + const result = await buildSearchIndexesOrDegrade(executeQuery); + + expect(result.ok).toBe(false); + expect(result.error).toContain('missing indexes'); + }); +}); + describe('getSearchFTSStemmer', () => { it('defaults to porter when unset', () => { expect(getSearchFTSStemmer()).toBe('porter'); diff --git a/gitnexus/test/unit/run-analyze-fts-repair.test.ts b/gitnexus/test/unit/run-analyze-fts-repair.test.ts index b0c0b4c9d..eb524305f 100644 --- a/gitnexus/test/unit/run-analyze-fts-repair.test.ts +++ b/gitnexus/test/unit/run-analyze-fts-repair.test.ts @@ -459,7 +459,11 @@ describe('runFullAnalysis FTS repair and verification failure paths', () => { } }); - it('fails full analyze when FTS verification reports missing indexes after creation', async () => { + it('degrades gracefully (no throw, warns, ftsSkipped) when FTS verification reports missing indexes after creation (#2544/#2546)', async () => { + // A native tokenizer error on one pre-existing row (the #2544/#2546 + // failure mode) must not abort an otherwise-successful full analyze — + // it degrades keyword search for this run instead, same contract as the + // FTS-extension-unavailable sibling test below. vi.doMock('../../src/core/lbug/lbug-adapter.js', () => ({ initLbug: vi.fn(async () => undefined), loadGraphToLbug: vi.fn(async () => undefined), @@ -480,34 +484,42 @@ describe('runFullAnalysis FTS repair and verification failure paths', () => { deleteAllCommunitiesAndProcesses: vi.fn(async () => undefined), queryImporters: vi.fn(async () => []), queryImportersBatch: vi.fn(async () => []), - // FTS extension loads → analyze proceeds to create + verify indexes. + // FTS extension loads → analyze proceeds to build + verify indexes. loadFTSExtension: vi.fn(async () => true), })); vi.doMock('../../src/core/search/fts-indexes.js', () => ({ initialiseSearchFTSStemmer: vi.fn(() => 'porter'), - createSearchFTSIndexes: vi.fn(async () => undefined), - verifySearchFTSIndexes: vi.fn(async () => ['Function.function_fts']), + buildSearchIndexesOrDegrade: vi.fn(async () => ({ + ok: false, + error: 'missing indexes after build: Function.function_fts', + })), })); vi.doMock('../../src/core/ingestion/pipeline.js', () => ({ runPipelineFromRepo: vi.fn(async (repoPath: string) => ({ repoPath, - // Full-analyze path only needs `forEachNode` before the FTS verify guard. + // Full-analyze path only needs `forEachNode` before the FTS phase. graph: { forEachNode: () => undefined }, })), })); const tmpRepo = await createTempDir('gitnexus-run-analyze-full-verify-fail-'); try { + const logs: string[] = []; const { runFullAnalysis } = await import('../../src/core/run-analyze.js'); - await expect( - runFullAnalysis( - tmpRepo.dbPath, - { force: true }, - { - onProgress: () => {}, - }, - ), - ).rejects.toThrow(/FTS verification failed - missing indexes after analyze/i); + const result = await runFullAnalysis( + tmpRepo.dbPath, + { force: true }, + { onProgress: () => {}, onLog: (msg: string) => logs.push(msg) }, + ); + + expect(result.ftsSkipped).toBe(true); + expect(logs.join('\n')).toMatch( + /FTS index build failed.*missing indexes after build.*keyword search degraded this run/i, + ); + + const { storagePath } = getStoragePaths(tmpRepo.dbPath); + const meta = JSON.parse(await fs.readFile(`${storagePath}/meta.json`, 'utf-8')); + expect(meta.capabilities.fts.status).toBe('unavailable'); } finally { await tmpRepo.cleanup(); } From 1abcac9c1630b66038fa611005cbd37266a7a79f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Sat, 18 Jul 2026 14:30:37 +0100 Subject: [PATCH 18/24] fix(scope-resolution): stop platform builtins resolving to unrelated same-file symbols (#2549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(scope-resolution): stop platform builtins resolving to unrelated same-file symbols (#2545) An unqualified call to a platform/language builtin (e.g. TypeScript's global fetch()) could resolve to an unrelated same-file declaration sharing that name, most visibly a Cloudflare Worker's `export default { async fetch(req) {...} }` handler. Two contributing gaps, both fixed: - Object literals had no scope boundary in the TS/JS grammar queries, so a method's/property-arrow's name auto-hoisted past the literal into whatever lexically enclosed it (scope-extractor.ts's auto-hoist logic had nowhere to stop). Give object literals a Block scope, like 6 other languages already do for lexical blocks. - Independently, finalize's per-file bindings bucket (materializeBindings in gitnexus-shared) flattens every local declaration in a file onto its module scope for cross-file import resolution, regardless of true nesting -- so free-call-fallback's scope-chain walk could still hit the leaked binding at module scope. Guard free-call resolution: when a match for a known builtin name (LanguageProvider.isBuiltInName, already populated for TS/JS but never consulted by this pass) has no binding reachable via the true lexical scope chain, leave the call unresolved instead of emitting a false CALLS edge. Verified against the full TS/JS resolver suites plus every other language populating builtInNames (Python, Go, C/C++, C#, Dart, Kotlin, PHP, Ruby, Rust, Swift, Vue) -- 2333 tests, no regressions. Co-Authored-By: Claude Sonnet 5 * fix(scope-resolution): extend the #2545 scope-leak fix to Kotlin and Java Anonymous object-expressions (Kotlin `object { ... }`) and anonymous class bodies (Java `new Runnable() { ... }`) have the same missing scope-boundary gap that caused #2545 in TypeScript/JavaScript: a method declared inside has no scope of its own to stop the auto-hoist at, so its name leaks past the container into the enclosing scope. - Kotlin: `(object_literal) @scope.class` (distinct from the already- scoped named `object_declaration`/`companion_object`). Kotlin already populates `builtInNames`, so free-call-fallback's isBuiltInName guard (added for #2545) fully closes the equivalent leak here too -- verified with a `println`-shadowing regression test. - Java: `(object_creation_expression (class_body) @scope.class)`, matching PHP's existing `anonymous_class` handling. Java has no `builtInNames` list, so the isBuiltInName guard doesn't engage -- the scope-tree fix is still correct and necessary (the anonymous class's own methods are now owned by the right scope), but an unqualified call to an unrelated same-file method sharing the anonymous class's method name can still resolve via finalize's per-file module-scope bucket (materializeBindings, shared/ language-agnostic, intentionally not touched by this PR). Documented in the test as a known residual gap, same as TS/JS/Kotlin's own non-builtin-name collisions. Audited every other language for the same shape (a value/container node with no @scope.* capture hosting a would-be-auto-hoisted named declaration): PHP and Vue already handle it correctly (PHP scopes anonymous_class; Vue's