@nudojs/parser 1.1.8 → 1.2.1
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 +266 -95
- 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
|
|
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;
|
|
216
266
|
var PURE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:pure\b/g;
|
|
217
267
|
var SKIP_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:skip\b(?:[ \t]+(\S[^\n]*))?/g;
|
|
218
|
-
var SAMPLE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:sample[ \t]+(\
|
|
219
|
-
var
|
|
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;
|
|
@@ -438,26 +488,6 @@ function findTopLevelArrow(s) {
|
|
|
438
488
|
}
|
|
439
489
|
return -1;
|
|
440
490
|
}
|
|
441
|
-
function extractBalancedParens(text, startIdx) {
|
|
442
|
-
if (text[startIdx] !== "(") return null;
|
|
443
|
-
let depth = 0;
|
|
444
|
-
let inString = null;
|
|
445
|
-
for (let i = startIdx; i < text.length; i++) {
|
|
446
|
-
const ch = text[i];
|
|
447
|
-
if (inString) {
|
|
448
|
-
if (ch === inString) inString = null;
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
if (ch === '"' || ch === "'") {
|
|
452
|
-
inString = ch;
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
if (ch === "(") depth++;
|
|
456
|
-
if (ch === ")") depth--;
|
|
457
|
-
if (depth === 0) return text.slice(startIdx + 1, i);
|
|
458
|
-
}
|
|
459
|
-
return null;
|
|
460
|
-
}
|
|
461
491
|
function parseArrowFunctionExpr(expr) {
|
|
462
492
|
try {
|
|
463
493
|
const wrapped = `const __mock = ${expr};`;
|
|
@@ -488,9 +518,46 @@ function parseArrowFunctionExpr(expr) {
|
|
|
488
518
|
return null;
|
|
489
519
|
}
|
|
490
520
|
}
|
|
521
|
+
function isExplicitUnknownExpr(expr) {
|
|
522
|
+
const s = expr.trim();
|
|
523
|
+
return s === "unknown" || s === "any" || s === "any()";
|
|
524
|
+
}
|
|
491
525
|
function parseAbsExpr(expr) {
|
|
492
526
|
return parseCaseArgExpr(expr);
|
|
493
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
|
+
}
|
|
494
561
|
function parseSinonExpr(expr) {
|
|
495
562
|
const s = expr.trim();
|
|
496
563
|
const stubReturnsMatch = s.match(/^sinon\.stub\(\)\.returns\((.+)\)$/);
|
|
@@ -657,17 +724,24 @@ function parseNudoMockExpr(expr) {
|
|
|
657
724
|
}
|
|
658
725
|
return null;
|
|
659
726
|
}
|
|
660
|
-
function
|
|
727
|
+
function parseDirectivesFromCommentTexts(comments) {
|
|
661
728
|
const directives = [];
|
|
662
|
-
for (const
|
|
663
|
-
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);
|
|
664
735
|
MOCK_FROM_REGEX.lastIndex = 0;
|
|
665
736
|
let mockFromMatch;
|
|
666
737
|
const mockFromRanges = [];
|
|
667
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;
|
|
668
742
|
directives.push({
|
|
669
743
|
kind: "mock",
|
|
670
|
-
name
|
|
744
|
+
name,
|
|
671
745
|
fromPath: mockFromMatch[2]
|
|
672
746
|
});
|
|
673
747
|
mockFromRanges.push([mockFromMatch.index, mockFromMatch.index + mockFromMatch[0].length]);
|
|
@@ -678,7 +752,9 @@ function parseDirectivesFromComments(comments) {
|
|
|
678
752
|
const inFromRange = mockFromRanges.some(
|
|
679
753
|
([s, e]) => mockMatch.index >= s && mockMatch.index < e
|
|
680
754
|
);
|
|
681
|
-
if (inFromRange) continue;
|
|
755
|
+
if (inFromRange || inCaseArgs(mockMatch.index)) continue;
|
|
756
|
+
const mockName = mockMatch[1];
|
|
757
|
+
if (!JS_IDENT_RE.test(mockName)) continue;
|
|
682
758
|
const expr = mockMatch[2].trim();
|
|
683
759
|
let arrowFn = parseArrowFunctionExpr(expr);
|
|
684
760
|
const sinonExpr = parseSinonExpr(expr);
|
|
@@ -691,29 +767,20 @@ function parseDirectivesFromComments(comments) {
|
|
|
691
767
|
}
|
|
692
768
|
directives.push({
|
|
693
769
|
kind: "mock",
|
|
694
|
-
name:
|
|
770
|
+
name: mockName,
|
|
695
771
|
expression: expr,
|
|
696
772
|
arrowFn: arrowFn ?? void 0,
|
|
697
773
|
sinonExpr: sinonExpr ?? void 0,
|
|
698
774
|
nudoMock: nudoMock ?? void 0
|
|
699
775
|
});
|
|
700
776
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const
|
|
706
|
-
const
|
|
707
|
-
const
|
|
708
|
-
if (argsStr === null) continue;
|
|
709
|
-
const cleaned = argsStr.split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n");
|
|
710
|
-
const argsAbs = splitTopLevelArgs(cleaned).map(parseCaseArgExpr);
|
|
711
|
-
const afterParen = parenStart + argsStr.length + 2;
|
|
712
|
-
const restLine = text.slice(afterParen).split("\n")[0].trim();
|
|
713
|
-
const arrowMatch = restLine.match(/^=>\s*(.+)/);
|
|
714
|
-
const expected = arrowMatch ? parseCaseArgExpr(arrowMatch[1].trim()) : void 0;
|
|
715
|
-
const tagOffset = match.index + match[0].indexOf("@nudo:case");
|
|
716
|
-
const linesBeforeMatch = text.slice(0, tagOffset).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;
|
|
717
784
|
const commentLine = commentStartLine + linesBeforeMatch;
|
|
718
785
|
directives.push({
|
|
719
786
|
kind: "case",
|
|
@@ -724,66 +791,138 @@ function parseDirectivesFromComments(comments) {
|
|
|
724
791
|
});
|
|
725
792
|
}
|
|
726
793
|
PURE_REGEX.lastIndex = 0;
|
|
727
|
-
|
|
794
|
+
let pureMatch;
|
|
795
|
+
while ((pureMatch = PURE_REGEX.exec(text)) !== null) {
|
|
796
|
+
if (inCaseArgs(pureMatch.index)) continue;
|
|
728
797
|
directives.push({ kind: "pure" });
|
|
798
|
+
break;
|
|
729
799
|
}
|
|
730
800
|
SKIP_REGEX.lastIndex = 0;
|
|
731
801
|
let skipMatch;
|
|
732
802
|
while ((skipMatch = SKIP_REGEX.exec(text)) !== null) {
|
|
733
|
-
|
|
803
|
+
if (inCaseArgs(skipMatch.index)) continue;
|
|
804
|
+
const returns = parseSkipReturnsExpr(skipMatch[1]);
|
|
734
805
|
directives.push({
|
|
735
806
|
kind: "skip",
|
|
736
|
-
returns
|
|
807
|
+
returns
|
|
737
808
|
});
|
|
738
809
|
}
|
|
739
810
|
SAMPLE_REGEX.lastIndex = 0;
|
|
740
811
|
let sampleMatch;
|
|
741
812
|
while ((sampleMatch = SAMPLE_REGEX.exec(text)) !== null) {
|
|
742
|
-
|
|
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) });
|
|
743
823
|
}
|
|
744
824
|
}
|
|
745
825
|
return directives;
|
|
746
826
|
}
|
|
747
|
-
function
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
+
});
|
|
754
852
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
+
}
|
|
758
876
|
}
|
|
759
|
-
return "<anonymous>";
|
|
760
877
|
}
|
|
761
878
|
function extractDirectives(ast) {
|
|
879
|
+
resetDirectiveDiagSeen();
|
|
762
880
|
const results = [];
|
|
763
881
|
if (ast.type !== "File") return results;
|
|
764
|
-
const
|
|
765
|
-
for (const
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
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
|
+
);
|
|
769
890
|
if (directives.length === 0) continue;
|
|
770
|
-
|
|
771
|
-
results.push({ node: stmt, name, directives });
|
|
891
|
+
results.push({ node: scope.node, name: scope.name, directives });
|
|
772
892
|
}
|
|
773
893
|
return results;
|
|
774
894
|
}
|
|
895
|
+
function extractDirectivesQuiet(ast) {
|
|
896
|
+
const since = directiveDiagCount();
|
|
897
|
+
const out = extractDirectives(ast);
|
|
898
|
+
takeDirectiveDiagsSince(since);
|
|
899
|
+
return out;
|
|
900
|
+
}
|
|
775
901
|
var AS_REGEX = /^\s*@nudo:as\s+(.+)/;
|
|
776
902
|
var REPLACE_REGEX = /^\s*@nudo:replace\s+(.+)/;
|
|
903
|
+
var AS_TRAILING_COMMENT_RE = /\s+(?:\/\/|\/\*)/;
|
|
777
904
|
function extractInlineDirectives(node) {
|
|
905
|
+
resetDirectiveDiagSeen();
|
|
778
906
|
const comments = node.leadingComments;
|
|
779
907
|
if (!comments) return [];
|
|
780
908
|
const results = [];
|
|
781
909
|
for (const comment of comments) {
|
|
782
|
-
if (comment.type !== "CommentLine") continue;
|
|
910
|
+
if (comment.type !== "CommentLine" && comment.type !== "CommentBlock") continue;
|
|
783
911
|
const text = comment.value;
|
|
784
912
|
const asMatch = text.match(AS_REGEX);
|
|
785
913
|
if (asMatch) {
|
|
786
|
-
|
|
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") });
|
|
787
926
|
continue;
|
|
788
927
|
}
|
|
789
928
|
const replaceMatch = text.match(REPLACE_REGEX);
|
|
@@ -796,7 +935,12 @@ function extractInlineDirectives(node) {
|
|
|
796
935
|
results.push({
|
|
797
936
|
kind: "replace",
|
|
798
937
|
targetSource,
|
|
799
|
-
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)}`
|
|
800
944
|
});
|
|
801
945
|
}
|
|
802
946
|
}
|
|
@@ -833,9 +977,8 @@ function findReplaceSeparator(raw) {
|
|
|
833
977
|
}
|
|
834
978
|
return -1;
|
|
835
979
|
}
|
|
836
|
-
var ENV_REGEX =
|
|
837
|
-
var MOCK_MODULE_REGEX =
|
|
838
|
-
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+(.+)/;
|
|
839
982
|
function extractFileDirectives(ast) {
|
|
840
983
|
const results = [];
|
|
841
984
|
if (ast.type !== "File") return results;
|
|
@@ -843,34 +986,44 @@ function extractFileDirectives(ast) {
|
|
|
843
986
|
for (const comment of comments) {
|
|
844
987
|
if (comment.type !== "CommentLine") continue;
|
|
845
988
|
const text = comment.value;
|
|
846
|
-
const partialMatch = text.match(MOCK_MODULE_PARTIAL_REGEX);
|
|
847
|
-
if (partialMatch) {
|
|
848
|
-
const names = partialMatch[2].split(",").map((n) => n.trim()).filter(Boolean);
|
|
849
|
-
results.push({
|
|
850
|
-
kind: "mock-module",
|
|
851
|
-
source: partialMatch[1],
|
|
852
|
-
names,
|
|
853
|
-
fromPath: partialMatch[3]
|
|
854
|
-
});
|
|
855
|
-
continue;
|
|
856
|
-
}
|
|
857
989
|
const mockModuleMatch = text.match(MOCK_MODULE_REGEX);
|
|
858
990
|
if (mockModuleMatch) {
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
+
}
|
|
865
1001
|
}
|
|
866
1002
|
const envMatch = text.match(ENV_REGEX);
|
|
867
1003
|
if (envMatch) {
|
|
868
|
-
const envs = envMatch[1]
|
|
869
|
-
results.push({ kind: "env", envs });
|
|
1004
|
+
const envs = parseEnvPayload(envMatch[1]);
|
|
1005
|
+
if (envs.length > 0) results.push({ kind: "env", envs });
|
|
870
1006
|
}
|
|
871
1007
|
}
|
|
872
1008
|
return results;
|
|
873
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";
|
|
874
1027
|
export {
|
|
875
1028
|
asAssignmentExpression,
|
|
876
1029
|
asExpression,
|
|
@@ -881,11 +1034,18 @@ export {
|
|
|
881
1034
|
classInstanceMethods,
|
|
882
1035
|
classMemberKey,
|
|
883
1036
|
classMemberKeyName,
|
|
1037
|
+
directiveDiagCount,
|
|
884
1038
|
exportSpecifierExportedName,
|
|
885
1039
|
exportSpecifierLocalName,
|
|
886
1040
|
extractDirectives,
|
|
1041
|
+
extractDirectivesQuiet,
|
|
887
1042
|
extractFileDirectives,
|
|
1043
|
+
extractFileEnvNames,
|
|
888
1044
|
extractInlineDirectives,
|
|
1045
|
+
extractMockModuleRecords,
|
|
1046
|
+
extractNudoImportRecords,
|
|
1047
|
+
findFnDirectiveScope,
|
|
1048
|
+
fnDirectiveCommentLines,
|
|
889
1049
|
fnOrClassIdLoc,
|
|
890
1050
|
fnOrClassIdName,
|
|
891
1051
|
getClassMembers,
|
|
@@ -898,6 +1058,7 @@ export {
|
|
|
898
1058
|
getFirstDeclaratorId,
|
|
899
1059
|
identifierName,
|
|
900
1060
|
isModuleExportsMember,
|
|
1061
|
+
listFnDirectiveScopes2 as listFnDirectiveScopes,
|
|
901
1062
|
memberObjectName,
|
|
902
1063
|
memberPropertyKey,
|
|
903
1064
|
memberPropertyName,
|
|
@@ -907,8 +1068,18 @@ export {
|
|
|
907
1068
|
paramName,
|
|
908
1069
|
parse,
|
|
909
1070
|
parseCaseArgExpr,
|
|
1071
|
+
parseEnvPayload2 as parseEnvPayload,
|
|
1072
|
+
parseMockModulePayload2 as parseMockModulePayload,
|
|
1073
|
+
parseNudoImportPayload,
|
|
910
1074
|
programBody,
|
|
1075
|
+
scanBudgetDecl,
|
|
1076
|
+
scanCaseTags2 as scanCaseTags,
|
|
1077
|
+
scanContractSegments,
|
|
1078
|
+
scanThrowsDecl,
|
|
1079
|
+
setDirectiveDiagCollector,
|
|
911
1080
|
stripTypes,
|
|
1081
|
+
takeDirectiveDiags,
|
|
1082
|
+
takeDirectiveDiagsSince,
|
|
912
1083
|
unwrapDefaultExport,
|
|
913
1084
|
unwrapExport
|
|
914
1085
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nudojs/parser",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
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.5.0",
|
|
43
43
|
"@babel/types": "^8.0.6"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|