@iris-code/core 0.1.2 → 0.3.0

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.
Files changed (49) hide show
  1. package/dist/config.d.ts +14 -1
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +48 -5
  4. package/dist/config.js.map +1 -1
  5. package/dist/fileNaming.d.ts +1 -0
  6. package/dist/fileNaming.d.ts.map +1 -1
  7. package/dist/fileNaming.js +29 -0
  8. package/dist/fileNaming.js.map +1 -1
  9. package/dist/guards.d.ts.map +1 -1
  10. package/dist/guards.js +44 -2
  11. package/dist/guards.js.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +45 -9
  14. package/dist/index.js.map +1 -1
  15. package/dist/java/analyser.d.ts +4 -0
  16. package/dist/java/analyser.d.ts.map +1 -0
  17. package/dist/java/analyser.js +394 -0
  18. package/dist/java/analyser.js.map +1 -0
  19. package/dist/java/lexer.d.ts +46 -0
  20. package/dist/java/lexer.d.ts.map +1 -0
  21. package/dist/java/lexer.js +198 -0
  22. package/dist/java/lexer.js.map +1 -0
  23. package/dist/languages.d.ts +2 -10
  24. package/dist/languages.d.ts.map +1 -1
  25. package/dist/languages.js +18 -35
  26. package/dist/languages.js.map +1 -1
  27. package/dist/registry.d.ts +39 -0
  28. package/dist/registry.d.ts.map +1 -0
  29. package/dist/registry.js +74 -0
  30. package/dist/registry.js.map +1 -0
  31. package/dist/rust/analyser.d.ts +26 -0
  32. package/dist/rust/analyser.d.ts.map +1 -0
  33. package/dist/rust/analyser.js +506 -0
  34. package/dist/rust/analyser.js.map +1 -0
  35. package/dist/rust/lexer.d.ts +39 -0
  36. package/dist/rust/lexer.d.ts.map +1 -0
  37. package/dist/rust/lexer.js +192 -0
  38. package/dist/rust/lexer.js.map +1 -0
  39. package/dist/secrets.d.ts.map +1 -1
  40. package/dist/secrets.js +357 -1
  41. package/dist/secrets.js.map +1 -1
  42. package/dist/sfc.d.ts.map +1 -1
  43. package/dist/sfc.js +4 -18
  44. package/dist/sfc.js.map +1 -1
  45. package/dist/types.d.ts +4 -4
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/types.js +6 -0
  48. package/dist/types.js.map +1 -1
  49. package/package.json +2 -2
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ /**
3
+ * The Rust lexical layer, in its own module because TWO callers need it: the
4
+ * Rust analyser (every structural rule) and `detectParseErrorReason` in
5
+ * guards.ts (the delimiter balance check that decides whether a file is
6
+ * analysed at all).
7
+ *
8
+ * That second caller is why this exists before any rule. C# shipped a defect
9
+ * where a verbatim string read as unterminated, every brace after it landed at
10
+ * the wrong depth, and the file came back a parse error with every finding
11
+ * zeroed - and a zeroed file is indistinguishable from a clean one.
12
+ *
13
+ * Both modes are LENGTH- and LINE-PRESERVING, so a finding's offset still maps
14
+ * to its native line with no remapping layer that could drift.
15
+ *
16
+ * THE LIFETIME IS THE RUST-SPECIFIC TRAP, and it is the exact shape that breaks
17
+ * a naive scanner. `&'a str` is a reference with a lifetime, not a char literal:
18
+ * a scanner that treats `'` as opening a character would run to the NEXT
19
+ * apostrophe somewhere further down the file, blanking real code in between.
20
+ * Lifetimes appear in most non-trivial Rust, so this is the common case rather
21
+ * than an edge one.
22
+ *
23
+ * Also handled, each with its own way of breaking a naive scan:
24
+ * - raw strings at any hash depth: `r"..."`, `r#"..."#`, `r##"..."##`, where a
25
+ * `"` inside does NOT close the literal
26
+ * - raw identifiers: `r#type` is an identifier, not a raw string, so `r#` only
27
+ * opens a literal when a quote follows the hashes
28
+ * - byte strings `b"..."` and byte chars `b'x'`, plus `br#"..."#`
29
+ * - NESTED block comments, which Rust allows and C does not. An inner block
30
+ * comment's closer does NOT end the outer one, so a depth counter is the only
31
+ * way to see where the comment really ends. (Writing that construct literally
32
+ * in this very comment ended it early on the first build - the same class of
33
+ * mistake the module exists to prevent.)
34
+ */
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.transformRust = transformRust;
37
+ exports.stripRustComments = stripRustComments;
38
+ exports.rustCodeOnly = rustCodeOnly;
39
+ /** True when the `'` at `index` opens a char literal rather than a lifetime. */
40
+ function isCharLiteral(source, index) {
41
+ // `'\n'`, `'\''`, `'\u{1F600}'`: an escape always means a char literal.
42
+ if (source[index + 1] === '\\')
43
+ return true;
44
+ // `'a'` is a char; `'a` followed by anything else is a lifetime. A char
45
+ // literal's body is a single scalar, so the quote sits two positions along.
46
+ if (source[index + 2] === "'")
47
+ return true;
48
+ return false;
49
+ }
50
+ function transformRust(source, blankStrings) {
51
+ let result = '';
52
+ let i = 0;
53
+ const blank = (ch) => (ch === '\n' || ch === '\r' ? ch : ' ');
54
+ const emit = (ch) => { result += blankStrings ? blank(ch) : ch; };
55
+ while (i < source.length) {
56
+ const ch = source[i];
57
+ if (ch === '/' && source[i + 1] === '/') {
58
+ while (i < source.length && source[i] !== '\n') {
59
+ result += blank(source[i]);
60
+ i++;
61
+ }
62
+ continue;
63
+ }
64
+ // Nested block comments. A single `*/` does not necessarily end the comment.
65
+ if (ch === '/' && source[i + 1] === '*') {
66
+ let depth = 0;
67
+ while (i < source.length) {
68
+ if (source[i] === '/' && source[i + 1] === '*') {
69
+ depth++;
70
+ result += ' ';
71
+ i += 2;
72
+ continue;
73
+ }
74
+ if (source[i] === '*' && source[i + 1] === '/') {
75
+ depth--;
76
+ result += ' ';
77
+ i += 2;
78
+ if (depth === 0)
79
+ break;
80
+ continue;
81
+ }
82
+ result += blank(source[i]);
83
+ i++;
84
+ }
85
+ continue;
86
+ }
87
+ // Raw and byte string prefixes: r"..", r#".."#, b"..", br#".."#.
88
+ if (ch === 'r' || ch === 'b') {
89
+ let j = i;
90
+ if (source[j] === 'b')
91
+ j++;
92
+ if (source[j] === 'r') {
93
+ j++;
94
+ let hashes = 0;
95
+ while (source[j] === '#') {
96
+ hashes++;
97
+ j++;
98
+ }
99
+ if (source[j] === '"') {
100
+ // A raw string. Its terminator is a quote followed by exactly the
101
+ // opening number of hashes, so an inner `"` cannot close it.
102
+ const closer = '"' + '#'.repeat(hashes);
103
+ for (let k = i; k <= j; k++)
104
+ emit(source[k]);
105
+ i = j + 1;
106
+ while (i < source.length) {
107
+ if (source.startsWith(closer, i)) {
108
+ for (let k = 0; k < closer.length; k++)
109
+ emit(source[i + k]);
110
+ i += closer.length;
111
+ break;
112
+ }
113
+ emit(source[i]);
114
+ i++;
115
+ }
116
+ continue;
117
+ }
118
+ // `r#type`: a raw identifier, not a literal. Fall through as code.
119
+ }
120
+ else if (source[i] === 'b' && (source[j] === '"' || source[j] === "'")) {
121
+ // Byte string or byte char: emit the prefix, then handle as usual.
122
+ result += 'b';
123
+ i++;
124
+ continue;
125
+ }
126
+ }
127
+ if (ch === '"') {
128
+ emit(ch);
129
+ i++;
130
+ while (i < source.length) {
131
+ const current = source[i];
132
+ if (current === '\\') {
133
+ emit(current);
134
+ i++;
135
+ if (i < source.length) {
136
+ emit(source[i]);
137
+ i++;
138
+ }
139
+ continue;
140
+ }
141
+ emit(current);
142
+ i++;
143
+ // Rust strings may span lines, so a newline does not terminate one.
144
+ if (current === '"')
145
+ break;
146
+ }
147
+ continue;
148
+ }
149
+ if (ch === "'") {
150
+ if (!isCharLiteral(source, i)) {
151
+ // A lifetime. Emit the apostrophe as code and carry on; treating it as
152
+ // a literal would blank everything up to the next apostrophe.
153
+ result += ch;
154
+ i++;
155
+ continue;
156
+ }
157
+ emit(ch);
158
+ i++;
159
+ while (i < source.length) {
160
+ const current = source[i];
161
+ if (current === '\n')
162
+ break;
163
+ if (current === '\\') {
164
+ emit(current);
165
+ i++;
166
+ if (i < source.length) {
167
+ emit(source[i]);
168
+ i++;
169
+ }
170
+ continue;
171
+ }
172
+ emit(current);
173
+ i++;
174
+ if (current === "'")
175
+ break;
176
+ }
177
+ continue;
178
+ }
179
+ result += ch;
180
+ i++;
181
+ }
182
+ return result;
183
+ }
184
+ /** Comments blanked, string contents intact. */
185
+ function stripRustComments(source) {
186
+ return transformRust(source, false);
187
+ }
188
+ /** Comments AND string contents blanked: the input for anything structural. */
189
+ function rustCodeOnly(source) {
190
+ return transformRust(source, true);
191
+ }
192
+ //# sourceMappingURL=lexer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lexer.js","sourceRoot":"","sources":["../../src/rust/lexer.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;;AAYH,sCAwHC;AAGD,8CAEC;AAGD,oCAEC;AA5ID,gFAAgF;AAChF,SAAS,aAAa,CAAC,MAAc,EAAE,KAAa;IAClD,wEAAwE;IACxE,IAAI,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC3C,wEAAwE;IACxE,4EAA4E;IAC5E,IAAI,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAA;IAC1C,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAgB,aAAa,CAAC,MAAc,EAAE,YAAqB;IACjE,IAAI,MAAM,GAAG,EAAE,CAAA;IACf,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,MAAM,KAAK,GAAG,CAAC,EAAU,EAAU,EAAE,CAAC,CAAC,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IAC7E,MAAM,IAAI,GAAG,CAAC,EAAU,EAAQ,EAAE,GAAG,MAAM,IAAI,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA,CAAC,CAAC,CAAA;IAE9E,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;QACzB,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;QAEpB,IAAI,EAAE,KAAK,GAAG,IAAI,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YACxC,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;gBAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;gBAAC,CAAC,EAAE,CAAA;YAAC,CAAC;YACnF,SAAQ;QACV,CAAC;QAED,6EAA6E;QAC7E,IAAI,EAAE,KAAK,GAAG,IAAI,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YACxC,IAAI,KAAK,GAAG,CAAC,CAAA;YACb,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;gBACzB,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBAC/C,KAAK,EAAE,CAAA;oBACP,MAAM,IAAI,IAAI,CAAA;oBACd,CAAC,IAAI,CAAC,CAAA;oBACN,SAAQ;gBACV,CAAC;gBACD,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBAC/C,KAAK,EAAE,CAAA;oBACP,MAAM,IAAI,IAAI,CAAA;oBACd,CAAC,IAAI,CAAC,CAAA;oBACN,IAAI,KAAK,KAAK,CAAC;wBAAE,MAAK;oBACtB,SAAQ;gBACV,CAAC;gBACD,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;gBAC1B,CAAC,EAAE,CAAA;YACL,CAAC;YACD,SAAQ;QACV,CAAC;QAED,iEAAiE;QACjE,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YAC7B,IAAI,CAAC,GAAG,CAAC,CAAA;YACT,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;gBAAE,CAAC,EAAE,CAAA;YAC1B,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBACtB,CAAC,EAAE,CAAA;gBACH,IAAI,MAAM,GAAG,CAAC,CAAA;gBACd,OAAO,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBAAC,MAAM,EAAE,CAAC;oBAAC,CAAC,EAAE,CAAA;gBAAC,CAAC;gBAC3C,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBACtB,kEAAkE;oBAClE,6DAA6D;oBAC7D,MAAM,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;oBACvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE;wBAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;oBAC5C,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;oBACT,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;wBACzB,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC;4BACjC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;gCAAE,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;4BAC3D,CAAC,IAAI,MAAM,CAAC,MAAM,CAAA;4BAClB,MAAK;wBACP,CAAC;wBACD,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;wBACf,CAAC,EAAE,CAAA;oBACL,CAAC;oBACD,SAAQ;gBACV,CAAC;gBACD,mEAAmE;YACrE,CAAC;iBAAM,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;gBACzE,mEAAmE;gBACnE,MAAM,IAAI,GAAG,CAAA;gBACb,CAAC,EAAE,CAAA;gBACH,SAAQ;YACV,CAAC;QACH,CAAC;QAED,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,IAAI,CAAC,EAAE,CAAC,CAAA;YACR,CAAC,EAAE,CAAA;YACH,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;gBACzB,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;gBACzB,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;oBACrB,IAAI,CAAC,OAAO,CAAC,CAAA;oBACb,CAAC,EAAE,CAAA;oBACH,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;wBAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;wBAAC,CAAC,EAAE,CAAA;oBAAC,CAAC;oBAC/C,SAAQ;gBACV,CAAC;gBACD,IAAI,CAAC,OAAO,CAAC,CAAA;gBACb,CAAC,EAAE,CAAA;gBACH,oEAAoE;gBACpE,IAAI,OAAO,KAAK,GAAG;oBAAE,MAAK;YAC5B,CAAC;YACD,SAAQ;QACV,CAAC;QAED,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC;gBAC9B,uEAAuE;gBACvE,8DAA8D;gBAC9D,MAAM,IAAI,EAAE,CAAA;gBACZ,CAAC,EAAE,CAAA;gBACH,SAAQ;YACV,CAAC;YACD,IAAI,CAAC,EAAE,CAAC,CAAA;YACR,CAAC,EAAE,CAAA;YACH,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;gBACzB,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;gBACzB,IAAI,OAAO,KAAK,IAAI;oBAAE,MAAK;gBAC3B,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;oBACrB,IAAI,CAAC,OAAO,CAAC,CAAA;oBACb,CAAC,EAAE,CAAA;oBACH,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;wBAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;wBAAC,CAAC,EAAE,CAAA;oBAAC,CAAC;oBAC/C,SAAQ;gBACV,CAAC;gBACD,IAAI,CAAC,OAAO,CAAC,CAAA;gBACb,CAAC,EAAE,CAAA;gBACH,IAAI,OAAO,KAAK,GAAG;oBAAE,MAAK;YAC5B,CAAC;YACD,SAAQ;QACV,CAAC;QAED,MAAM,IAAI,EAAE,CAAA;QACZ,CAAC,EAAE,CAAA;IACL,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,gDAAgD;AAChD,SAAgB,iBAAiB,CAAC,MAAc;IAC9C,OAAO,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;AACrC,CAAC;AAED,+EAA+E;AAC/E,SAAgB,YAAY,CAAC,MAAc;IACzC,OAAO,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;AACpC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAMnD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO1D;AAoND,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,QAAQ,EAClB,QAAQ,SAAK,GACZ,eAAe,EAAE,CAsBnB"}
1
+ {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAMnD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO1D;AA8hBD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,QAAQ,EAClB,QAAQ,SAAK,GACZ,eAAe,EAAE,CA6BnB"}
package/dist/secrets.js CHANGED
@@ -53,6 +53,8 @@ function detectLayer1Ts(source) {
53
53
  continue;
54
54
  if (isPlaceholder(value, varName))
55
55
  continue;
56
+ if (valueIsNotACredential(value, varName))
57
+ continue;
56
58
  results.push({
57
59
  line: lineOf(source, m.index),
58
60
  variable: varName,
@@ -79,6 +81,8 @@ function detectLayer1Go(source) {
79
81
  continue;
80
82
  if (isPlaceholder(value, varName))
81
83
  continue;
84
+ if (valueIsNotACredential(value, varName))
85
+ continue;
82
86
  results.push({
83
87
  line: lineOf(source, m.index),
84
88
  variable: varName,
@@ -99,6 +103,8 @@ function detectLayer1Go(source) {
99
103
  continue;
100
104
  if (isPlaceholder(value, varName))
101
105
  continue;
106
+ if (valueIsNotACredential(value, varName))
107
+ continue;
102
108
  results.push({
103
109
  line: lineOf(source, m.index),
104
110
  variable: varName,
@@ -124,6 +130,8 @@ function detectLayer1Py(source) {
124
130
  continue;
125
131
  if (isPlaceholder(value, varName))
126
132
  continue;
133
+ if (valueIsNotACredential(value, varName))
134
+ continue;
127
135
  results.push({
128
136
  line: lineOf(source, m.index),
129
137
  variable: varName,
@@ -145,6 +153,8 @@ function detectLayer1Ruby(source) {
145
153
  const value = m[4] ?? m[5];
146
154
  if (!varName || !value || value.length < 8 || !hasSuspectKeyword(varName) || isPlaceholder(value, varName))
147
155
  continue;
156
+ if (valueIsNotACredential(value, varName))
157
+ continue;
148
158
  results.push({ line: lineOf(source, m.index), variable: varName, pattern: 'suspicious-name', message: `Possible hardcoded secret: ${varName} assigned directly to a string literal`, value, maskedValue: maskSecret(value) });
149
159
  }
150
160
  return results;
@@ -169,6 +179,8 @@ function detectLayer1CSharp(source) {
169
179
  const value = m[2] ?? m[3];
170
180
  if (!varName || !value || value.length < 8 || !hasSuspectKeyword(varName) || isPlaceholder(value, varName))
171
181
  continue;
182
+ if (valueIsNotACredential(value, varName))
183
+ continue;
172
184
  results.push({ line: lineOf(source, m.index), variable: varName, pattern: 'suspicious-name', message: `Possible hardcoded secret: ${varName} assigned directly to a string literal`, value, maskedValue: maskSecret(value) });
173
185
  }
174
186
  const xmlRe = /<([A-Za-z_][\w.-]*(?:Key|Token|Secret|Password|Credential|Private|Pwd)[\w.-]*)>([^<]{8,500})<\/\1>/gi;
@@ -177,6 +189,8 @@ function detectLayer1CSharp(source) {
177
189
  const value = m[2].trim();
178
190
  if (!value || CSHARP_XML_EXEMPT_PROPERTIES.has(varName.toLowerCase()) || isPlaceholder(value, varName))
179
191
  continue;
192
+ if (valueIsNotACredential(value, varName))
193
+ continue;
180
194
  // A value made only of MSBuild property/item references - $(ApiToken),
181
195
  // @(Items), %(Meta) - is injected at build time from CI or a secret store.
182
196
  // That is the FIX for this rule, so flagging it would punish it.
@@ -186,6 +200,342 @@ function detectLayer1CSharp(source) {
186
200
  }
187
201
  return results;
188
202
  }
203
+ /**
204
+ * A value that NAMES a credential field rather than holding one.
205
+ *
206
+ * `private static final String PASSWORD_FIELD = "password"` and
207
+ * `API_KEY_HEADER = "X-API-Key"` are constants declaring a key name, and they
208
+ * are everywhere in Java config and HTTP code. Both halves carry a suspect
209
+ * keyword, so the variable-name heuristic alone reports every one of them.
210
+ *
211
+ * Three conditions together, and all three are needed. The suspect keyword alone
212
+ * is far too loose: `an0therPr0dSecret` is a plausible password that contains
213
+ * the segment "secret", and an earlier version of this check silently swallowed
214
+ * it - trading a false positive for a false NEGATIVE on the rule that matters
215
+ * most here.
216
+ *
217
+ * - NO DIGITS. A field name is `password` or `X-API-Key`; a credential almost
218
+ * always carries at least one digit. This is the strongest of the three.
219
+ * - SHORT. Field names are 8 to 16 characters; credentials are longer.
220
+ * - The value is a single unspaced identifier or header token that is ITSELF a
221
+ * suspect keyword.
222
+ *
223
+ * Accepted edge: a genuine credential that is short, digit-free AND contains a
224
+ * credential word - `secretpass` - is missed. A value that weak is a placeholder
225
+ * far more often than a live key, and Layer 2 still covers anything with a
226
+ * recognisable provider format.
227
+ *
228
+ * Scoped to the detectors added with it rather than folded into `isPlaceholder`,
229
+ * so the existing per-language benchmark baselines cannot shift underneath a
230
+ * change that was about Java. Worth promoting once it has a baseline of its own.
231
+ */
232
+ function namesAFieldRatherThanHoldingOne(value) {
233
+ if (value.length > 16)
234
+ return false;
235
+ if (!/^[A-Za-z][A-Za-z._-]*$/.test(value))
236
+ return false;
237
+ return hasSuspectKeyword(value);
238
+ }
239
+ /** Longest unbroken run of letters and digits. */
240
+ function longestAlphanumericRun(value) {
241
+ let best = '';
242
+ let run = '';
243
+ for (const ch of value) {
244
+ if (/[A-Za-z0-9]/.test(ch)) {
245
+ run += ch;
246
+ if (run.length > best.length)
247
+ best = run;
248
+ }
249
+ else
250
+ run = '';
251
+ }
252
+ return best;
253
+ }
254
+ /**
255
+ * True when the value contains a run that looks like a generated credential.
256
+ *
257
+ * Length alone is not the signal, which the first version of this got wrong: it
258
+ * treated any 12-character run as credential-shaped, and `authentication` is 14
259
+ * letters of ordinary English. That single mistake kept `ACKNOWLEDGED_KEY =
260
+ * "acknowledged"` and a log message containing the word "authentication" in the
261
+ * results, because the gate refused to let any other rule exempt them.
262
+ *
263
+ * A generated credential MIXES letters and digits, or is long enough that no
264
+ * English word explains it. Both forms are checked against the longest run rather
265
+ * than the whole value, so separators cannot dilute the signal.
266
+ */
267
+ function looksCredentialShaped(value) {
268
+ const run = longestAlphanumericRun(value);
269
+ if (run.length < 12)
270
+ return false;
271
+ return (/[0-9]/.test(run) && /[A-Za-z]/.test(run)) || run.length >= 20;
272
+ }
273
+ /** Letters and digits only, lowercased, so two spellings of one name compare equal. */
274
+ function alphanumericLower(value) {
275
+ return value.toLowerCase().replace(/[^a-z0-9]/g, '');
276
+ }
277
+ /**
278
+ * True when the value is the variable's OWN NAME rather than a credential.
279
+ *
280
+ * This is the single most common false positive on real Java, and the shape is
281
+ * unmistakable once seen: a constant that declares a settings key or a JSON field
282
+ * has a value that mirrors its own identifier.
283
+ *
284
+ * static final String PATTERNS_KEY = "patterns";
285
+ * static final String CLUSTER_UUID_KEY = "cluster_uuid";
286
+ * static final String HIDDEN_FIELD_KEY = "hidden_field";
287
+ * static final String SERVICE_ACCOUNT_TOKEN_DOC_TYPE = "service_account_token";
288
+ *
289
+ * Compared after stripping separators and case, and after removing the credential
290
+ * nouns from the NAME - which is what makes `PATTERNS_KEY` reduce to `patterns`.
291
+ * `isPlaceholder` already covers exact equality; this covers the suffixed forms.
292
+ *
293
+ * Crucially it does NOT fire on a real credential, because a password does not
294
+ * spell out its own variable name: `PASSWORD = "secret-test-password"` reduces to
295
+ * `""` vs `secrettestpassword` and still reports.
296
+ */
297
+ function valueMirrorsItsOwnName(value, varName) {
298
+ const name = alphanumericLower(varName);
299
+ const val = alphanumericLower(value);
300
+ if (val.length < 4 || name.length < 4)
301
+ return false;
302
+ if (val === name)
303
+ return true;
304
+ // The name minus its credential nouns: PATTERNS_KEY -> patterns.
305
+ const stripped = [...CREDENTIAL_NOUNS, 'auth', 'api'].reduce((acc, noun) => acc.split(noun).join(''), name);
306
+ if (stripped.length >= 4 && stripped === val)
307
+ return true;
308
+ // The value is the name minus a trailing descriptor:
309
+ // SERVICE_ACCOUNT_TOKEN_DOC_TYPE -> service_account_token.
310
+ return val.length >= 6 && name.startsWith(val);
311
+ }
312
+ /**
313
+ * True when the value is provably not a credential, whatever its variable is
314
+ * called. Applies to EVERY language's Layer 1, not just the newest ones.
315
+ *
316
+ * Found 2026-08-17 by scanning the real Elasticsearch repository: 59 findings in
317
+ * production source, almost all of them constants whose name carries a credential
318
+ * word and whose value is a settings key, a path, an algorithm, or a log message.
319
+ * At 10 points each that buries the real findings, which is worse than missing
320
+ * them - a rule nobody trusts gets switched off.
321
+ *
322
+ * THE GATE COMES FIRST and it is what makes the rest safe: anything containing a
323
+ * long unbroken alphanumeric run is treated as a credential and never exempted, so
324
+ * a base64 blob or a 64-character hex token cannot be talked away by the shapes
325
+ * below. Deliberately NOT included is any "looks random enough" requirement -
326
+ * that would drop weak-but-real passwords like `secret-test-password`, trading a
327
+ * visible false positive for an invisible false negative on the highest-value rule
328
+ * in the product. Noise is annoying; a missed credential is the actual danger.
329
+ */
330
+ function valueIsNotACredential(value, varName) {
331
+ const v = value.trim();
332
+ // Checked BEFORE the gate, because both are unambiguous whatever they contain: a
333
+ // credential is never a URL, and never a command-line flag. `--validElastic
334
+ // LicenseKeyConfirmed=` is a 31-character run and would otherwise be held back
335
+ // by the gate below.
336
+ if (v.includes('://') || v.startsWith('--'))
337
+ return true;
338
+ // Anything credential-shaped is never exempted, whatever else it resembles. This
339
+ // is what makes every rule below safe to apply.
340
+ if (looksCredentialShaped(v))
341
+ return false;
342
+ if (valueMirrorsItsOwnName(v, varName))
343
+ return true;
344
+ // A LOG MESSAGE, not prose in general - and the distinction is the whole point.
345
+ //
346
+ // The first version of this read "four or more words is a sentence, so it is not
347
+ // a password", and the accuracy benchmark rejected it within a minute:
348
+ // `@password = "correct horse battery staple"` is four words and is a real
349
+ // passphrase, the XKCD form that security guidance actively recommends. Missing
350
+ // it would have been the exact trade this function's header refuses to make.
351
+ //
352
+ // So a log message has to prove itself: five or more words AND a mark that prose
353
+ // carries and a passphrase does not - a capitalised opening, punctuation, or a
354
+ // digit. "Internal cloud API key minted for cross-project datafeed" qualifies;
355
+ // "correct horse battery staple" does not, and neither does its capitalised form,
356
+ // because four words is under the floor either way.
357
+ const words = v.split(/\s+/).filter(Boolean);
358
+ if (words.length >= 5 && (/^[A-Z]/.test(v) || /[.,;:!?()[\]{}]/.test(v) || /\d/.test(v)))
359
+ return true;
360
+ // A path, with or without a leading slash: data/rows.ndjson,
361
+ // /api/v1/monitor/dep-graph, computeMetadata/v1/instance/service-accounts
362
+ if (/^\/?[\w.-]+(?:\/[\w.-]+)+\/?$/.test(v))
363
+ return true;
364
+ // A namespaced setting key: index.store.snapshot.partial
365
+ if ((v.match(/\./g) ?? []).length >= 2 && /^[\w.-]+$/.test(v))
366
+ return true;
367
+ return false;
368
+ }
369
+ /**
370
+ * Layer 1: Java fields, locals, and setter calls.
371
+ *
372
+ * Java has NO `const`/`let`/`var`-style opener on a field, which is why routing
373
+ * it to the TypeScript detector left it detecting nothing at all. The everyday
374
+ * shape - `private static final String apiKey = "..."` - matched no pattern in
375
+ * this file, so the only Java secrets ever reported were the ones Layer 2 caught
376
+ * by token FORMAT. A committed database password has no recognisable format, so
377
+ * it was invisible.
378
+ *
379
+ * `var` is included because Java 10+ allows it for locals, and setter calls are
380
+ * included because `.setPassword("literal")` is as common in Java config code as
381
+ * a field assignment and is the same mistake.
382
+ */
383
+ function detectLayer1Java(source) {
384
+ const results = [];
385
+ const push = (index, varName, value) => {
386
+ if (!varName || !value || value.length < 8)
387
+ return;
388
+ if (!hasSuspectKeyword(varName) || isPlaceholder(value, varName))
389
+ return;
390
+ if (namesAFieldRatherThanHoldingOne(value))
391
+ return;
392
+ if (valueIsNotACredential(value, varName))
393
+ return;
394
+ results.push({
395
+ line: lineOf(source, index),
396
+ variable: varName,
397
+ pattern: 'suspicious-name',
398
+ message: `Possible hardcoded secret: ${varName} assigned directly to a string literal`,
399
+ value,
400
+ maskedValue: maskSecret(value),
401
+ });
402
+ };
403
+ // Field or local declaration. The modifier list is optional and unordered, so
404
+ // it is matched as a repeated group rather than a fixed sequence.
405
+ const declRe = /(?:(?:public|private|protected|static|final|transient|volatile|synchronized)\s+)*\b(?:String|CharSequence|var)\s+([A-Za-z_$][\w$]*)\s*=\s*"([^"\n]{4,500})"/g;
406
+ let m;
407
+ while ((m = declRe.exec(source)) !== null)
408
+ push(m.index, m[1], m[2]);
409
+ // `.setPassword("...")` / `.setApiKey("...")`: the method name carries the
410
+ // suspect word, so it plays the part the variable name plays above.
411
+ const setterRe = /\.\s*(set[A-Z][\w$]*)\s*\(\s*"([^"\n]{4,500})"\s*\)/g;
412
+ while ((m = setterRe.exec(source)) !== null)
413
+ push(m.index, m[1].slice(3), m[2]);
414
+ // `props.put("spring.datasource.password", "...")` and `setProperty(...)`:
415
+ // here the KEY literal is the name, not the method.
416
+ const keyedRe = /\b(?:put|setProperty|addProperty|set)\s*\(\s*"([\w.\-]+)"\s*,\s*"([^"\n]{4,500})"\s*\)/g;
417
+ while ((m = keyedRe.exec(source)) !== null)
418
+ push(m.index, m[1], m[2]);
419
+ return results;
420
+ }
421
+ /**
422
+ * Nouns that NAME a secret, as opposed to describing a mechanism.
423
+ *
424
+ * A `.properties` file is mostly settings, and a settings key mentioning
425
+ * authentication somewhere in its path is not announcing a credential. The JDK's
426
+ * own `conf/net.properties` ships `jdk.http.ntlm.transparentAuth=disabled`,
427
+ * which the general suspect-keyword test reports: the key contains `auth` and
428
+ * the value is eight characters long. `auth` is the weakest word in the set for
429
+ * exactly this reason - it qualifies a mechanism - so a properties key must end
430
+ * in a word that names the secret itself.
431
+ *
432
+ * `spring.datasource.password` ends in `password` and still fires;
433
+ * `jdk.http.auth.tunneling.disabledSchemes` ends in `disabledSchemes` and does
434
+ * not. Found 2026-08-16 by scanning a real JDK installation, not a fixture.
435
+ */
436
+ const CREDENTIAL_NOUNS = new Set([
437
+ 'key', 'token', 'secret', 'password', 'passwd', 'pwd', 'credential',
438
+ 'credentials', 'apikey', 'accesskey', 'privatekey',
439
+ ]);
440
+ /** The final dotted-or-camelCase segment of a properties key. */
441
+ function finalKeySegment(key) {
442
+ const segments = key
443
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
444
+ .replace(/[^A-Za-z0-9]+/g, ' ')
445
+ .toLowerCase()
446
+ .split(/\s+/)
447
+ .filter(Boolean);
448
+ return segments[segments.length - 1] ?? '';
449
+ }
450
+ /**
451
+ * Layer 1: `.properties` config files.
452
+ *
453
+ * These are the reason `.properties` is a registered Java extension at all: a
454
+ * `spring.datasource.password` is the most commonly committed Java credential.
455
+ * They are key/value pairs with no quotes and no declaration keyword, so nothing
456
+ * else in this file could ever have matched one.
457
+ */
458
+ function detectLayer1Properties(source) {
459
+ const results = [];
460
+ const lines = source.split('\n');
461
+ lines.forEach((rawLine, index) => {
462
+ const line = rawLine.trim();
463
+ // `#` and `!` both start a comment in the .properties format.
464
+ if (line === '' || line.startsWith('#') || line.startsWith('!'))
465
+ return;
466
+ const match = /^([\w.\-/]+)[ \t]*[=:][ \t]*(.*)$/.exec(line);
467
+ if (!match)
468
+ return;
469
+ const key = match[1];
470
+ const value = match[2].trim();
471
+ // The key must END in a word that names a secret, not merely contain one
472
+ // somewhere - see CREDENTIAL_NOUNS.
473
+ if (!CREDENTIAL_NOUNS.has(finalKeySegment(key)))
474
+ return;
475
+ if (value.length < 8 || isPlaceholder(value, key))
476
+ return;
477
+ if (namesAFieldRatherThanHoldingOne(value))
478
+ return;
479
+ if (valueIsNotACredential(value, key))
480
+ return;
481
+ // A value that is only a `${...}` placeholder is resolved at runtime from
482
+ // the environment or a secret store. That is the FIX for this rule, so
483
+ // flagging it would punish the correct pattern - the same exemption the
484
+ // .csproj reader gives `$(MSBuildProperty)` references.
485
+ if (/^\$\{[^}]*\}$/.test(value))
486
+ return;
487
+ results.push({
488
+ line: index + 1,
489
+ variable: key,
490
+ pattern: 'suspicious-name',
491
+ message: `Possible hardcoded secret: ${key} set directly in a properties file`,
492
+ value,
493
+ maskedValue: maskSecret(value),
494
+ });
495
+ });
496
+ return results;
497
+ }
498
+ /**
499
+ * Layer 1: Rust bindings, constants, statics, and struct-literal fields.
500
+ *
501
+ * `let` and `const` happened to work through the TypeScript detector, since it
502
+ * matches those keywords - but `static API_KEY: &str` did not, and neither did
503
+ * a struct literal, which is how configuration is usually written in Rust.
504
+ */
505
+ function detectLayer1Rust(source) {
506
+ const results = [];
507
+ const push = (index, varName, value) => {
508
+ if (!varName || !value || value.length < 8)
509
+ return;
510
+ if (!hasSuspectKeyword(varName) || isPlaceholder(value, varName))
511
+ return;
512
+ if (namesAFieldRatherThanHoldingOne(value))
513
+ return;
514
+ if (valueIsNotACredential(value, varName))
515
+ return;
516
+ results.push({
517
+ line: lineOf(source, index),
518
+ variable: varName,
519
+ pattern: 'suspicious-name',
520
+ message: `Possible hardcoded secret: ${varName} assigned directly to a string literal`,
521
+ value,
522
+ maskedValue: maskSecret(value),
523
+ });
524
+ };
525
+ // The type annotation is optional on `let` and required on `const`/`static`,
526
+ // so it is matched loosely up to the `=`.
527
+ const bindingRe = /\b(?:let(?:\s+mut)?|const|static(?:\s+mut)?)\s+([A-Za-z_]\w*)\s*(?::\s*[^=\n]+)?=\s*"([^"\n]{4,500})"/g;
528
+ let m;
529
+ while ((m = bindingRe.exec(source)) !== null)
530
+ push(m.index, m[1], m[2]);
531
+ // Struct literal or map entry: `Config { api_key: "..." }`. Gated on the
532
+ // suspect-keyword check like everything else, which is what keeps a match arm
533
+ // or a type position from producing noise.
534
+ const fieldRe = /(?:^|[{,])\s*([a-z_]\w*)\s*:\s*"([^"\n]{4,500})"/gm;
535
+ while ((m = fieldRe.exec(source)) !== null)
536
+ push(m.index, m[1], m[2]);
537
+ return results;
538
+ }
189
539
  // Layer 2: Known secret format patterns — language-agnostic, scan full source
190
540
  const KNOWN_PATTERNS = [
191
541
  { re: /sk-[a-zA-Z0-9]{10,}/g, name: 'OpenAI/Stripe secret key (sk-)' },
@@ -229,13 +579,19 @@ function detectHardcodedSecrets(source, config, language, filePath = '') {
229
579
  // and not real. Skip the suspicious-name heuristic (Layer 1) there, but keep
230
580
  // Layer 2 so a genuinely leaked provider token (AWS/Stripe/GitHub/...) is still
231
581
  // flagged even when it appears in a test.
582
+ // A `.properties` file is key/value config, not Java source, so it gets its
583
+ // own reader rather than the Java one. It is matched on the PATH because the
584
+ // registry maps the extension to the `java` language key.
585
+ const isPropertiesFile = /\.properties$/i.test(filePath);
232
586
  const layer1 = (0, guards_1.isTestFile)(filePath)
233
587
  ? []
234
588
  : language === 'go' ? detectLayer1Go(source) :
235
589
  language === 'python' ? detectLayer1Py(source) :
236
590
  language === 'ruby' ? detectLayer1Ruby(source) :
237
591
  language === 'csharp' ? detectLayer1CSharp(source) :
238
- detectLayer1Ts(source);
592
+ language === 'java' ? (isPropertiesFile ? detectLayer1Properties(source) : detectLayer1Java(source)) :
593
+ language === 'rust' ? detectLayer1Rust(source) :
594
+ detectLayer1Ts(source);
239
595
  const layer2 = detectLayer2(source);
240
596
  // Suppress Layer 2 hits on lines already flagged by Layer 1 (avoid duplicate Problems entries)
241
597
  const layer1Lines = new Set(layer1.map(s => s.line));