kodingo-cli 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/csharpParser.test.js +187 -0
- package/dist/__tests__/vendoredCodeDetector.test.js +98 -0
- package/dist/__tests__/vendoredCodeFiltering.test.js +121 -0
- package/dist/commands/scan-repo.js +23 -3
- package/dist/utils/ast-parser.js +1 -1
- package/dist/utils/graph-builder.js +14 -3
- package/dist/utils/tree-sitter-parser.js +26 -0
- package/dist/utils/vendoredCodeDetector.js +145 -0
- package/package.json +2 -2
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Real tests for C#/.NET support in tree-sitter-parser.ts / ast-parser.ts —
|
|
4
|
+
* build brief "Add real C#/.NET support to the CLI's parser" (2026-09-20).
|
|
5
|
+
*
|
|
6
|
+
* The real bar per the brief: not just "C# symbols get extracted", but
|
|
7
|
+
* genuine, stable deduplication — the same, unchanged function produces a
|
|
8
|
+
* byte-for-byte identical fingerprint across repeated runs, and a genuinely
|
|
9
|
+
* changed function's fingerprint correctly changes.
|
|
10
|
+
*
|
|
11
|
+
* Uses Node's built-in test runner (node:test) — this repo has no existing
|
|
12
|
+
* test framework to inherit (confirmed: package.json's own "test" script
|
|
13
|
+
* was a stub), and node:test needs zero new dependencies. Compiled and run
|
|
14
|
+
* via `npm test` (tsc then `node --test dist/__tests__`).
|
|
15
|
+
*/
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
const node_test_1 = require("node:test");
|
|
21
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
22
|
+
const tree_sitter_parser_1 = require("../utils/tree-sitter-parser");
|
|
23
|
+
const ast_parser_1 = require("../utils/ast-parser");
|
|
24
|
+
const REAL_CSHARP_SOURCE = `
|
|
25
|
+
using System;
|
|
26
|
+
using System.Threading.Tasks;
|
|
27
|
+
|
|
28
|
+
namespace Acme.Orders
|
|
29
|
+
{
|
|
30
|
+
public class OrderService : IOrderService
|
|
31
|
+
{
|
|
32
|
+
private readonly IRepository _repo;
|
|
33
|
+
|
|
34
|
+
public OrderService(IRepository repo)
|
|
35
|
+
{
|
|
36
|
+
_repo = repo;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async Task<Order> GetOrderAsync(int id)
|
|
40
|
+
{
|
|
41
|
+
var order = await _repo.FindAsync(id);
|
|
42
|
+
LogAccess(id);
|
|
43
|
+
return order;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private void LogAccess(int id)
|
|
47
|
+
{
|
|
48
|
+
Console.WriteLine($"Accessed order {id}");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public static int Add(int a, int b)
|
|
52
|
+
{
|
|
53
|
+
if (a > 0)
|
|
54
|
+
{
|
|
55
|
+
return a + b;
|
|
56
|
+
}
|
|
57
|
+
return b;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public interface IOrderService
|
|
62
|
+
{
|
|
63
|
+
Task<Order> GetOrderAsync(int id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public struct Point
|
|
67
|
+
{
|
|
68
|
+
public int Sum(int x, int y) => x + y;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public record OrderDto(int Id, string Name)
|
|
72
|
+
{
|
|
73
|
+
public string Describe() => $"{Id}: {Name}";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public class Order { }
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
// A real, structural change to GetOrderAsync — a distinct code path added,
|
|
80
|
+
// not a cosmetic rename/whitespace/comment tweak (those are what
|
|
81
|
+
// computeFingerprint is specifically designed to SURVIVE).
|
|
82
|
+
const CHANGED_CSHARP_SOURCE = REAL_CSHARP_SOURCE.replace(`public async Task<Order> GetOrderAsync(int id)
|
|
83
|
+
{
|
|
84
|
+
var order = await _repo.FindAsync(id);
|
|
85
|
+
LogAccess(id);
|
|
86
|
+
return order;
|
|
87
|
+
}`, `public async Task<Order> GetOrderAsync(int id)
|
|
88
|
+
{
|
|
89
|
+
var order = await _repo.FindAsync(id);
|
|
90
|
+
LogAccess(id);
|
|
91
|
+
if (order == null) throw new InvalidOperationException("not found");
|
|
92
|
+
return order;
|
|
93
|
+
}`);
|
|
94
|
+
(0, node_test_1.describe)("parseWithTreeSitter — real C# extraction", () => {
|
|
95
|
+
(0, node_test_1.it)("extracts every real method, constructor, and local function with class-qualified names", async () => {
|
|
96
|
+
const symbols = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
97
|
+
const names = symbols.map(s => s.name).sort();
|
|
98
|
+
strict_1.default.deepEqual(names, [
|
|
99
|
+
"IOrderService.GetOrderAsync",
|
|
100
|
+
"OrderDto.Describe",
|
|
101
|
+
"OrderService.Add",
|
|
102
|
+
"OrderService.GetOrderAsync",
|
|
103
|
+
"OrderService.LogAccess",
|
|
104
|
+
"OrderService.OrderService",
|
|
105
|
+
"Point.Sum",
|
|
106
|
+
].sort());
|
|
107
|
+
});
|
|
108
|
+
(0, node_test_1.it)("real, correct method vs constructor typing", async () => {
|
|
109
|
+
const symbols = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
110
|
+
const ctor = symbols.find(s => s.name === "OrderService.OrderService");
|
|
111
|
+
const method = symbols.find(s => s.name === "OrderService.GetOrderAsync");
|
|
112
|
+
strict_1.default.equal(ctor?.type, "method");
|
|
113
|
+
strict_1.default.equal(method?.type, "method");
|
|
114
|
+
});
|
|
115
|
+
(0, node_test_1.it)("real call extraction trims member-access callees to their trailing identifier", async () => {
|
|
116
|
+
const symbols = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
117
|
+
const method = symbols.find(s => s.name === "OrderService.GetOrderAsync");
|
|
118
|
+
strict_1.default.ok(method);
|
|
119
|
+
strict_1.default.ok(method.calls.includes("FindAsync"), `expected FindAsync in ${JSON.stringify(method.calls)}`);
|
|
120
|
+
strict_1.default.ok(method.calls.includes("LogAccess"), `expected LogAccess in ${JSON.stringify(method.calls)}`);
|
|
121
|
+
});
|
|
122
|
+
(0, node_test_1.it)("real using-directive extraction", async () => {
|
|
123
|
+
const symbols = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
124
|
+
strict_1.default.ok(symbols[0].imports.some(i => i.source.includes("using System;")));
|
|
125
|
+
strict_1.default.ok(symbols[0].imports.some(i => i.source.includes("using System.Threading.Tasks;")));
|
|
126
|
+
});
|
|
127
|
+
(0, node_test_1.it)("returns [] for an unrecognized extension rather than throwing", async () => {
|
|
128
|
+
const symbols = await (0, tree_sitter_parser_1.parseWithTreeSitter)("readme.md", "# not code");
|
|
129
|
+
strict_1.default.deepEqual(symbols, []);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
(0, node_test_1.describe)("real deduplication — the actual bar this brief is judged against", () => {
|
|
133
|
+
(0, node_test_1.it)("the SAME, unchanged C# function produces a byte-for-byte identical fingerprint across repeated runs", async () => {
|
|
134
|
+
const run1 = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
135
|
+
const run2 = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
136
|
+
const run3 = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
137
|
+
const fp1 = run1.find(s => s.name === "OrderService.GetOrderAsync")?.fingerprint;
|
|
138
|
+
const fp2 = run2.find(s => s.name === "OrderService.GetOrderAsync")?.fingerprint;
|
|
139
|
+
const fp3 = run3.find(s => s.name === "OrderService.GetOrderAsync")?.fingerprint;
|
|
140
|
+
strict_1.default.ok(fp1);
|
|
141
|
+
strict_1.default.equal(fp1, fp2);
|
|
142
|
+
strict_1.default.equal(fp2, fp3);
|
|
143
|
+
// Every single real symbol in the file, not just one — genuine, stable
|
|
144
|
+
// dedup across the whole file, not a cherry-picked case.
|
|
145
|
+
for (let i = 0; i < run1.length; i++) {
|
|
146
|
+
strict_1.default.equal(run1[i].fingerprint, run2[i].fingerprint, `symbol ${run1[i].name} fingerprint drifted across runs`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
(0, node_test_1.it)("via the real, full parseFile dispatch (the actual path scan-repo.ts calls) — same stability", async () => {
|
|
150
|
+
const run1 = await (0, ast_parser_1.parseFile)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
151
|
+
const run2 = await (0, ast_parser_1.parseFile)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
152
|
+
strict_1.default.ok(run1.length > 0);
|
|
153
|
+
strict_1.default.deepEqual(run1.map(s => s.fingerprint), run2.map(s => s.fingerprint));
|
|
154
|
+
});
|
|
155
|
+
(0, node_test_1.it)("cosmetic-only changes (comments, whitespace) do NOT change the fingerprint — by design", async () => {
|
|
156
|
+
const cosmetic = REAL_CSHARP_SOURCE.replace(`public static int Add(int a, int b)`, `// adds two numbers\n public static int Add(int a, int b)`);
|
|
157
|
+
const before = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
158
|
+
const after = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", cosmetic);
|
|
159
|
+
const fpBefore = before.find(s => s.name === "OrderService.Add")?.fingerprint;
|
|
160
|
+
const fpAfter = after.find(s => s.name === "OrderService.Add")?.fingerprint;
|
|
161
|
+
strict_1.default.equal(fpBefore, fpAfter);
|
|
162
|
+
});
|
|
163
|
+
(0, node_test_1.it)("a genuinely CHANGED function's fingerprint correctly changes — an edit is not invisible", async () => {
|
|
164
|
+
const before = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
165
|
+
const after = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", CHANGED_CSHARP_SOURCE);
|
|
166
|
+
const fpBefore = before.find(s => s.name === "OrderService.GetOrderAsync")?.fingerprint;
|
|
167
|
+
const fpAfter = after.find(s => s.name === "OrderService.GetOrderAsync")?.fingerprint;
|
|
168
|
+
strict_1.default.ok(fpBefore);
|
|
169
|
+
strict_1.default.ok(fpAfter);
|
|
170
|
+
strict_1.default.notEqual(fpBefore, fpAfter);
|
|
171
|
+
// Every OTHER real symbol in the file is untouched by the edit and must
|
|
172
|
+
// keep its own stable fingerprint — proving the change is properly
|
|
173
|
+
// scoped to the actual edited function, not a global reshuffle.
|
|
174
|
+
for (const name of ["OrderService.Add", "OrderService.LogAccess", "OrderService.OrderService", "Point.Sum"]) {
|
|
175
|
+
const b = before.find(s => s.name === name)?.fingerprint;
|
|
176
|
+
const a = after.find(s => s.name === name)?.fingerprint;
|
|
177
|
+
strict_1.default.equal(b, a, `unrelated symbol ${name} should not have changed`);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
(0, node_test_1.describe)("computeFingerprint — sanity on real C# body text", () => {
|
|
182
|
+
(0, node_test_1.it)("normalizes C# string interpolation/comments deterministically", () => {
|
|
183
|
+
const a = (0, ast_parser_1.computeFingerprint)(`Console.WriteLine($"Accessed order {id}"); // log it`);
|
|
184
|
+
const b = (0, ast_parser_1.computeFingerprint)(`Console.WriteLine($"Accessed order {id}");`);
|
|
185
|
+
strict_1.default.equal(a, b);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Real tests for vendoredCodeDetector.ts — build brief "Stop capturing
|
|
4
|
+
* vendored/third-party code as if it were the client's own" (2026-09-22).
|
|
5
|
+
* Mirrors kodingo-api's own test coverage for the same, identically-
|
|
6
|
+
* designed module.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const node_test_1 = require("node:test");
|
|
13
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
14
|
+
const vendoredCodeDetector_1 = require("../utils/vendoredCodeDetector");
|
|
15
|
+
(0, node_test_1.describe)("isVendoredPath", () => {
|
|
16
|
+
(0, node_test_1.describe)("POSITIVE — real files confirmed polluting TestHaven/FOTX", () => {
|
|
17
|
+
for (const p of [
|
|
18
|
+
"wwwroot/lib/jquery/jquery.js",
|
|
19
|
+
"wwwroot/lib/jquery/jquery.min.js",
|
|
20
|
+
"Scripts/WebUIValidation.js",
|
|
21
|
+
"Scripts/WebForms.js",
|
|
22
|
+
"Scripts/MicrosoftAjaxWebForms.js",
|
|
23
|
+
]) {
|
|
24
|
+
(0, node_test_1.it)(`flags ${p}`, () => {
|
|
25
|
+
strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredPath)(p).vendored, true);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
(0, node_test_1.describe)("POSITIVE — general path/directory/filename conventions", () => {
|
|
30
|
+
for (const p of [
|
|
31
|
+
"node_modules/react/index.js",
|
|
32
|
+
"vendor/autoload.php",
|
|
33
|
+
"bower_components/angular/angular.js",
|
|
34
|
+
"jspm_packages/npm/lodash@4.js",
|
|
35
|
+
"Pods/Alamofire/Source/Request.swift",
|
|
36
|
+
".venv/lib/python3.11/site-packages/flask/app.py",
|
|
37
|
+
"packages/Newtonsoft.Json.13.0.1/lib/net45/Newtonsoft.Json.cs",
|
|
38
|
+
"src/assets/app.min.js",
|
|
39
|
+
"src/assets/theme.min.css",
|
|
40
|
+
"jquery-3.6.0.js",
|
|
41
|
+
"bootstrap.bundle.min.js",
|
|
42
|
+
"modernizr-2.8.3.js",
|
|
43
|
+
]) {
|
|
44
|
+
(0, node_test_1.it)(`flags ${p}`, () => {
|
|
45
|
+
strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredPath)(p).vendored, true);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
(0, node_test_1.describe)("NEGATIVE — real, legitimate application files must NOT be flagged", () => {
|
|
50
|
+
for (const p of [
|
|
51
|
+
"src/validators/UserValidation.js",
|
|
52
|
+
"src/utils/MicrosoftAjaxHelper.cs",
|
|
53
|
+
"packages/api/src/OrderService.cs",
|
|
54
|
+
"src/lib/formatCurrency.ts",
|
|
55
|
+
"Scripts/OrderService.js",
|
|
56
|
+
"Scripts/checkout-page.js",
|
|
57
|
+
"src/services/OrderService.cs",
|
|
58
|
+
"src/controllers/CheckoutController.cs",
|
|
59
|
+
]) {
|
|
60
|
+
(0, node_test_1.it)(`does not flag ${p}`, () => {
|
|
61
|
+
const result = (0, vendoredCodeDetector_1.isVendoredPath)(p);
|
|
62
|
+
strict_1.default.equal(result.vendored, false);
|
|
63
|
+
strict_1.default.equal(result.reason, null);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
(0, node_test_1.describe)("isVendoredContent", () => {
|
|
69
|
+
(0, node_test_1.it)("flags genuinely minified-shaped content", () => {
|
|
70
|
+
const content = Array.from({ length: 5 }, () => "x".repeat(4000)).join("\n");
|
|
71
|
+
strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredContent)(content).vendored, true);
|
|
72
|
+
});
|
|
73
|
+
(0, node_test_1.it)("does not flag a real, dense, hand-written file with normal line breaks", () => {
|
|
74
|
+
const lines = [];
|
|
75
|
+
for (let i = 0; i < 300; i++) {
|
|
76
|
+
lines.push(` public Order GetOrder${i}(int id) { return _repo.Find(id, "status", "active"); }`);
|
|
77
|
+
}
|
|
78
|
+
const content = `namespace Acme.Orders {\n public class OrderService {\n${lines.join("\n")}\n }\n}`;
|
|
79
|
+
strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredContent)(content).vendored, false);
|
|
80
|
+
});
|
|
81
|
+
(0, node_test_1.it)("flags an explicit auto-generated header", () => {
|
|
82
|
+
const content = "// <auto-generated>\n// This code was generated by a tool.\nnamespace Acme { public class Foo {} }";
|
|
83
|
+
strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredContent)(content).vendored, true);
|
|
84
|
+
});
|
|
85
|
+
(0, node_test_1.it)("does not flag a small real file with one long line", () => {
|
|
86
|
+
const content = "const CONFIG = " + JSON.stringify({ a: 1, b: 2 });
|
|
87
|
+
strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredContent)(content).vendored, false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
(0, node_test_1.describe)("isVendoredCode", () => {
|
|
91
|
+
(0, node_test_1.it)("flags on path alone without content", () => {
|
|
92
|
+
strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredCode)("node_modules/react/index.js", null).vendored, true);
|
|
93
|
+
});
|
|
94
|
+
(0, node_test_1.it)("real, legitimate file is never flagged", () => {
|
|
95
|
+
const content = `export function calculateTotal(items) {\n return items.reduce((sum, i) => sum + i.price, 0);\n}`;
|
|
96
|
+
strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredCode)("src/services/OrderService.ts", content).vendored, false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Real, end-to-end proof that vendored files never reach symbol
|
|
4
|
+
* extraction in either real local capture path — build brief "Stop
|
|
5
|
+
* capturing vendored/third-party code as if it were the client's own"
|
|
6
|
+
* (2026-09-22). Uses a real temp directory on disk (no mocking needed —
|
|
7
|
+
* these are pure filesystem + static-analysis functions), following the
|
|
8
|
+
* exact real symptom shape confirmed for TestHaven/FOTX.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
44
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
45
|
+
};
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
const node_test_1 = require("node:test");
|
|
48
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const os = __importStar(require("os"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const scan_repo_1 = require("../commands/scan-repo");
|
|
53
|
+
const graph_builder_1 = require("../utils/graph-builder");
|
|
54
|
+
let repoRoot;
|
|
55
|
+
function write(relPath, content) {
|
|
56
|
+
const full = path.join(repoRoot, relPath);
|
|
57
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
58
|
+
fs.writeFileSync(full, content, "utf-8");
|
|
59
|
+
}
|
|
60
|
+
function realFunctionSource(name) {
|
|
61
|
+
return `function ${name}(x) {\n const result = x + 1;\n return result;\n}\n`;
|
|
62
|
+
}
|
|
63
|
+
(0, node_test_1.before)(() => {
|
|
64
|
+
repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), "kortex-vendored-test-"));
|
|
65
|
+
// Real client business logic — must always survive.
|
|
66
|
+
write("src/OrderService.ts", realFunctionSource("getOrder"));
|
|
67
|
+
write("src/CheckoutController.ts", realFunctionSource("processCheckout"));
|
|
68
|
+
// Real jQuery, checked in under ASP.NET Core's real wwwroot/lib/ convention.
|
|
69
|
+
write("wwwroot/lib/jquery/jquery.js", `function domManip(a, b) { return a; }\nfunction isWindow(obj) { return obj === obj.window; }\n`);
|
|
70
|
+
// Real WebForms framework file, checked in under an ordinary Scripts/ folder.
|
|
71
|
+
write("Scripts/WebForms.js", `function WebForm_GetElementByTagName(tag) { return document.getElementsByTagName(tag); }\n`);
|
|
72
|
+
// Vendored via node_modules — already excluded at the directory-walk
|
|
73
|
+
// level by IGNORED_DIRS, included here to confirm it's genuinely never
|
|
74
|
+
// reached either way.
|
|
75
|
+
write("node_modules/some-lib/index.js", realFunctionSource("someLibInternal"));
|
|
76
|
+
// Genuinely minified, ordinary-looking filename — only real content
|
|
77
|
+
// reveals it.
|
|
78
|
+
write("src/thirdPartyBundle.js", Array.from({ length: 5 }, () => "x".repeat(4000)).join("\n"));
|
|
79
|
+
});
|
|
80
|
+
(0, node_test_1.after)(() => {
|
|
81
|
+
fs.rmSync(repoRoot, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
(0, node_test_1.describe)("scan-repo.ts — real walkFiles + extractSymbols never surface vendored files", () => {
|
|
84
|
+
(0, node_test_1.it)("walkFiles excludes the real jQuery file (path-level) but includes the client's own real files", () => {
|
|
85
|
+
const files = (0, scan_repo_1.walkFiles)(repoRoot).map(f => path.relative(repoRoot, f).split(path.sep).join("/"));
|
|
86
|
+
strict_1.default.ok(!files.includes("wwwroot/lib/jquery/jquery.js"), `jquery.js should have been excluded, got: ${JSON.stringify(files)}`);
|
|
87
|
+
strict_1.default.ok(!files.includes("Scripts/WebForms.js"), `WebForms.js should have been excluded, got: ${JSON.stringify(files)}`);
|
|
88
|
+
strict_1.default.ok(!files.includes("node_modules/some-lib/index.js"));
|
|
89
|
+
strict_1.default.ok(files.includes("src/OrderService.ts"));
|
|
90
|
+
strict_1.default.ok(files.includes("src/CheckoutController.ts"));
|
|
91
|
+
// The minified bundle passes the PATH check (ordinary name) — content
|
|
92
|
+
// filtering happens later, in extractSymbols.
|
|
93
|
+
strict_1.default.ok(files.includes("src/thirdPartyBundle.js"));
|
|
94
|
+
});
|
|
95
|
+
(0, node_test_1.it)("extractSymbols never returns real jQuery/WebForms symbols, and returns [] for the minified bundle via content-level detection", async () => {
|
|
96
|
+
const files = (0, scan_repo_1.walkFiles)(repoRoot);
|
|
97
|
+
const allSymbols = (await Promise.all(files.map(f => (0, scan_repo_1.extractSymbols)(f)))).flat();
|
|
98
|
+
const names = allSymbols.map(s => s.name);
|
|
99
|
+
strict_1.default.ok(!names.includes("domManip"), `domManip should never have been extracted, got: ${JSON.stringify(names)}`);
|
|
100
|
+
strict_1.default.ok(!names.includes("isWindow"));
|
|
101
|
+
strict_1.default.ok(!names.includes("WebForm_GetElementByTagName"));
|
|
102
|
+
strict_1.default.ok(names.includes("getOrder"));
|
|
103
|
+
strict_1.default.ok(names.includes("processCheckout"));
|
|
104
|
+
const bundleFile = files.find(f => f.endsWith("thirdPartyBundle.js"));
|
|
105
|
+
strict_1.default.ok(bundleFile);
|
|
106
|
+
const bundleSymbols = await (0, scan_repo_1.extractSymbols)(bundleFile);
|
|
107
|
+
strict_1.default.deepEqual(bundleSymbols, []);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
(0, node_test_1.describe)("graph-builder.ts — real buildGraph never indexes vendored symbols", () => {
|
|
111
|
+
(0, node_test_1.it)("the real, built graph contains the client's own real functions and none of the vendored ones", async () => {
|
|
112
|
+
const graph = await (0, graph_builder_1.buildGraph)(repoRoot);
|
|
113
|
+
const names = Object.values(graph.nodes).map(n => n.name);
|
|
114
|
+
strict_1.default.ok(!names.includes("domManip"), `domManip should never appear in the graph, got: ${JSON.stringify(names)}`);
|
|
115
|
+
strict_1.default.ok(!names.includes("isWindow"));
|
|
116
|
+
strict_1.default.ok(!names.includes("WebForm_GetElementByTagName"));
|
|
117
|
+
strict_1.default.ok(!names.includes("someLibInternal"));
|
|
118
|
+
strict_1.default.ok(names.includes("getOrder"));
|
|
119
|
+
strict_1.default.ok(names.includes("processCheckout"));
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -49,6 +49,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
49
49
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
50
50
|
};
|
|
51
51
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.walkFiles = walkFiles;
|
|
53
|
+
exports.extractSymbols = extractSymbols;
|
|
52
54
|
exports.scanRepoCommand = scanRepoCommand;
|
|
53
55
|
const fs_1 = __importDefault(require("fs"));
|
|
54
56
|
const path_1 = __importDefault(require("path"));
|
|
@@ -57,11 +59,12 @@ const os = __importStar(require("os"));
|
|
|
57
59
|
const ast_parser_1 = require("../utils/ast-parser");
|
|
58
60
|
const graph_builder_1 = require("../utils/graph-builder");
|
|
59
61
|
const context_assembler_1 = require("../utils/context-assembler");
|
|
62
|
+
const vendoredCodeDetector_1 = require("../utils/vendoredCodeDetector");
|
|
60
63
|
const API_BASE = "https://kodingo-api.onrender.com";
|
|
61
64
|
const SUPPORTED_EXTENSIONS = [
|
|
62
65
|
".ts", ".tsx", ".js", ".jsx",
|
|
63
66
|
".py", ".go", ".rs", ".php",
|
|
64
|
-
".java", ".kt", ".rb", ".c", ".cpp",
|
|
67
|
+
".java", ".kt", ".rb", ".c", ".cpp", ".cs",
|
|
65
68
|
];
|
|
66
69
|
const IGNORED_DIRS = [
|
|
67
70
|
"node_modules", ".git", ".kortex", "dist", "build",
|
|
@@ -84,7 +87,7 @@ function findRepoRoot(startPath) {
|
|
|
84
87
|
current = parent;
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
|
-
function walkFiles(dir) {
|
|
90
|
+
function walkFiles(dir, repoRoot = dir) {
|
|
88
91
|
const results = [];
|
|
89
92
|
try {
|
|
90
93
|
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
@@ -93,9 +96,20 @@ function walkFiles(dir) {
|
|
|
93
96
|
continue;
|
|
94
97
|
const fullPath = path_1.default.join(dir, entry.name);
|
|
95
98
|
if (entry.isDirectory()) {
|
|
96
|
-
results.push(...walkFiles(fullPath));
|
|
99
|
+
results.push(...walkFiles(fullPath, repoRoot));
|
|
97
100
|
}
|
|
98
101
|
else if (entry.isFile() && SUPPORTED_EXTENSIONS.includes(path_1.default.extname(entry.name))) {
|
|
102
|
+
// Real, general vendored-code detection (build brief "Stop
|
|
103
|
+
// capturing vendored/third-party code as if it were the client's
|
|
104
|
+
// own", 2026-09-22) — IGNORED_DIRS above already prunes
|
|
105
|
+
// single-segment conventions (node_modules, vendor, .venv, ...)
|
|
106
|
+
// at real traversal speed; this catches the rest (two-segment
|
|
107
|
+
// conventions like wwwroot/lib/, minified-filename conventions,
|
|
108
|
+
// and known bundled framework files) which need the real,
|
|
109
|
+
// relative path to detect correctly.
|
|
110
|
+
const relativePath = path_1.default.relative(repoRoot, fullPath).split(path_1.default.sep).join("/");
|
|
111
|
+
if ((0, vendoredCodeDetector_1.isVendoredPath)(relativePath).vendored)
|
|
112
|
+
continue;
|
|
99
113
|
results.push(fullPath);
|
|
100
114
|
}
|
|
101
115
|
}
|
|
@@ -106,6 +120,12 @@ function walkFiles(dir) {
|
|
|
106
120
|
async function extractSymbols(filePath) {
|
|
107
121
|
try {
|
|
108
122
|
const source = fs_1.default.readFileSync(filePath, "utf-8");
|
|
123
|
+
// Content-level check — catches minified/auto-generated files that
|
|
124
|
+
// pass the path check (e.g. a bundled framework script checked in
|
|
125
|
+
// directly, not under a vendored-directory convention). Recognized
|
|
126
|
+
// and skipped BEFORE symbol extraction, not filtered after the fact.
|
|
127
|
+
if ((0, vendoredCodeDetector_1.isVendoredContent)(source).vendored)
|
|
128
|
+
return [];
|
|
109
129
|
const nodes = await (0, ast_parser_1.parseFile)(filePath, source);
|
|
110
130
|
return nodes.map(n => ({
|
|
111
131
|
name: n.name,
|
package/dist/utils/ast-parser.js
CHANGED
|
@@ -417,7 +417,7 @@ const KOTLIN_PATTERNS = [
|
|
|
417
417
|
/^(?:fun|override\s+fun|suspend\s+fun|private\s+fun|public\s+fun)\s+(\w+)/,
|
|
418
418
|
/^(?:class|data class|object|interface)\s+(\w+)/,
|
|
419
419
|
];
|
|
420
|
-
const TREE_SITTER_EXTENSIONS = new Set([".py", ".go", ".rs", ".java", ".kt", ".rb", ".php", ".c", ".cpp"]);
|
|
420
|
+
const TREE_SITTER_EXTENSIONS = new Set([".py", ".go", ".rs", ".java", ".kt", ".rb", ".php", ".c", ".cpp", ".cs"]);
|
|
421
421
|
async function parseFile(filePath, source) {
|
|
422
422
|
const ext = path.extname(filePath).toLowerCase();
|
|
423
423
|
switch (ext) {
|
|
@@ -61,10 +61,11 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
61
61
|
const path_1 = __importDefault(require("path"));
|
|
62
62
|
const ts = __importStar(require("typescript"));
|
|
63
63
|
const ast_parser_1 = require("./ast-parser");
|
|
64
|
+
const vendoredCodeDetector_1 = require("./vendoredCodeDetector");
|
|
64
65
|
const SUPPORTED_EXTENSIONS = [
|
|
65
66
|
".ts", ".tsx", ".js", ".jsx",
|
|
66
67
|
".py", ".go", ".rs", ".php",
|
|
67
|
-
".java", ".kt", ".rb", ".c", ".cpp",
|
|
68
|
+
".java", ".kt", ".rb", ".c", ".cpp", ".cs",
|
|
68
69
|
];
|
|
69
70
|
const TS_JS_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
70
71
|
const IGNORED_DIRS = [
|
|
@@ -73,7 +74,7 @@ const IGNORED_DIRS = [
|
|
|
73
74
|
".venv", "venv", "env", "site-packages", "virtualenv",
|
|
74
75
|
".tox", "target", ".gradle", ".idea", ".vscode", "bin", "obj",
|
|
75
76
|
];
|
|
76
|
-
function walkFiles(dir) {
|
|
77
|
+
function walkFiles(dir, repoRoot = dir) {
|
|
77
78
|
const results = [];
|
|
78
79
|
try {
|
|
79
80
|
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
@@ -82,9 +83,17 @@ function walkFiles(dir) {
|
|
|
82
83
|
continue;
|
|
83
84
|
const fullPath = path_1.default.join(dir, entry.name);
|
|
84
85
|
if (entry.isDirectory()) {
|
|
85
|
-
results.push(...walkFiles(fullPath));
|
|
86
|
+
results.push(...walkFiles(fullPath, repoRoot));
|
|
86
87
|
}
|
|
87
88
|
else if (entry.isFile() && SUPPORTED_EXTENSIONS.includes(path_1.default.extname(entry.name))) {
|
|
89
|
+
// Real, general vendored-code detection (build brief "Stop
|
|
90
|
+
// capturing vendored/third-party code as if it were the client's
|
|
91
|
+
// own", 2026-09-22) — a polluted graph corrupts real centrality/
|
|
92
|
+
// reachability scoring (used by attack-surface reasoning), the
|
|
93
|
+
// same real underlying problem as polluted memory capture.
|
|
94
|
+
const relativePath = path_1.default.relative(repoRoot, fullPath).split(path_1.default.sep).join("/");
|
|
95
|
+
if ((0, vendoredCodeDetector_1.isVendoredPath)(relativePath).vendored)
|
|
96
|
+
continue;
|
|
88
97
|
results.push(fullPath);
|
|
89
98
|
}
|
|
90
99
|
}
|
|
@@ -109,6 +118,8 @@ async function collectAllSymbols(repoRoot) {
|
|
|
109
118
|
for (const file of files) {
|
|
110
119
|
try {
|
|
111
120
|
const source = fs_1.default.readFileSync(file, "utf-8");
|
|
121
|
+
if ((0, vendoredCodeDetector_1.isVendoredContent)(source).vendored)
|
|
122
|
+
continue;
|
|
112
123
|
const nodes = await (0, ast_parser_1.parseFile)(file, source);
|
|
113
124
|
for (const n of nodes) {
|
|
114
125
|
all.push({ ...n, language: languageForFile(file) });
|
|
@@ -104,6 +104,31 @@ const LANGUAGE_CONFIGS = {
|
|
|
104
104
|
nameFieldCandidates: [], // handled via declarator fallback
|
|
105
105
|
importTypes: ["preproc_include"],
|
|
106
106
|
},
|
|
107
|
+
// C# — build brief "Add real C#/.NET support to the CLI's parser"
|
|
108
|
+
// (2026-09-20). Real node types confirmed directly against the bundled
|
|
109
|
+
// tree-sitter-c_sharp.wasm grammar (tree-sitter-wasms package, already a
|
|
110
|
+
// real dependency here — no new native package needed, see this brief's
|
|
111
|
+
// own final report for why that matters). method_declaration,
|
|
112
|
+
// constructor_declaration, and local_function_statement all carry a real
|
|
113
|
+
// `name` field; invocation_expression carries a real `function` field
|
|
114
|
+
// whose text (e.g. "_repo.FindAsync", "Console.WriteLine") is trimmed to
|
|
115
|
+
// its trailing identifier by the same extractTrailingIdentifier() already
|
|
116
|
+
// proven for PHP's "$this->service->method" shape — no C#-specific call
|
|
117
|
+
// handling needed. Deliberately excludes operator_declaration,
|
|
118
|
+
// destructor_declaration, and property accessors — real, distinct node
|
|
119
|
+
// shapes with no `name` field (or none at all) that no other language
|
|
120
|
+
// here gets special-cased treatment for either; kept proportionate to the
|
|
121
|
+
// existing five languages' own level of coverage rather than gold-plating
|
|
122
|
+
// C# beyond them.
|
|
123
|
+
csharp: {
|
|
124
|
+
wasmFile: "tree-sitter-c_sharp.wasm",
|
|
125
|
+
functionTypes: ["method_declaration", "constructor_declaration", "local_function_statement"],
|
|
126
|
+
classTypes: ["class_declaration", "interface_declaration", "struct_declaration", "record_declaration"],
|
|
127
|
+
callTypes: ["invocation_expression"],
|
|
128
|
+
calleeFieldCandidates: ["function"],
|
|
129
|
+
nameFieldCandidates: ["name"],
|
|
130
|
+
importTypes: ["using_directive"],
|
|
131
|
+
},
|
|
107
132
|
};
|
|
108
133
|
const EXT_TO_LANGUAGE = {
|
|
109
134
|
".py": "python",
|
|
@@ -115,6 +140,7 @@ const EXT_TO_LANGUAGE = {
|
|
|
115
140
|
".php": "php",
|
|
116
141
|
".c": "c",
|
|
117
142
|
".cpp": "cpp",
|
|
143
|
+
".cs": "csharp",
|
|
118
144
|
};
|
|
119
145
|
let initialised = false;
|
|
120
146
|
const languageCache = new Map();
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vendoredCodeDetector.ts
|
|
4
|
+
*
|
|
5
|
+
* Real, general, structural detection of vendored/third-party/bundled code
|
|
6
|
+
* — NOT a hardcoded library-name list as the primary mechanism. Build
|
|
7
|
+
* brief: "Stop capturing vendored/third-party code as if it were the
|
|
8
|
+
* client's own" (2026-09-22). Mirrored, identically-designed module in
|
|
9
|
+
* kodingo-api (utils/vendoredCodeDetector.ts) and kodingo-vscode
|
|
10
|
+
* (src/utils/vendoredCodeDetector.ts) — each repo has its own real,
|
|
11
|
+
* separate walk/extract pipeline with no shared low-level utility package
|
|
12
|
+
* between them (kodingo-core is explicitly scoped to decision/memory
|
|
13
|
+
* lifecycle domain logic, not general file classification), so this is
|
|
14
|
+
* duplicated by design, matching this codebase's own established
|
|
15
|
+
* precedent (e.g. IGNORED_DIRS is already independently duplicated across
|
|
16
|
+
* scan-repo.ts and security-scan.ts within this same repo). See this
|
|
17
|
+
* brief's own final report for the full reasoning.
|
|
18
|
+
*
|
|
19
|
+
* Real, confirmed evidence this fixes: an admin deep scan of a real
|
|
20
|
+
* .NET/WebForms project (TestHaven/FOTX) captured real jQuery internals
|
|
21
|
+
* (domManip, isWindow, movePrevious) and real ASP.NET WebForms framework
|
|
22
|
+
* internals (WebPartManager_InitiateWebPartDragDrop,
|
|
23
|
+
* WebForm_GetElementByTagName, Zone_UpdatePosition) as if they were the
|
|
24
|
+
* client's own business logic — diluting a real answer to "what is this
|
|
25
|
+
* project" into near-nothing.
|
|
26
|
+
*
|
|
27
|
+
* Three real, independent layers, checked cheapest/most-certain first:
|
|
28
|
+
* 1. Path/directory conventions — reliable across languages/ecosystems,
|
|
29
|
+
* needs only the file path.
|
|
30
|
+
* 2. File-naming conventions (.min.js/.min.css) — same, path-only.
|
|
31
|
+
* 3. Content heuristics (minified shape, an explicit auto-generated
|
|
32
|
+
* header) — needs real file content.
|
|
33
|
+
* A fourth, explicitly-acknowledged-INCOMPLETE layer: a real, growing list
|
|
34
|
+
* of specific, well-known bundled framework files. This list will NEVER be
|
|
35
|
+
* complete — it exists because layers 1-2, by design, cannot catch every
|
|
36
|
+
* real case: these specific files are frequently checked in un-minified,
|
|
37
|
+
* under an ordinarily-legitimate-looking folder name (e.g. "Scripts/",
|
|
38
|
+
* which is NOT treated as a vendored-directory convention here precisely
|
|
39
|
+
* because plenty of real WebForms projects also put their OWN
|
|
40
|
+
* app-specific JS there — see isVendoredPath's own reasoning below).
|
|
41
|
+
*/
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.isVendoredPath = isVendoredPath;
|
|
44
|
+
exports.isVendoredContent = isVendoredContent;
|
|
45
|
+
exports.isVendoredCode = isVendoredCode;
|
|
46
|
+
const NOT_VENDORED = { vendored: false, reason: null };
|
|
47
|
+
// ── Layer 1: path/directory conventions ─────────────────────────────────────
|
|
48
|
+
const VENDORED_DIR_SEGMENTS = [
|
|
49
|
+
"node_modules", "vendor", "bower_components", "jspm_packages", "pods",
|
|
50
|
+
".venv", "venv", "site-packages",
|
|
51
|
+
];
|
|
52
|
+
// "wwwroot/lib/" specifically is ASP.NET Core's real, well-known convention
|
|
53
|
+
// for client-side libraries pulled in via LibMan/npm — directly relevant
|
|
54
|
+
// to this exact client's real stack. "lib/" alone is NOT treated as
|
|
55
|
+
// vendored — an extremely common real application folder name on its own.
|
|
56
|
+
const VENDORED_PATH_PAIRS = [
|
|
57
|
+
["wwwroot", "lib"],
|
|
58
|
+
];
|
|
59
|
+
// Classic NuGet packages/ convention: packages/<PackageName>.<Version>/...
|
|
60
|
+
// Deliberately NOT a bare "packages/" match — that would false-positive on
|
|
61
|
+
// the equally common JS/TS monorepo convention (Lerna/Yarn/npm workspaces)
|
|
62
|
+
// where packages/<name>/ holds the CLIENT'S OWN first-party code.
|
|
63
|
+
const NUGET_VERSIONED_DIR = /^[A-Za-z0-9_.-]+\.\d+\.\d+(\.\d+)?(-[A-Za-z0-9.]+)?$/;
|
|
64
|
+
function pathSegments(filePath) {
|
|
65
|
+
return filePath.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
function isVendoredPath(filePath) {
|
|
68
|
+
const segments = pathSegments(filePath);
|
|
69
|
+
const lowerSegments = segments.map(s => s.toLowerCase());
|
|
70
|
+
for (const dir of VENDORED_DIR_SEGMENTS) {
|
|
71
|
+
if (lowerSegments.includes(dir)) {
|
|
72
|
+
return { vendored: true, reason: `Path contains the "${dir}/" vendored-dependency directory convention.` };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const [first, second] of VENDORED_PATH_PAIRS) {
|
|
76
|
+
for (let i = 0; i < lowerSegments.length - 1; i++) {
|
|
77
|
+
if (lowerSegments[i] === first && lowerSegments[i + 1] === second) {
|
|
78
|
+
return { vendored: true, reason: `Path contains the "${first}/${second}/" vendored-dependency directory convention.` };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const packagesIdx = lowerSegments.indexOf("packages");
|
|
83
|
+
if (packagesIdx !== -1 && packagesIdx + 1 < segments.length && NUGET_VERSIONED_DIR.test(segments[packagesIdx + 1])) {
|
|
84
|
+
return { vendored: true, reason: `Path matches the classic NuGet "packages/<Name>.<Version>/" vendored-package convention.` };
|
|
85
|
+
}
|
|
86
|
+
const basename = segments[segments.length - 1] ?? "";
|
|
87
|
+
if (/\.min\.(js|css)$/i.test(basename) || /-min\.(js|css)$/i.test(basename)) {
|
|
88
|
+
return { vendored: true, reason: `Filename "${basename}" follows the minified-distribution naming convention (.min.js/.min.css).` };
|
|
89
|
+
}
|
|
90
|
+
const knownFileReason = matchKnownVendoredFile(basename);
|
|
91
|
+
if (knownFileReason)
|
|
92
|
+
return { vendored: true, reason: knownFileReason };
|
|
93
|
+
return NOT_VENDORED;
|
|
94
|
+
}
|
|
95
|
+
// ── Layer 2: content heuristics ─────────────────────────────────────────────
|
|
96
|
+
const MIN_CONTENT_LENGTH_FOR_MINIFICATION_CHECK = 5000;
|
|
97
|
+
const MINIFIED_AVG_LINE_LENGTH_THRESHOLD = 500;
|
|
98
|
+
const AUTO_GENERATED_HEADER_PATTERN = /<auto-generated|@generated\b|do not (edit|modify) this file|this file (is|was) auto-generated|machine[- ]generated/i;
|
|
99
|
+
function isVendoredContent(content) {
|
|
100
|
+
const header = content.slice(0, 500);
|
|
101
|
+
if (AUTO_GENERATED_HEADER_PATTERN.test(header)) {
|
|
102
|
+
return { vendored: true, reason: "File header contains an explicit auto-generated marker." };
|
|
103
|
+
}
|
|
104
|
+
if (content.length >= MIN_CONTENT_LENGTH_FOR_MINIFICATION_CHECK) {
|
|
105
|
+
const lineCount = content.split("\n").length;
|
|
106
|
+
const avgLineLength = content.length / Math.max(1, lineCount);
|
|
107
|
+
if (avgLineLength > MINIFIED_AVG_LINE_LENGTH_THRESHOLD) {
|
|
108
|
+
return {
|
|
109
|
+
vendored: true,
|
|
110
|
+
reason: `Unusually long average line length (${Math.round(avgLineLength)} chars/line across ${lineCount} lines) consistent with minified/bundled code, not hand-written source.`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return NOT_VENDORED;
|
|
115
|
+
}
|
|
116
|
+
// ── Layer 3 (secondary, explicitly incomplete): known bundled framework files ──
|
|
117
|
+
const KNOWN_VENDORED_FILENAME_PATTERNS = [
|
|
118
|
+
/^jquery(-\d+(\.\d+){1,2})?(\.min)?\.js$/i,
|
|
119
|
+
/^jquery-ui(-\d+(\.\d+){1,2})?(\.min)?\.js$/i,
|
|
120
|
+
/^jquery\.validate(\.unobtrusive)?(\.min)?\.js$/i,
|
|
121
|
+
/^jquery\.unobtrusive-ajax(\.min)?\.js$/i,
|
|
122
|
+
/^WebUIValidation\.js$/i,
|
|
123
|
+
/^WebForms\.js$/i,
|
|
124
|
+
/^MicrosoftAjax(WebForms|Timer)?(\.debug)?\.js$/i,
|
|
125
|
+
/^SmartNav\.js$/i,
|
|
126
|
+
/^bootstrap(\.bundle)?(\.min)?\.(js|css)$/i,
|
|
127
|
+
/^modernizr(-\d+(\.\d+){1,2})?(\.min)?\.js$/i,
|
|
128
|
+
];
|
|
129
|
+
function matchKnownVendoredFile(basename) {
|
|
130
|
+
for (const pattern of KNOWN_VENDORED_FILENAME_PATTERNS) {
|
|
131
|
+
if (pattern.test(basename)) {
|
|
132
|
+
return `Filename "${basename}" matches a real, known bundled/vendored framework file (secondary, explicitly-incomplete list — see this module's own file header).`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
// ── Combined convenience ─────────────────────────────────────────────────────
|
|
138
|
+
function isVendoredCode(filePath, content) {
|
|
139
|
+
const pathResult = isVendoredPath(filePath);
|
|
140
|
+
if (pathResult.vendored)
|
|
141
|
+
return pathResult;
|
|
142
|
+
if (content !== null)
|
|
143
|
+
return isVendoredContent(content);
|
|
144
|
+
return NOT_VENDORED;
|
|
145
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodingo-cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "Kodingo CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"build": "tsc -p tsconfig.json",
|
|
17
17
|
"dev": "tsc -p tsconfig.json --watch",
|
|
18
18
|
"start": "node dist/cli.js",
|
|
19
|
-
"test": "
|
|
19
|
+
"test": "npm run build && node --test dist/__tests__/*.js",
|
|
20
20
|
"prepare": "npm run build"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|