@nudojs/parser 1.1.7 → 1.2.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/dist/index.d.ts +28 -2
- package/dist/index.js +269 -111
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { File, Node, AssignmentExpression, Expression, Identifier, MemberExpression, Program, ClassBody, VariableDeclarator, ExportSpecifier, ExportDefaultSpecifier, ExportNamespaceSpecifier, Statement } from '@babel/types';
|
|
2
2
|
import { Abs, MockHelper } from '@nudojs/core';
|
|
3
|
-
export { stripTypes } from '@nudojs/core';
|
|
3
|
+
export { CaseTag, FnDirectiveScope, MockModuleRecord, NudoImportRecord, extractFileEnvNames, extractMockModuleRecords, extractNudoImportRecords, findFnDirectiveScope, fnDirectiveCommentLines, listFnDirectiveScopes, parseEnvPayload, parseMockModulePayload, parseNudoImportPayload, scanBudgetDecl, scanCaseTags, scanContractSegments, scanThrowsDecl, stripTypes } from '@nudojs/core';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* 所有权:Babel 解析与 TS 剥除的**实现**在 `@nudojs/core`
|
|
@@ -170,13 +170,39 @@ type FunctionWithDirectives = {
|
|
|
170
170
|
name: string;
|
|
171
171
|
directives: Directive[];
|
|
172
172
|
};
|
|
173
|
+
type DirectiveDiag = {
|
|
174
|
+
code: string;
|
|
175
|
+
message: string;
|
|
176
|
+
};
|
|
177
|
+
declare function setDirectiveDiagCollector(fn: ((d: DirectiveDiag) => void) | null): void;
|
|
178
|
+
/** 当前诊断累计序号(since 锚:消费方只排干自身 extract 产生的增量) */
|
|
179
|
+
declare function directiveDiagCount(): number;
|
|
180
|
+
/** 取走已收集的诊断(全量排干 + 清空 seen)——CLI/测试整批消费 */
|
|
181
|
+
declare function takeDirectiveDiags(): DirectiveDiag[];
|
|
182
|
+
/**
|
|
183
|
+
* 只取走 seq > since 的诊断(清空仅限增量)——对齐 takeInterfaceDiagsSince。
|
|
184
|
+
* LSP 长驻进程里 analyze/check/lens 用它排干**自身 extract** 产生的诊断,
|
|
185
|
+
* 不窃取在途其他消费方待收的指令文法诊断(全量 take 曾在 await 窗口偷走跨文件诊断)。
|
|
186
|
+
*/
|
|
187
|
+
declare function takeDirectiveDiagsSince(since: number): DirectiveDiag[];
|
|
173
188
|
/**
|
|
174
189
|
* case 实参 / 指令类型表达式唯一文法:约束构建器优先,其余为具体字面量、
|
|
175
190
|
* 结构字面量与箭头函数。`T.*` 文法已物理删除。
|
|
176
191
|
*/
|
|
177
192
|
declare function parseCaseArgExpr(expr: string): Abs;
|
|
193
|
+
/**
|
|
194
|
+
* D6=G2:指令绑定 AST 最近 Function(含 nested function、class method)。
|
|
195
|
+
* 作用域清单与 refine 的 @nudo:contract 同源(core directive-scan),
|
|
196
|
+
* case 与 contract 对同一函数集合同时可见。
|
|
197
|
+
*/
|
|
178
198
|
declare function extractDirectives(ast: Node): FunctionWithDirectives[];
|
|
199
|
+
/**
|
|
200
|
+
* 纯查询 extract:排干自身产生的指令文法诊断增量并丢弃——
|
|
201
|
+
* hover/completion/collectSkipReturns 等探测路径不得把诊断留在全局 buffer
|
|
202
|
+
* 供在途 validate/check 误窃,也不得自己背走别人的在途诊断。
|
|
203
|
+
*/
|
|
204
|
+
declare function extractDirectivesQuiet(ast: Node): FunctionWithDirectives[];
|
|
179
205
|
declare function extractInlineDirectives(node: Node): InlineDirective[];
|
|
180
206
|
declare function extractFileDirectives(ast: Node): FileDirective[];
|
|
181
207
|
|
|
182
|
-
export { type AsDirective, type CaseDirective, type Directive, type EnvDirective, type FileDirective, type FunctionWithDirectives, type InlineDirective, type InstanceMethod, type MockDirective, type MockModuleDirective, type PureDirective, type ReplaceDirective, type SampleDirective, type SinonExpression, type SkipDirective, asAssignmentExpression, asExpression, asIdentifier, asMemberExpression, asProgram, classIdName, classInstanceMethods, classMemberKey, classMemberKeyName, exportSpecifierExportedName, exportSpecifierLocalName, extractDirectives, extractFileDirectives, extractInlineDirectives, fnOrClassIdLoc, fnOrClassIdName, getClassMembers, getDeclarations, getDeclaratorId, getDeclaratorInit, getExportDeclaration, getExportSpecifiers, getExpressionStatementExpression, getFirstDeclaratorId, identifierName, isModuleExportsMember, memberObjectName, memberPropertyKey, memberPropertyName, methodFunctionNode, nameOrStringValue, paramDisplayName, paramName, parse, parseCaseArgExpr, programBody, unwrapDefaultExport, unwrapExport };
|
|
208
|
+
export { type AsDirective, type CaseDirective, type Directive, type DirectiveDiag, type EnvDirective, type FileDirective, type FunctionWithDirectives, type InlineDirective, type InstanceMethod, type MockDirective, type MockModuleDirective, type PureDirective, type ReplaceDirective, type SampleDirective, type SinonExpression, type SkipDirective, asAssignmentExpression, asExpression, asIdentifier, asMemberExpression, asProgram, classIdName, classInstanceMethods, classMemberKey, classMemberKeyName, directiveDiagCount, exportSpecifierExportedName, exportSpecifierLocalName, extractDirectives, extractDirectivesQuiet, extractFileDirectives, extractInlineDirectives, fnOrClassIdLoc, fnOrClassIdName, getClassMembers, getDeclarations, getDeclaratorId, getDeclaratorInit, getExportDeclaration, getExportSpecifiers, getExpressionStatementExpression, getFirstDeclaratorId, identifierName, isModuleExportsMember, memberObjectName, memberPropertyKey, memberPropertyName, methodFunctionNode, nameOrStringValue, paramDisplayName, paramName, parse, parseCaseArgExpr, programBody, setDirectiveDiagCollector, takeDirectiveDiags, takeDirectiveDiagsSince, unwrapDefaultExport, unwrapExport };
|
package/dist/index.js
CHANGED
|
@@ -189,7 +189,14 @@ import {
|
|
|
189
189
|
execNudoModule,
|
|
190
190
|
isNudoConstraint,
|
|
191
191
|
constraintToEntryAbs,
|
|
192
|
-
|
|
192
|
+
CONSTRAINT_EXPR_RE,
|
|
193
|
+
SELF,
|
|
194
|
+
listFnDirectiveScopes,
|
|
195
|
+
parseEnvPayload,
|
|
196
|
+
parseMockModulePayload,
|
|
197
|
+
scanCaseArgSpans,
|
|
198
|
+
scanCaseTags,
|
|
199
|
+
extractBalancedParens
|
|
193
200
|
} from "@nudojs/core";
|
|
194
201
|
function absExact(shape) {
|
|
195
202
|
return makeAbs(shape, void 0, void 0, "exact");
|
|
@@ -210,13 +217,56 @@ function absLit(v) {
|
|
|
210
217
|
if (v === null) return absNullLit();
|
|
211
218
|
return absUndefLit();
|
|
212
219
|
}
|
|
213
|
-
var
|
|
214
|
-
var
|
|
215
|
-
var
|
|
216
|
-
var
|
|
217
|
-
var
|
|
218
|
-
|
|
219
|
-
|
|
220
|
+
var directiveDiagCollector = null;
|
|
221
|
+
var directiveDiagSeq = 0;
|
|
222
|
+
var directiveDiags = [];
|
|
223
|
+
var MAX_DIRECTIVE_DIAGS = 1024;
|
|
224
|
+
var directiveDiagSeen = /* @__PURE__ */ new Set();
|
|
225
|
+
function setDirectiveDiagCollector(fn) {
|
|
226
|
+
directiveDiagCollector = fn;
|
|
227
|
+
}
|
|
228
|
+
function directiveDiagCount() {
|
|
229
|
+
return directiveDiagSeq;
|
|
230
|
+
}
|
|
231
|
+
function takeDirectiveDiags() {
|
|
232
|
+
const out = directiveDiags.map((e) => e.d);
|
|
233
|
+
directiveDiags.length = 0;
|
|
234
|
+
directiveDiagSeen.clear();
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
function takeDirectiveDiagsSince(since) {
|
|
238
|
+
const out = [];
|
|
239
|
+
let kept = 0;
|
|
240
|
+
for (const e of directiveDiags) {
|
|
241
|
+
if (e.seq > since) out.push(e.d);
|
|
242
|
+
else directiveDiags[kept++] = e;
|
|
243
|
+
}
|
|
244
|
+
directiveDiags.length = kept;
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
function emitDirectiveDiag(d) {
|
|
248
|
+
const key = `${d.code}\0${d.message}`;
|
|
249
|
+
if (directiveDiagSeen.has(key)) return;
|
|
250
|
+
directiveDiagSeen.add(key);
|
|
251
|
+
if (directiveDiags.length >= MAX_DIRECTIVE_DIAGS) {
|
|
252
|
+
directiveDiags.shift();
|
|
253
|
+
directiveDiagSeen.clear();
|
|
254
|
+
}
|
|
255
|
+
directiveDiags.push({ seq: ++directiveDiagSeq, d });
|
|
256
|
+
directiveDiagCollector?.(d);
|
|
257
|
+
}
|
|
258
|
+
function resetDirectiveDiagSeen() {
|
|
259
|
+
directiveDiagSeen.clear();
|
|
260
|
+
}
|
|
261
|
+
var CASE_TAG_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:case\s+([^\n]+)/g;
|
|
262
|
+
var JS_IDENT_RE = /^[\p{L}$_][\p{L}\p{N}$_]*$/u;
|
|
263
|
+
var MOCK_INLINE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:mock\s+([^\s=]+)\s*=\s*(.+)/g;
|
|
264
|
+
var MOCK_FROM_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:mock\s+([^\s]+)\s+from\s+"([^"]+)"/g;
|
|
265
|
+
var MOCK_TAG_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:mock\s+([^\n]+)/g;
|
|
266
|
+
var PURE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:pure\b/g;
|
|
267
|
+
var SKIP_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:skip\b(?:[ \t]+(\S[^\n]*))?/g;
|
|
268
|
+
var SAMPLE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:sample\b(?:[ \t]+(\S+))?/g;
|
|
269
|
+
var SAMPLE_NUM_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
220
270
|
function tryParseConstraint(expr) {
|
|
221
271
|
const s = expr.trim();
|
|
222
272
|
if (!CONSTRAINT_EXPR_RE.test(s)) return void 0;
|
|
@@ -350,11 +400,6 @@ function scanWithStrings(s, onChar, onStructural) {
|
|
|
350
400
|
const ch = s[i];
|
|
351
401
|
if (inString) {
|
|
352
402
|
onChar(ch, i);
|
|
353
|
-
if (ch === "\\") {
|
|
354
|
-
i++;
|
|
355
|
-
if (i < s.length) onChar(s[i], i);
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
403
|
if (ch === inString) inString = null;
|
|
359
404
|
continue;
|
|
360
405
|
}
|
|
@@ -388,11 +433,6 @@ function splitTopLevelArgs(s) {
|
|
|
388
433
|
const ch = s[i];
|
|
389
434
|
if (inString) {
|
|
390
435
|
current += ch;
|
|
391
|
-
if (ch === "\\") {
|
|
392
|
-
i++;
|
|
393
|
-
if (i < s.length) current += s[i];
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
436
|
if (ch === inString) inString = null;
|
|
397
437
|
continue;
|
|
398
438
|
}
|
|
@@ -448,30 +488,6 @@ function findTopLevelArrow(s) {
|
|
|
448
488
|
}
|
|
449
489
|
return -1;
|
|
450
490
|
}
|
|
451
|
-
function extractBalancedParens(text, startIdx) {
|
|
452
|
-
if (text[startIdx] !== "(") return null;
|
|
453
|
-
let depth = 0;
|
|
454
|
-
let inString = null;
|
|
455
|
-
for (let i = startIdx; i < text.length; i++) {
|
|
456
|
-
const ch = text[i];
|
|
457
|
-
if (inString) {
|
|
458
|
-
if (ch === "\\") {
|
|
459
|
-
i++;
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
if (ch === inString) inString = null;
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
if (ch === '"' || ch === "'") {
|
|
466
|
-
inString = ch;
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
if (ch === "(") depth++;
|
|
470
|
-
if (ch === ")") depth--;
|
|
471
|
-
if (depth === 0) return text.slice(startIdx + 1, i);
|
|
472
|
-
}
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
491
|
function parseArrowFunctionExpr(expr) {
|
|
476
492
|
try {
|
|
477
493
|
const wrapped = `const __mock = ${expr};`;
|
|
@@ -502,9 +518,46 @@ function parseArrowFunctionExpr(expr) {
|
|
|
502
518
|
return null;
|
|
503
519
|
}
|
|
504
520
|
}
|
|
521
|
+
function isExplicitUnknownExpr(expr) {
|
|
522
|
+
const s = expr.trim();
|
|
523
|
+
return s === "unknown" || s === "any" || s === "any()";
|
|
524
|
+
}
|
|
505
525
|
function parseAbsExpr(expr) {
|
|
506
526
|
return parseCaseArgExpr(expr);
|
|
507
527
|
}
|
|
528
|
+
function parseAbsExprDiag(expr, context) {
|
|
529
|
+
const abs = parseCaseArgExpr(expr);
|
|
530
|
+
if (abs.shape.k === "unknown" && !abs.term && !isExplicitUnknownExpr(expr)) {
|
|
531
|
+
emitDirectiveDiag({
|
|
532
|
+
code: "nudo:directive-syntax",
|
|
533
|
+
message: `Unrecognized type expression in ${context}: ${expr.trim()} (use constraint builders, literals, or structure syntax)`
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
return abs;
|
|
537
|
+
}
|
|
538
|
+
function parseSkipReturnsExpr(rest) {
|
|
539
|
+
if (rest === void 0) return void 0;
|
|
540
|
+
const s = rest.trim();
|
|
541
|
+
if (s === "") return void 0;
|
|
542
|
+
if (s.startsWith("=>")) {
|
|
543
|
+
const expr = s.slice(2).trim();
|
|
544
|
+
if (expr === "") {
|
|
545
|
+
emitDirectiveDiag({
|
|
546
|
+
code: "nudo:directive-syntax",
|
|
547
|
+
message: `@nudo:skip => requires a type expression (e.g. @nudo:skip => number())`
|
|
548
|
+
});
|
|
549
|
+
return void 0;
|
|
550
|
+
}
|
|
551
|
+
return parseAbsExprDiag(expr, "@nudo:skip =>");
|
|
552
|
+
}
|
|
553
|
+
if (s.startsWith("(")) {
|
|
554
|
+
const inner = extractBalancedParens(s, 0);
|
|
555
|
+
if (inner !== null && s.slice(inner.length + 2).trim() === "") {
|
|
556
|
+
return parseAbsExprDiag(inner, "@nudo:skip (...)");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return void 0;
|
|
560
|
+
}
|
|
508
561
|
function parseSinonExpr(expr) {
|
|
509
562
|
const s = expr.trim();
|
|
510
563
|
const stubReturnsMatch = s.match(/^sinon\.stub\(\)\.returns\((.+)\)$/);
|
|
@@ -671,17 +724,24 @@ function parseNudoMockExpr(expr) {
|
|
|
671
724
|
}
|
|
672
725
|
return null;
|
|
673
726
|
}
|
|
674
|
-
function
|
|
727
|
+
function parseDirectivesFromCommentTexts(comments) {
|
|
675
728
|
const directives = [];
|
|
676
|
-
for (const
|
|
677
|
-
const
|
|
729
|
+
for (const { text, startLine: commentStartLine } of comments) {
|
|
730
|
+
const caseArgSpans = scanCaseArgSpans(text);
|
|
731
|
+
const caseTags = scanCaseTags(text);
|
|
732
|
+
scanMalformedCaseNames(text, caseArgSpans);
|
|
733
|
+
const inCaseArgs = (idx) => caseArgSpans.some(([s, e]) => idx >= s && idx < e);
|
|
734
|
+
scanMalformedMockNames(text, caseArgSpans);
|
|
678
735
|
MOCK_FROM_REGEX.lastIndex = 0;
|
|
679
736
|
let mockFromMatch;
|
|
680
737
|
const mockFromRanges = [];
|
|
681
738
|
while ((mockFromMatch = MOCK_FROM_REGEX.exec(text)) !== null) {
|
|
739
|
+
if (inCaseArgs(mockFromMatch.index)) continue;
|
|
740
|
+
const name = mockFromMatch[1];
|
|
741
|
+
if (!JS_IDENT_RE.test(name)) continue;
|
|
682
742
|
directives.push({
|
|
683
743
|
kind: "mock",
|
|
684
|
-
name
|
|
744
|
+
name,
|
|
685
745
|
fromPath: mockFromMatch[2]
|
|
686
746
|
});
|
|
687
747
|
mockFromRanges.push([mockFromMatch.index, mockFromMatch.index + mockFromMatch[0].length]);
|
|
@@ -692,7 +752,9 @@ function parseDirectivesFromComments(comments) {
|
|
|
692
752
|
const inFromRange = mockFromRanges.some(
|
|
693
753
|
([s, e]) => mockMatch.index >= s && mockMatch.index < e
|
|
694
754
|
);
|
|
695
|
-
if (inFromRange) continue;
|
|
755
|
+
if (inFromRange || inCaseArgs(mockMatch.index)) continue;
|
|
756
|
+
const mockName = mockMatch[1];
|
|
757
|
+
if (!JS_IDENT_RE.test(mockName)) continue;
|
|
696
758
|
const expr = mockMatch[2].trim();
|
|
697
759
|
let arrowFn = parseArrowFunctionExpr(expr);
|
|
698
760
|
const sinonExpr = parseSinonExpr(expr);
|
|
@@ -705,28 +767,20 @@ function parseDirectivesFromComments(comments) {
|
|
|
705
767
|
}
|
|
706
768
|
directives.push({
|
|
707
769
|
kind: "mock",
|
|
708
|
-
name:
|
|
770
|
+
name: mockName,
|
|
709
771
|
expression: expr,
|
|
710
772
|
arrowFn: arrowFn ?? void 0,
|
|
711
773
|
sinonExpr: sinonExpr ?? void 0,
|
|
712
774
|
nudoMock: nudoMock ?? void 0
|
|
713
775
|
});
|
|
714
776
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
const
|
|
721
|
-
const
|
|
722
|
-
if (argsStr === null) continue;
|
|
723
|
-
const cleaned = argsStr.split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n");
|
|
724
|
-
const argsAbs = splitTopLevelArgs(cleaned).map(parseCaseArgExpr);
|
|
725
|
-
const afterParen = parenStart + argsStr.length + 2;
|
|
726
|
-
const restLine = text.slice(afterParen).split("\n")[0].trim();
|
|
727
|
-
const arrowMatch = restLine.match(/^=>\s*(.+)/);
|
|
728
|
-
const expected = arrowMatch ? parseCaseArgExpr(arrowMatch[1].trim()) : void 0;
|
|
729
|
-
const linesBeforeMatch = text.slice(0, match.index).split("\n").length - 1;
|
|
777
|
+
for (const tag of caseTags) {
|
|
778
|
+
const name = tag.name;
|
|
779
|
+
const cleaned = tag.argsText.split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n");
|
|
780
|
+
const rawArgs = splitTopLevelArgs(cleaned);
|
|
781
|
+
const argsAbs = rawArgs.map((a) => parseAbsExprDiag(a, `@nudo:case "${name}" argument`));
|
|
782
|
+
const expected = tag.expectedText !== void 0 ? parseAbsExprDiag(tag.expectedText, `@nudo:case "${name}" => expected`) : void 0;
|
|
783
|
+
const linesBeforeMatch = text.slice(0, tag.tagOffset).split("\n").length - 1;
|
|
730
784
|
const commentLine = commentStartLine + linesBeforeMatch;
|
|
731
785
|
directives.push({
|
|
732
786
|
kind: "case",
|
|
@@ -737,66 +791,138 @@ function parseDirectivesFromComments(comments) {
|
|
|
737
791
|
});
|
|
738
792
|
}
|
|
739
793
|
PURE_REGEX.lastIndex = 0;
|
|
740
|
-
|
|
794
|
+
let pureMatch;
|
|
795
|
+
while ((pureMatch = PURE_REGEX.exec(text)) !== null) {
|
|
796
|
+
if (inCaseArgs(pureMatch.index)) continue;
|
|
741
797
|
directives.push({ kind: "pure" });
|
|
798
|
+
break;
|
|
742
799
|
}
|
|
743
800
|
SKIP_REGEX.lastIndex = 0;
|
|
744
801
|
let skipMatch;
|
|
745
802
|
while ((skipMatch = SKIP_REGEX.exec(text)) !== null) {
|
|
746
|
-
|
|
803
|
+
if (inCaseArgs(skipMatch.index)) continue;
|
|
804
|
+
const returns = parseSkipReturnsExpr(skipMatch[1]);
|
|
747
805
|
directives.push({
|
|
748
806
|
kind: "skip",
|
|
749
|
-
returns
|
|
807
|
+
returns
|
|
750
808
|
});
|
|
751
809
|
}
|
|
752
810
|
SAMPLE_REGEX.lastIndex = 0;
|
|
753
811
|
let sampleMatch;
|
|
754
812
|
while ((sampleMatch = SAMPLE_REGEX.exec(text)) !== null) {
|
|
755
|
-
|
|
813
|
+
if (inCaseArgs(sampleMatch.index)) continue;
|
|
814
|
+
const raw = sampleMatch[1];
|
|
815
|
+
if (raw === void 0 || !SAMPLE_NUM_RE.test(raw)) {
|
|
816
|
+
emitDirectiveDiag({
|
|
817
|
+
code: "nudo:directive-syntax",
|
|
818
|
+
message: `Malformed @nudo:sample: expected a numeric count (integer, decimal, or negative) \u2014 got: @nudo:sample ${raw ?? "(missing count)"}`
|
|
819
|
+
});
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
directives.push({ kind: "sample", count: Number(raw) });
|
|
756
823
|
}
|
|
757
824
|
}
|
|
758
825
|
return directives;
|
|
759
826
|
}
|
|
760
|
-
function
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
827
|
+
function scanMalformedCaseNames(text, caseArgSpans) {
|
|
828
|
+
const inCaseArgs = (idx) => caseArgSpans.some(([s, e]) => idx >= s && idx < e);
|
|
829
|
+
CASE_TAG_REGEX.lastIndex = 0;
|
|
830
|
+
let m;
|
|
831
|
+
while ((m = CASE_TAG_REGEX.exec(text)) !== null) {
|
|
832
|
+
if (inCaseArgs(m.index)) continue;
|
|
833
|
+
const rest = m[1].trim();
|
|
834
|
+
if (/^"[^"]+"\s*\(/.test(rest)) continue;
|
|
835
|
+
if (/^"[^"]+"\s*\(/.test(rest)) continue;
|
|
836
|
+
let reason;
|
|
837
|
+
if (rest.startsWith("'")) {
|
|
838
|
+
reason = `single-quoted name (use double quotes: @nudo:case "name" (\u2026))`;
|
|
839
|
+
} else if (rest.startsWith('""')) {
|
|
840
|
+
reason = `empty name (name must be non-empty)`;
|
|
841
|
+
} else if (/^"[^"]*\\/.test(rest)) {
|
|
842
|
+
reason = `escaped quote in name (avoid embedded double quotes)`;
|
|
843
|
+
} else if (rest.startsWith('"')) {
|
|
844
|
+
reason = `malformed name or missing argument list (expected @nudo:case "name" (\u2026))`;
|
|
845
|
+
} else {
|
|
846
|
+
reason = `missing quoted name (expected @nudo:case "name" (\u2026))`;
|
|
847
|
+
}
|
|
848
|
+
emitDirectiveDiag({
|
|
849
|
+
code: "nudo:directive-syntax",
|
|
850
|
+
message: `Malformed @nudo:case: ${reason} \u2014 got: @nudo:case ${rest.slice(0, 60)}`
|
|
851
|
+
});
|
|
767
852
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
853
|
+
}
|
|
854
|
+
function scanMalformedMockNames(text, caseArgSpans) {
|
|
855
|
+
const inCaseArgs = (idx) => caseArgSpans.some(([s, e]) => idx >= s && idx < e);
|
|
856
|
+
MOCK_TAG_REGEX.lastIndex = 0;
|
|
857
|
+
let m;
|
|
858
|
+
while ((m = MOCK_TAG_REGEX.exec(text)) !== null) {
|
|
859
|
+
if (inCaseArgs(m.index)) continue;
|
|
860
|
+
const rest = m[1].trim();
|
|
861
|
+
const nameMatch = rest.match(/^([^\s=]+)(?:\s*=|\s+from\b)/);
|
|
862
|
+
if (!nameMatch) {
|
|
863
|
+
emitDirectiveDiag({
|
|
864
|
+
code: "nudo:directive-syntax",
|
|
865
|
+
message: `Malformed @nudo:mock: cannot parse name (expected @nudo:mock <ident> = <expr> or @nudo:mock <ident> from "path") \u2014 got: ${rest.slice(0, 60)}`
|
|
866
|
+
});
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const name = nameMatch[1];
|
|
870
|
+
if (!JS_IDENT_RE.test(name)) {
|
|
871
|
+
emitDirectiveDiag({
|
|
872
|
+
code: "nudo:directive-syntax",
|
|
873
|
+
message: `Malformed @nudo:mock name '${name}': not a valid identifier (use letters, digits, _, $; Unicode letters allowed)`
|
|
874
|
+
});
|
|
875
|
+
}
|
|
771
876
|
}
|
|
772
|
-
return "<anonymous>";
|
|
773
877
|
}
|
|
774
878
|
function extractDirectives(ast) {
|
|
879
|
+
resetDirectiveDiagSeen();
|
|
775
880
|
const results = [];
|
|
776
881
|
if (ast.type !== "File") return results;
|
|
777
|
-
const
|
|
778
|
-
for (const
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
882
|
+
const scopes = listFnDirectiveScopes(ast);
|
|
883
|
+
for (const scope of scopes) {
|
|
884
|
+
const directives = parseDirectivesFromCommentTexts(
|
|
885
|
+
scope.commentTexts.map((text, i) => ({
|
|
886
|
+
text,
|
|
887
|
+
startLine: scope.commentStartLines[i] ?? 0
|
|
888
|
+
}))
|
|
889
|
+
);
|
|
782
890
|
if (directives.length === 0) continue;
|
|
783
|
-
|
|
784
|
-
results.push({ node: stmt, name, directives });
|
|
891
|
+
results.push({ node: scope.node, name: scope.name, directives });
|
|
785
892
|
}
|
|
786
893
|
return results;
|
|
787
894
|
}
|
|
895
|
+
function extractDirectivesQuiet(ast) {
|
|
896
|
+
const since = directiveDiagCount();
|
|
897
|
+
const out = extractDirectives(ast);
|
|
898
|
+
takeDirectiveDiagsSince(since);
|
|
899
|
+
return out;
|
|
900
|
+
}
|
|
788
901
|
var AS_REGEX = /^\s*@nudo:as\s+(.+)/;
|
|
789
902
|
var REPLACE_REGEX = /^\s*@nudo:replace\s+(.+)/;
|
|
903
|
+
var AS_TRAILING_COMMENT_RE = /\s+(?:\/\/|\/\*)/;
|
|
790
904
|
function extractInlineDirectives(node) {
|
|
905
|
+
resetDirectiveDiagSeen();
|
|
791
906
|
const comments = node.leadingComments;
|
|
792
907
|
if (!comments) return [];
|
|
793
908
|
const results = [];
|
|
794
909
|
for (const comment of comments) {
|
|
795
|
-
if (comment.type !== "CommentLine") continue;
|
|
910
|
+
if (comment.type !== "CommentLine" && comment.type !== "CommentBlock") continue;
|
|
796
911
|
const text = comment.value;
|
|
797
912
|
const asMatch = text.match(AS_REGEX);
|
|
798
913
|
if (asMatch) {
|
|
799
|
-
|
|
914
|
+
const rawExpr = asMatch[1].trim();
|
|
915
|
+
const trailing = rawExpr.match(AS_TRAILING_COMMENT_RE);
|
|
916
|
+
if (trailing) {
|
|
917
|
+
const typePart = rawExpr.slice(0, trailing.index).trim();
|
|
918
|
+
emitDirectiveDiag({
|
|
919
|
+
code: "nudo:directive-syntax",
|
|
920
|
+
message: `Trailing comment in @nudo:as type expression: '${rawExpr}' (remove the comment; the type must be a clean expression)`
|
|
921
|
+
});
|
|
922
|
+
results.push({ kind: "as", typeAbs: typePart ? parseAbsExprDiag(typePart, "@nudo:as") : absUnknown() });
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
results.push({ kind: "as", typeAbs: parseAbsExprDiag(rawExpr, "@nudo:as") });
|
|
800
926
|
continue;
|
|
801
927
|
}
|
|
802
928
|
const replaceMatch = text.match(REPLACE_REGEX);
|
|
@@ -809,7 +935,12 @@ function extractInlineDirectives(node) {
|
|
|
809
935
|
results.push({
|
|
810
936
|
kind: "replace",
|
|
811
937
|
targetSource,
|
|
812
|
-
typeAbs:
|
|
938
|
+
typeAbs: parseAbsExprDiag(typeExprStr, "@nudo:replace")
|
|
939
|
+
});
|
|
940
|
+
} else {
|
|
941
|
+
emitDirectiveDiag({
|
|
942
|
+
code: "nudo:directive-syntax",
|
|
943
|
+
message: `Malformed @nudo:replace: cannot separate target from type (expected @nudo:replace <target> <typeExpr>) \u2014 got: ${raw.slice(0, 60)}`
|
|
813
944
|
});
|
|
814
945
|
}
|
|
815
946
|
}
|
|
@@ -822,7 +953,7 @@ function findReplaceSeparator(raw) {
|
|
|
822
953
|
for (let i = 0; i < raw.length; i++) {
|
|
823
954
|
const ch = raw[i];
|
|
824
955
|
if (inString) {
|
|
825
|
-
if (ch === inString
|
|
956
|
+
if (ch === inString) inString = null;
|
|
826
957
|
continue;
|
|
827
958
|
}
|
|
828
959
|
if (ch === '"' || ch === "'") {
|
|
@@ -846,9 +977,8 @@ function findReplaceSeparator(raw) {
|
|
|
846
977
|
}
|
|
847
978
|
return -1;
|
|
848
979
|
}
|
|
849
|
-
var ENV_REGEX =
|
|
850
|
-
var MOCK_MODULE_REGEX =
|
|
851
|
-
var MOCK_MODULE_PARTIAL_REGEX = /^\/\s*@nudo:mock-module\s+"([^"]+)"\s*\{([^}]+)\}\s*from\s+"([^"]+)"/;
|
|
980
|
+
var ENV_REGEX = /^\/?\s*@nudo:env\s+(.+)/;
|
|
981
|
+
var MOCK_MODULE_REGEX = /^\/?\s*@nudo:mock-module\s+(.+)/;
|
|
852
982
|
function extractFileDirectives(ast) {
|
|
853
983
|
const results = [];
|
|
854
984
|
if (ast.type !== "File") return results;
|
|
@@ -856,34 +986,44 @@ function extractFileDirectives(ast) {
|
|
|
856
986
|
for (const comment of comments) {
|
|
857
987
|
if (comment.type !== "CommentLine") continue;
|
|
858
988
|
const text = comment.value;
|
|
859
|
-
const partialMatch = text.match(MOCK_MODULE_PARTIAL_REGEX);
|
|
860
|
-
if (partialMatch) {
|
|
861
|
-
const names = partialMatch[2].split(",").map((n) => n.trim()).filter(Boolean);
|
|
862
|
-
results.push({
|
|
863
|
-
kind: "mock-module",
|
|
864
|
-
source: partialMatch[1],
|
|
865
|
-
names,
|
|
866
|
-
fromPath: partialMatch[3]
|
|
867
|
-
});
|
|
868
|
-
continue;
|
|
869
|
-
}
|
|
870
989
|
const mockModuleMatch = text.match(MOCK_MODULE_REGEX);
|
|
871
990
|
if (mockModuleMatch) {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
991
|
+
const rec = parseMockModulePayload(mockModuleMatch[1]);
|
|
992
|
+
if (rec) {
|
|
993
|
+
results.push({
|
|
994
|
+
kind: "mock-module",
|
|
995
|
+
source: rec.source,
|
|
996
|
+
...rec.names ? { names: rec.names } : {},
|
|
997
|
+
fromPath: rec.fromPath
|
|
998
|
+
});
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
878
1001
|
}
|
|
879
1002
|
const envMatch = text.match(ENV_REGEX);
|
|
880
1003
|
if (envMatch) {
|
|
881
|
-
const envs = envMatch[1]
|
|
882
|
-
results.push({ kind: "env", envs });
|
|
1004
|
+
const envs = parseEnvPayload(envMatch[1]);
|
|
1005
|
+
if (envs.length > 0) results.push({ kind: "env", envs });
|
|
883
1006
|
}
|
|
884
1007
|
}
|
|
885
1008
|
return results;
|
|
886
1009
|
}
|
|
1010
|
+
|
|
1011
|
+
// src/index.ts
|
|
1012
|
+
import {
|
|
1013
|
+
extractFileEnvNames,
|
|
1014
|
+
extractMockModuleRecords,
|
|
1015
|
+
extractNudoImportRecords,
|
|
1016
|
+
findFnDirectiveScope,
|
|
1017
|
+
fnDirectiveCommentLines,
|
|
1018
|
+
listFnDirectiveScopes as listFnDirectiveScopes2,
|
|
1019
|
+
parseEnvPayload as parseEnvPayload2,
|
|
1020
|
+
parseMockModulePayload as parseMockModulePayload2,
|
|
1021
|
+
parseNudoImportPayload,
|
|
1022
|
+
scanBudgetDecl,
|
|
1023
|
+
scanCaseTags as scanCaseTags2,
|
|
1024
|
+
scanContractSegments,
|
|
1025
|
+
scanThrowsDecl
|
|
1026
|
+
} from "@nudojs/core";
|
|
887
1027
|
export {
|
|
888
1028
|
asAssignmentExpression,
|
|
889
1029
|
asExpression,
|
|
@@ -894,11 +1034,18 @@ export {
|
|
|
894
1034
|
classInstanceMethods,
|
|
895
1035
|
classMemberKey,
|
|
896
1036
|
classMemberKeyName,
|
|
1037
|
+
directiveDiagCount,
|
|
897
1038
|
exportSpecifierExportedName,
|
|
898
1039
|
exportSpecifierLocalName,
|
|
899
1040
|
extractDirectives,
|
|
1041
|
+
extractDirectivesQuiet,
|
|
900
1042
|
extractFileDirectives,
|
|
1043
|
+
extractFileEnvNames,
|
|
901
1044
|
extractInlineDirectives,
|
|
1045
|
+
extractMockModuleRecords,
|
|
1046
|
+
extractNudoImportRecords,
|
|
1047
|
+
findFnDirectiveScope,
|
|
1048
|
+
fnDirectiveCommentLines,
|
|
902
1049
|
fnOrClassIdLoc,
|
|
903
1050
|
fnOrClassIdName,
|
|
904
1051
|
getClassMembers,
|
|
@@ -911,6 +1058,7 @@ export {
|
|
|
911
1058
|
getFirstDeclaratorId,
|
|
912
1059
|
identifierName,
|
|
913
1060
|
isModuleExportsMember,
|
|
1061
|
+
listFnDirectiveScopes2 as listFnDirectiveScopes,
|
|
914
1062
|
memberObjectName,
|
|
915
1063
|
memberPropertyKey,
|
|
916
1064
|
memberPropertyName,
|
|
@@ -920,8 +1068,18 @@ export {
|
|
|
920
1068
|
paramName,
|
|
921
1069
|
parse,
|
|
922
1070
|
parseCaseArgExpr,
|
|
1071
|
+
parseEnvPayload2 as parseEnvPayload,
|
|
1072
|
+
parseMockModulePayload2 as parseMockModulePayload,
|
|
1073
|
+
parseNudoImportPayload,
|
|
923
1074
|
programBody,
|
|
1075
|
+
scanBudgetDecl,
|
|
1076
|
+
scanCaseTags2 as scanCaseTags,
|
|
1077
|
+
scanContractSegments,
|
|
1078
|
+
scanThrowsDecl,
|
|
1079
|
+
setDirectiveDiagCollector,
|
|
924
1080
|
stripTypes,
|
|
1081
|
+
takeDirectiveDiags,
|
|
1082
|
+
takeDirectiveDiagsSince,
|
|
925
1083
|
unwrapDefaultExport,
|
|
926
1084
|
unwrapExport
|
|
927
1085
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nudojs/parser",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=20"
|
|
6
6
|
},
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"registry": "https://registry.npmjs.org"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@nudojs/core": "1.
|
|
42
|
+
"@nudojs/core": "1.4.0",
|
|
43
43
|
"@babel/types": "^8.0.6"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|