@activemind/scd 1.4.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.
- package/LICENSE.md +35 -0
- package/README.md +417 -0
- package/bin/scd.js +140 -0
- package/lib/audit-report.js +93 -0
- package/lib/audit-sync.js +172 -0
- package/lib/audit.js +356 -0
- package/lib/cli-helpers.js +108 -0
- package/lib/commands/accept.js +28 -0
- package/lib/commands/audit.js +17 -0
- package/lib/commands/configure.js +200 -0
- package/lib/commands/doctor.js +14 -0
- package/lib/commands/exceptions.js +19 -0
- package/lib/commands/export-findings.js +46 -0
- package/lib/commands/findings.js +306 -0
- package/lib/commands/ignore.js +28 -0
- package/lib/commands/init.js +16 -0
- package/lib/commands/insights.js +24 -0
- package/lib/commands/install.js +15 -0
- package/lib/commands/list.js +109 -0
- package/lib/commands/remove.js +16 -0
- package/lib/commands/repo.js +862 -0
- package/lib/commands/report.js +234 -0
- package/lib/commands/resolve.js +25 -0
- package/lib/commands/rules.js +185 -0
- package/lib/commands/scan.js +519 -0
- package/lib/commands/scope.js +341 -0
- package/lib/commands/sync.js +40 -0
- package/lib/commands/uninstall.js +15 -0
- package/lib/commands/version.js +33 -0
- package/lib/comment-map.js +388 -0
- package/lib/config.js +325 -0
- package/lib/context-modifiers.js +211 -0
- package/lib/deep-analyzer.js +225 -0
- package/lib/doctor.js +236 -0
- package/lib/exception-manager.js +675 -0
- package/lib/export-findings.js +376 -0
- package/lib/file-context.js +380 -0
- package/lib/file-filter.js +204 -0
- package/lib/file-manifest.js +145 -0
- package/lib/git-utils.js +102 -0
- package/lib/global-config.js +239 -0
- package/lib/hooks-manager.js +130 -0
- package/lib/init-repo.js +147 -0
- package/lib/insights-analyzer.js +416 -0
- package/lib/insights-output.js +160 -0
- package/lib/installer.js +128 -0
- package/lib/output-constants.js +32 -0
- package/lib/output-terminal.js +407 -0
- package/lib/push-queue.js +322 -0
- package/lib/remove-repo.js +108 -0
- package/lib/repo-context.js +187 -0
- package/lib/report-html.js +1154 -0
- package/lib/report-index.js +157 -0
- package/lib/report-json.js +136 -0
- package/lib/report-markdown.js +250 -0
- package/lib/resolve-manager.js +148 -0
- package/lib/rule-registry.js +205 -0
- package/lib/scan-cache.js +171 -0
- package/lib/scan-context.js +312 -0
- package/lib/scan-schema.js +67 -0
- package/lib/scanner-full.js +681 -0
- package/lib/scanner-manual.js +348 -0
- package/lib/scanner-secrets.js +83 -0
- package/lib/scope.js +331 -0
- package/lib/store-verify.js +395 -0
- package/lib/store.js +310 -0
- package/lib/taint-register.js +196 -0
- package/lib/version-check.js +46 -0
- package/package.json +37 -0
- package/rules/rule-loader.js +324 -0
- package/rules/rules-aspx-cs.json +399 -0
- package/rules/rules-aspx.json +222 -0
- package/rules/rules-infra-leakage.json +434 -0
- package/rules/rules-js.json +664 -0
- package/rules/rules-php.json +521 -0
- package/rules/rules-python.json +466 -0
- package/rules/rules-secrets.json +99 -0
- package/rules/rules-sensitive-files.json +475 -0
- package/rules/rules-ts.json +76 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* comment-map.js
|
|
3
|
+
* Builds a map of comment lines for a file before rules are applied.
|
|
4
|
+
*
|
|
5
|
+
* Returns a CommentMap object that classifies each line as:
|
|
6
|
+
* CODE — no comment content on this line
|
|
7
|
+
* COMMENT — entire line is a comment (line comment or inside a block comment)
|
|
8
|
+
* MIXED — line contains both code and an inline comment
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const map = buildCommentMap(content, 'js');
|
|
12
|
+
* map.isComment(lineNumber) // true if line is COMMENT
|
|
13
|
+
* map.isMixed(lineNumber) // true if line is MIXED
|
|
14
|
+
* map.lineType(lineNumber) // 'CODE' | 'COMMENT' | 'MIXED'
|
|
15
|
+
*
|
|
16
|
+
* Design notes:
|
|
17
|
+
* - Content is never modified — no side-effects on matchIndex or line-offsets.
|
|
18
|
+
* - Block comments tracked via range state machine, not regex on each line.
|
|
19
|
+
* - Inline comment detection is optional (disabled by default per config).
|
|
20
|
+
* - Unsupported extensions return an empty map (all lines → CODE).
|
|
21
|
+
* - Multi-line string literals (Python triple-quotes) treated as comments
|
|
22
|
+
* when used as docstrings (file/function level), not when assigned to a
|
|
23
|
+
* variable (those are data, not comments).
|
|
24
|
+
*
|
|
25
|
+
* Future: this module is designed to become a first-class scan step in the
|
|
26
|
+
* proposed 6-step execution model. It produces structured line metadata that
|
|
27
|
+
* can feed both the code scanner (skip COMMENT lines) and a dedicated comment
|
|
28
|
+
* scanner (analyse COMMENT lines with separate rules or AI).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
// ── Line type constants ────────────────────────────────────────────────────
|
|
34
|
+
const LINE_TYPE = {
|
|
35
|
+
CODE: 'CODE',
|
|
36
|
+
COMMENT: 'COMMENT',
|
|
37
|
+
MIXED: 'MIXED',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ── Language comment syntax ────────────────────────────────────────────────
|
|
41
|
+
// Each entry defines:
|
|
42
|
+
// lineStart {string[]} Prefixes that make an entire line a comment (trimmed)
|
|
43
|
+
// blockOpen {string} Block comment open marker
|
|
44
|
+
// blockClose {string} Block comment close marker
|
|
45
|
+
// inlineStart {string[]} Markers that begin an inline (end-of-line) comment
|
|
46
|
+
//
|
|
47
|
+
// Notes:
|
|
48
|
+
// - YAML and Shell have no block comments. # is both line and inline.
|
|
49
|
+
// - Python triple-quote strings are handled separately via TRIPLE_QUOTE_EXTS.
|
|
50
|
+
// - SQL uses -- for line comments and /* */ for block comments.
|
|
51
|
+
// - XML/HTML comments are <!-- --> and can span multiple lines.
|
|
52
|
+
|
|
53
|
+
const SYNTAX = {
|
|
54
|
+
// JavaScript / TypeScript / C# / Java — C-style comments
|
|
55
|
+
js: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
56
|
+
mjs: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
57
|
+
cjs: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
58
|
+
ts: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
59
|
+
jsx: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
60
|
+
tsx: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
61
|
+
cs: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
62
|
+
go: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
63
|
+
java: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
64
|
+
kt: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
65
|
+
rs: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
66
|
+
|
|
67
|
+
// PHP — supports both // and # as line comments, plus /* */
|
|
68
|
+
php: { lineStart: ['//', '#'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//', '#'] },
|
|
69
|
+
|
|
70
|
+
// Python — # for line/inline; triple-quoted strings handled separately
|
|
71
|
+
py: { lineStart: ['#'], blockOpen: null, blockClose: null, inlineStart: ['#'] },
|
|
72
|
+
|
|
73
|
+
// Shell / Bash / PowerShell
|
|
74
|
+
sh: { lineStart: ['#'], blockOpen: null, blockClose: null, inlineStart: ['#'] },
|
|
75
|
+
bash: { lineStart: ['#'], blockOpen: null, blockClose: null, inlineStart: ['#'] },
|
|
76
|
+
ps1: { lineStart: ['#'], blockOpen: '<#', blockClose: '#>', inlineStart: ['#'] },
|
|
77
|
+
|
|
78
|
+
// Ruby
|
|
79
|
+
rb: { lineStart: ['#'], blockOpen: '=begin', blockClose: '=end', inlineStart: ['#'] },
|
|
80
|
+
|
|
81
|
+
// YAML — # only, no block comments
|
|
82
|
+
yml: { lineStart: ['#'], blockOpen: null, blockClose: null, inlineStart: ['#'] },
|
|
83
|
+
yaml: { lineStart: ['#'], blockOpen: null, blockClose: null, inlineStart: ['#'] },
|
|
84
|
+
|
|
85
|
+
// SQL
|
|
86
|
+
sql: { lineStart: ['--'], blockOpen: '/*', blockClose: '*/', inlineStart: ['--'] },
|
|
87
|
+
|
|
88
|
+
// INI / CFG / properties — ; and # as line comments
|
|
89
|
+
ini: { lineStart: [';', '#'], blockOpen: null, blockClose: null, inlineStart: [';', '#'] },
|
|
90
|
+
cfg: { lineStart: [';', '#'], blockOpen: null, blockClose: null, inlineStart: [';', '#'] },
|
|
91
|
+
conf: { lineStart: [';', '#'], blockOpen: null, blockClose: null, inlineStart: [';', '#'] },
|
|
92
|
+
properties: { lineStart: ['#', '!'], blockOpen: null, blockClose: null, inlineStart: [] },
|
|
93
|
+
|
|
94
|
+
// XML / HTML — <!-- --> block comments only
|
|
95
|
+
xml: { lineStart: [], blockOpen: '<!--', blockClose: '-->', inlineStart: [] },
|
|
96
|
+
html: { lineStart: [], blockOpen: '<!--', blockClose: '-->', inlineStart: [] },
|
|
97
|
+
|
|
98
|
+
// ASP.NET / Web.config — same comment syntax as XML/HTML
|
|
99
|
+
aspx: { lineStart: [], blockOpen: '<!--', blockClose: '-->', inlineStart: [] },
|
|
100
|
+
ascx: { lineStart: [], blockOpen: '<!--', blockClose: '-->', inlineStart: [] },
|
|
101
|
+
master: { lineStart: [], blockOpen: '<!--', blockClose: '-->', inlineStart: [] },
|
|
102
|
+
config: { lineStart: [], blockOpen: '<!--', blockClose: '-->', inlineStart: [] },
|
|
103
|
+
|
|
104
|
+
// Markdown — # headings are not executable, treat as comment-like.
|
|
105
|
+
// Findings in Markdown headings/paragraphs are almost never actionable.
|
|
106
|
+
md: { lineStart: ['#'], blockOpen: null, blockClose: null, inlineStart: [] },
|
|
107
|
+
|
|
108
|
+
// Backup/original files — inherit C-style // as default since most backed-up
|
|
109
|
+
// source files are JS/TS/C#. Findings in backup files are rarely actionable.
|
|
110
|
+
bak: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
111
|
+
old: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
112
|
+
orig: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
|
|
113
|
+
|
|
114
|
+
// Batch / Windows CMD — REM and ::
|
|
115
|
+
bat: { lineStart: ['rem ', 'rem\t', '::'], blockOpen: null, blockClose: null, inlineStart: [] },
|
|
116
|
+
cmd: { lineStart: ['rem ', 'rem\t', '::'], blockOpen: null, blockClose: null, inlineStart: [] },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Extensions where triple-quoted strings act as docstrings/comments
|
|
120
|
+
// when at the start of a file, class, or function.
|
|
121
|
+
// We treat any triple-quoted string that starts on a line with no preceding code
|
|
122
|
+
// as a COMMENT block for the purposes of FP suppression.
|
|
123
|
+
const TRIPLE_QUOTE_EXTS = new Set(['py']);
|
|
124
|
+
|
|
125
|
+
// ── CommentMap ────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
class CommentMap {
|
|
128
|
+
/**
|
|
129
|
+
* @param {Map<number, string>} lineTypes Map of 1-based lineNumber → LINE_TYPE
|
|
130
|
+
*/
|
|
131
|
+
constructor(lineTypes) {
|
|
132
|
+
this._types = lineTypes;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Returns true if the line is entirely a comment (or inside a block comment). */
|
|
136
|
+
isComment(lineNumber) {
|
|
137
|
+
return this._types.get(lineNumber) === LINE_TYPE.COMMENT;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Returns true if the line has both code and an inline comment. */
|
|
141
|
+
isMixed(lineNumber) {
|
|
142
|
+
return this._types.get(lineNumber) === LINE_TYPE.MIXED;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Returns 'CODE' | 'COMMENT' | 'MIXED'. Defaults to 'CODE' for unknown lines. */
|
|
146
|
+
lineType(lineNumber) {
|
|
147
|
+
return this._types.get(lineNumber) || LINE_TYPE.CODE;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Total number of lines classified as COMMENT. */
|
|
151
|
+
get commentCount() {
|
|
152
|
+
let n = 0;
|
|
153
|
+
for (const t of this._types.values()) if (t === LINE_TYPE.COMMENT) n++;
|
|
154
|
+
return n;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Total number of lines classified as MIXED. */
|
|
158
|
+
get mixedCount() {
|
|
159
|
+
let n = 0;
|
|
160
|
+
for (const t of this._types.values()) if (t === LINE_TYPE.MIXED) n++;
|
|
161
|
+
return n;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Empty map (for unsupported extensions) ─────────────────────────────────
|
|
166
|
+
const EMPTY_MAP = new CommentMap(new Map());
|
|
167
|
+
|
|
168
|
+
// ── Builder ────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build a CommentMap for the given file content and extension.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} content Full file content
|
|
174
|
+
* @param {string} ext File extension (lowercase, without dot)
|
|
175
|
+
* @param {object} [opts]
|
|
176
|
+
* @param {boolean} [opts.inline] Classify MIXED lines (inline comments). Default: false.
|
|
177
|
+
* @returns {CommentMap}
|
|
178
|
+
*/
|
|
179
|
+
function buildCommentMap(content, ext, opts = {}) {
|
|
180
|
+
const syntax = SYNTAX[ext];
|
|
181
|
+
if (!syntax) return EMPTY_MAP;
|
|
182
|
+
|
|
183
|
+
const { inline = false } = opts;
|
|
184
|
+
const lines = content.split('\n');
|
|
185
|
+
const lineTypes = new Map();
|
|
186
|
+
|
|
187
|
+
// State for block comment tracking
|
|
188
|
+
let inBlock = false;
|
|
189
|
+
|
|
190
|
+
// State for Python triple-quote docstring tracking
|
|
191
|
+
let inTriple = false;
|
|
192
|
+
let tripleMarker = null; // '"""' or "'''"
|
|
193
|
+
|
|
194
|
+
for (let i = 0; i < lines.length; i++) {
|
|
195
|
+
const lineNum = i + 1; // 1-based
|
|
196
|
+
const raw = lines[i];
|
|
197
|
+
const trimmed = raw.trimStart();
|
|
198
|
+
|
|
199
|
+
// ── Python triple-quote docstrings ───────────────────────────────────
|
|
200
|
+
if (TRIPLE_QUOTE_EXTS.has(ext)) {
|
|
201
|
+
if (inTriple) {
|
|
202
|
+
// Inside a triple-quoted string — classify as COMMENT
|
|
203
|
+
lineTypes.set(lineNum, LINE_TYPE.COMMENT);
|
|
204
|
+
// Check if this line closes the triple quote
|
|
205
|
+
const closeIdx = raw.indexOf(tripleMarker);
|
|
206
|
+
if (closeIdx !== -1) {
|
|
207
|
+
inTriple = false;
|
|
208
|
+
tripleMarker = null;
|
|
209
|
+
// Line that closes the triple-quote is still COMMENT —
|
|
210
|
+
// closing marker is part of the docstring
|
|
211
|
+
}
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check if this line opens a triple-quoted string that acts as a docstring.
|
|
216
|
+
// We treat it as a docstring (COMMENT) when the line starts with the marker
|
|
217
|
+
// (possibly with leading whitespace) with no preceding code.
|
|
218
|
+
const tripleOpen = trimmed.startsWith('"""') ? '"""'
|
|
219
|
+
: trimmed.startsWith("'''") ? "'''"
|
|
220
|
+
: null;
|
|
221
|
+
|
|
222
|
+
if (tripleOpen) {
|
|
223
|
+
// Check if the triple-quote closes on the same line (single-line docstring)
|
|
224
|
+
const rest = trimmed.slice(3);
|
|
225
|
+
const closeIdx = rest.indexOf(tripleOpen);
|
|
226
|
+
if (closeIdx !== -1) {
|
|
227
|
+
// Same-line triple-quote: classify as COMMENT
|
|
228
|
+
lineTypes.set(lineNum, LINE_TYPE.COMMENT);
|
|
229
|
+
} else {
|
|
230
|
+
// Multi-line triple-quote opens here
|
|
231
|
+
inTriple = true;
|
|
232
|
+
tripleMarker = tripleOpen;
|
|
233
|
+
lineTypes.set(lineNum, LINE_TYPE.COMMENT);
|
|
234
|
+
}
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Block comment state ──────────────────────────────────────────────
|
|
240
|
+
if (syntax.blockOpen && syntax.blockClose) {
|
|
241
|
+
if (inBlock) {
|
|
242
|
+
lineTypes.set(lineNum, LINE_TYPE.COMMENT);
|
|
243
|
+
// Check if the block closes on this line
|
|
244
|
+
const closeIdx = raw.indexOf(syntax.blockClose);
|
|
245
|
+
if (closeIdx !== -1) {
|
|
246
|
+
inBlock = false;
|
|
247
|
+
// If there is non-whitespace content after the close marker,
|
|
248
|
+
// this line transitions back to CODE after the close — but since
|
|
249
|
+
// the close marker itself is comment content, treat the whole line
|
|
250
|
+
// as COMMENT for simplicity. The code after */ is rare and low-risk.
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check if a block comment opens on this line
|
|
256
|
+
const openIdx = raw.indexOf(syntax.blockOpen);
|
|
257
|
+
if (openIdx !== -1) {
|
|
258
|
+
// Verify no code precedes the block open on this line
|
|
259
|
+
const before = raw.slice(0, openIdx).trim();
|
|
260
|
+
|
|
261
|
+
// Check if the block also closes on this same line
|
|
262
|
+
const afterOpen = raw.slice(openIdx + syntax.blockOpen.length);
|
|
263
|
+
const closeOnSame = afterOpen.indexOf(syntax.blockClose);
|
|
264
|
+
|
|
265
|
+
if (before.length === 0) {
|
|
266
|
+
// Block open starts the line — it's a COMMENT line
|
|
267
|
+
if (closeOnSame !== -1) {
|
|
268
|
+
// Opens and closes on same line — single-line block comment
|
|
269
|
+
lineTypes.set(lineNum, LINE_TYPE.COMMENT);
|
|
270
|
+
} else {
|
|
271
|
+
// Block opens but doesn't close — mark as COMMENT, enter block state
|
|
272
|
+
inBlock = true;
|
|
273
|
+
lineTypes.set(lineNum, LINE_TYPE.COMMENT);
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
// Code precedes the block open — MIXED line, block may continue
|
|
277
|
+
if (closeOnSame === -1) {
|
|
278
|
+
inBlock = true;
|
|
279
|
+
}
|
|
280
|
+
if (inline) {
|
|
281
|
+
lineTypes.set(lineNum, LINE_TYPE.MIXED);
|
|
282
|
+
}
|
|
283
|
+
// If inline is off, this stays CODE — block tracking still applies
|
|
284
|
+
}
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── Line comment check ───────────────────────────────────────────────
|
|
290
|
+
if (syntax.lineStart.length > 0) {
|
|
291
|
+
// Batch files: 'rem' is case-insensitive
|
|
292
|
+
const checkStr = ext === 'bat' || ext === 'cmd'
|
|
293
|
+
? trimmed.toLowerCase()
|
|
294
|
+
: trimmed;
|
|
295
|
+
|
|
296
|
+
const isLineComment = syntax.lineStart.some(prefix => checkStr.startsWith(prefix));
|
|
297
|
+
if (isLineComment) {
|
|
298
|
+
// ── Magic comment exception ────────────────────────────────────
|
|
299
|
+
// JS/TS "magic comments" begin with //# or //@ and are runtime-
|
|
300
|
+
// significant directives, not documentation:
|
|
301
|
+
// //# sourceMappingURL=file.js.map (source maps — browser reads this)
|
|
302
|
+
// //@ sourceURL=file.js (older source map syntax)
|
|
303
|
+
// //# sourceURL=... (eval source naming)
|
|
304
|
+
// TypeScript compiler directives (// @ts-ignore etc.) are handled
|
|
305
|
+
// separately: they start with "// @" (space before @) and are
|
|
306
|
+
// intentionally classified as CODE per design decision 2026-05-15.
|
|
307
|
+
if ((ext === 'js' || ext === 'ts' || ext === 'mjs' || ext === 'cjs' ||
|
|
308
|
+
ext === 'jsx' || ext === 'tsx') &&
|
|
309
|
+
(trimmed.startsWith('//#') || trimmed.startsWith('//@'))) {
|
|
310
|
+
// Magic comment — treat as CODE, fall through
|
|
311
|
+
} else {
|
|
312
|
+
lineTypes.set(lineNum, LINE_TYPE.COMMENT);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Inline comment check (optional) ──────────────────────────────────
|
|
319
|
+
if (inline && syntax.inlineStart.length > 0) {
|
|
320
|
+
// Detect inline comment: look for the marker outside of string literals.
|
|
321
|
+
// Full string-aware parsing is expensive; we use a heuristic:
|
|
322
|
+
// find the first inline marker occurrence and check if it's likely
|
|
323
|
+
// inside a string by counting unescaped quotes before it.
|
|
324
|
+
const inlineMarker = findInlineCommentMarker(raw, syntax.inlineStart);
|
|
325
|
+
if (inlineMarker !== -1) {
|
|
326
|
+
lineTypes.set(lineNum, LINE_TYPE.MIXED);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ── Default: CODE ─────────────────────────────────────────────────────
|
|
332
|
+
// Don't set lineTypes for CODE lines — isComment() defaults to CODE.
|
|
333
|
+
// This keeps the Map small (only COMMENT and MIXED lines stored).
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return new CommentMap(lineTypes);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── Inline comment heuristic ──────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Find the character index of an inline comment marker in a line,
|
|
343
|
+
* using a simple heuristic to skip markers inside string literals.
|
|
344
|
+
*
|
|
345
|
+
* Returns -1 if no inline comment marker found outside a string.
|
|
346
|
+
*
|
|
347
|
+
* Heuristic: track whether we're inside a string by counting unescaped
|
|
348
|
+
* quote characters. Not perfect for complex cases (nested quotes, template
|
|
349
|
+
* literals) but sufficient for the common cases we care about.
|
|
350
|
+
*
|
|
351
|
+
* @param {string} line Raw line content
|
|
352
|
+
* @param {string[]} markers List of inline comment marker strings
|
|
353
|
+
* @returns {number} Index of marker, or -1
|
|
354
|
+
*/
|
|
355
|
+
function findInlineCommentMarker(line, markers) {
|
|
356
|
+
let inString = false;
|
|
357
|
+
let stringChar = null;
|
|
358
|
+
|
|
359
|
+
for (let i = 0; i < line.length; i++) {
|
|
360
|
+
const ch = line[i];
|
|
361
|
+
|
|
362
|
+
// Toggle string state on unescaped quote characters
|
|
363
|
+
if (!inString && (ch === '"' || ch === "'" || ch === '`')) {
|
|
364
|
+
inString = true;
|
|
365
|
+
stringChar = ch;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (inString && ch === stringChar && line[i - 1] !== '\\') {
|
|
369
|
+
inString = false;
|
|
370
|
+
stringChar = null;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Skip characters inside strings
|
|
375
|
+
if (inString) continue;
|
|
376
|
+
|
|
377
|
+
// Check each marker at current position
|
|
378
|
+
for (const marker of markers) {
|
|
379
|
+
if (line.startsWith(marker, i)) {
|
|
380
|
+
return i;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return -1;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = { buildCommentMap, CommentMap, LINE_TYPE };
|