@nudojs/parser 1.4.23 → 1.5.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 +20 -7
- package/dist/index.js +252 -225
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -170,6 +170,7 @@ type FunctionWithDirectives = {
|
|
|
170
170
|
name: string;
|
|
171
171
|
directives: Directive[];
|
|
172
172
|
};
|
|
173
|
+
|
|
173
174
|
type DirectiveDiag = {
|
|
174
175
|
code: string;
|
|
175
176
|
message: string;
|
|
@@ -181,12 +182,6 @@ type DirectiveDiag = {
|
|
|
181
182
|
* 的 D1 段)。fn 内显式传 diags 的 extract 自成新去重域。
|
|
182
183
|
*/
|
|
183
184
|
declare function runWithDirectiveDiags<T>(diags: DirectiveDiag[], fn: () => T): T;
|
|
184
|
-
/**
|
|
185
|
-
* case 实参 / 指令类型表达式唯一文法:约束构建器优先,其余为具体字面量、
|
|
186
|
-
* 结构字面量与箭头函数。`T.*` 文法已物理删除。
|
|
187
|
-
* depth 按包含关系逐层 +1 线程化传递(兄弟共享父深度,不用共享可变计数器)。
|
|
188
|
-
*/
|
|
189
|
-
declare function parseCaseArgExpr(expr: string, depth?: number): Abs;
|
|
190
185
|
/**
|
|
191
186
|
* extractDirectives 显式诊断通道 opts。
|
|
192
187
|
*
|
|
@@ -198,6 +193,14 @@ declare function parseCaseArgExpr(expr: string, depth?: number): Abs;
|
|
|
198
193
|
type ExtractDirectivesOpts = {
|
|
199
194
|
diags?: DirectiveDiag[];
|
|
200
195
|
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* case 实参 / 指令类型表达式唯一文法:约束构建器优先,其余为具体字面量、
|
|
199
|
+
* 结构字面量与箭头函数。`T.*` 文法已物理删除。
|
|
200
|
+
* depth 按包含关系逐层 +1 线程化传递(兄弟共享父深度,不用共享可变计数器)。
|
|
201
|
+
*/
|
|
202
|
+
declare function parseCaseArgExpr(expr: string, depth?: number): Abs;
|
|
203
|
+
|
|
201
204
|
/**
|
|
202
205
|
* D6=G2:指令绑定 AST 最近 Function(含 nested function、class method)。
|
|
203
206
|
* 作用域清单与 refine 的 @nudo:contract 同源(core directive-scan),
|
|
@@ -210,6 +213,7 @@ declare function extractDirectives(ast: Node, opts?: ExtractDirectivesOpts): Fun
|
|
|
210
213
|
* 需要诊断的调用方用 extractDirectives(ast, { diags })。
|
|
211
214
|
*/
|
|
212
215
|
declare function extractDirectivesQuiet(ast: Node): FunctionWithDirectives[];
|
|
216
|
+
|
|
213
217
|
/**
|
|
214
218
|
* 行内指令(@nudo:as / @nudo:replace)抽取,源为节点的 leadingComments。
|
|
215
219
|
* 诊断通道与 extractDirectives 同形:传 opts.diags 落调用方数组(单次调用
|
|
@@ -217,6 +221,15 @@ declare function extractDirectivesQuiet(ast: Node): FunctionWithDirectives[];
|
|
|
217
221
|
* runWithDirectiveDiags 域(无则丢弃,纯查询形态)。
|
|
218
222
|
*/
|
|
219
223
|
declare function extractInlineDirectives(node: Node, opts?: ExtractDirectivesOpts): InlineDirective[];
|
|
220
|
-
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 从 AST 抽文件级指令(`@nudo:env` / `@nudo:mock-module`,只认行注释)。
|
|
227
|
+
*
|
|
228
|
+
* `diags`:调用方自备的诊断累积数组——JSDoc/块注释内错位的文件级指令族
|
|
229
|
+
* 标签发 nudo:directive-syntax 警告(单次调用内同文案去重)。不传时为
|
|
230
|
+
* 纯查询形态:诊断丢弃;若外层有 runWithDirectiveDiags 活动域,则落入
|
|
231
|
+
* 该域并与其共享去重。
|
|
232
|
+
*/
|
|
233
|
+
declare function extractFileDirectives(ast: Node, opts?: ExtractDirectivesOpts): FileDirective[];
|
|
221
234
|
|
|
222
235
|
export { type AsDirective, type CaseDirective, type Directive, type DirectiveDiag, type EnvDirective, type ExtractDirectivesOpts, 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, 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, runWithDirectiveDiags, unwrapDefaultExport, unwrapExport };
|
package/dist/index.js
CHANGED
|
@@ -175,37 +175,7 @@ function unwrapDefaultExport(mod) {
|
|
|
175
175
|
return mod.default;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
// src/directives.ts
|
|
179
|
-
import {
|
|
180
|
-
stub,
|
|
181
|
-
spy,
|
|
182
|
-
mock,
|
|
183
|
-
abs as makeAbs,
|
|
184
|
-
numLit,
|
|
185
|
-
strLit,
|
|
186
|
-
boolLit,
|
|
187
|
-
absFunction,
|
|
188
|
-
joinAbs,
|
|
189
|
-
execNudoModule,
|
|
190
|
-
isNudoConstraint,
|
|
191
|
-
constraintToEntryAbs,
|
|
192
|
-
CONSTRAINT_BUILDER_NAMES,
|
|
193
|
-
CONSTRAINT_EXPR_RE,
|
|
194
|
-
SELF,
|
|
195
|
-
listFnDirectiveScopes,
|
|
196
|
-
parseEnvPayload,
|
|
197
|
-
parseMockModulePayload,
|
|
198
|
-
scanCaseArgSpans,
|
|
199
|
-
scanCaseTags,
|
|
200
|
-
extractBalancedParens
|
|
201
|
-
} from "@nudojs/core";
|
|
202
|
-
import { absLit } from "@nudojs/core/internal";
|
|
203
|
-
function absExact(shape) {
|
|
204
|
-
return makeAbs(shape, void 0, void 0, "exact");
|
|
205
|
-
}
|
|
206
|
-
function absUnknown() {
|
|
207
|
-
return makeAbs({ k: "unknown" }, void 0, void 0, "partial");
|
|
208
|
-
}
|
|
178
|
+
// src/directives/diag.ts
|
|
209
179
|
var activeDiagSink = null;
|
|
210
180
|
function runWithDirectiveDiags(diags, fn) {
|
|
211
181
|
const prev = activeDiagSink;
|
|
@@ -223,15 +193,158 @@ function emitDirectiveDiag(d) {
|
|
|
223
193
|
activeDiagSink.seen.add(key);
|
|
224
194
|
activeDiagSink.list.push(d);
|
|
225
195
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
196
|
+
|
|
197
|
+
// src/directives/case-arg.ts
|
|
198
|
+
import {
|
|
199
|
+
abs as makeAbs,
|
|
200
|
+
absFunction,
|
|
201
|
+
joinAbs,
|
|
202
|
+
numLit,
|
|
203
|
+
strLit,
|
|
204
|
+
boolLit,
|
|
205
|
+
execNudoModule,
|
|
206
|
+
isNudoConstraint,
|
|
207
|
+
constraintToEntryAbs,
|
|
208
|
+
CONSTRAINT_BUILDER_NAMES,
|
|
209
|
+
CONSTRAINT_EXPR_RE,
|
|
210
|
+
SELF
|
|
211
|
+
} from "@nudojs/core";
|
|
212
|
+
import { absLit, DIAG } from "@nudojs/core/internal";
|
|
213
|
+
|
|
214
|
+
// src/directives/expr-scan.ts
|
|
215
|
+
function scanWithStrings(s, onChar, onStructural) {
|
|
216
|
+
let depth = 0;
|
|
217
|
+
let inString = null;
|
|
218
|
+
for (let i = 0; i < s.length; i++) {
|
|
219
|
+
const ch = s[i];
|
|
220
|
+
if (inString) {
|
|
221
|
+
onChar(ch, i);
|
|
222
|
+
if (ch === inString) inString = null;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (ch === '"' || ch === "'") {
|
|
226
|
+
inString = ch;
|
|
227
|
+
onChar(ch, i);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
231
|
+
depth++;
|
|
232
|
+
onChar(ch, i);
|
|
233
|
+
onStructural(ch, i, depth);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (ch === ")" || ch === "]" || ch === "}") {
|
|
237
|
+
depth--;
|
|
238
|
+
onChar(ch, i);
|
|
239
|
+
onStructural(ch, i, depth);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
onChar(ch, i);
|
|
243
|
+
onStructural(ch, i, depth);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function splitTopLevelArgs(s) {
|
|
247
|
+
const result = [];
|
|
248
|
+
let current = "";
|
|
249
|
+
let depth = 0;
|
|
250
|
+
let inString = null;
|
|
251
|
+
for (let i = 0; i < s.length; i++) {
|
|
252
|
+
const ch = s[i];
|
|
253
|
+
if (inString) {
|
|
254
|
+
current += ch;
|
|
255
|
+
if (ch === inString) inString = null;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (ch === '"' || ch === "'") {
|
|
259
|
+
inString = ch;
|
|
260
|
+
current += ch;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
264
|
+
if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
265
|
+
if (ch === "," && depth === 0) {
|
|
266
|
+
result.push(current.trim());
|
|
267
|
+
current = "";
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
current += ch;
|
|
271
|
+
}
|
|
272
|
+
if (current.trim()) result.push(current.trim());
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
function findTopLevelColon(s) {
|
|
276
|
+
let found = -1;
|
|
277
|
+
scanWithStrings(
|
|
278
|
+
s,
|
|
279
|
+
() => {
|
|
280
|
+
},
|
|
281
|
+
(ch, i, depth) => {
|
|
282
|
+
if (found === -1 && ch === ":" && depth === 0) found = i;
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
return found;
|
|
286
|
+
}
|
|
287
|
+
function findTopLevelArrow(s) {
|
|
288
|
+
let depth = 0;
|
|
289
|
+
let inString = null;
|
|
290
|
+
for (let i = 0; i < s.length - 1; i++) {
|
|
291
|
+
const ch = s[i];
|
|
292
|
+
if (inString) {
|
|
293
|
+
if (ch === "\\") {
|
|
294
|
+
i++;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (ch === inString) inString = null;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (ch === '"' || ch === "'") {
|
|
301
|
+
inString = ch;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
305
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
306
|
+
else if (depth === 0 && ch === "=" && s[i + 1] === ">") return i;
|
|
307
|
+
}
|
|
308
|
+
return -1;
|
|
309
|
+
}
|
|
310
|
+
function parseArrowFunctionExpr(expr) {
|
|
311
|
+
try {
|
|
312
|
+
const wrapped = `const __mock = ${expr};`;
|
|
313
|
+
const ast = parse(wrapped);
|
|
314
|
+
const decl = ast.program.body[0];
|
|
315
|
+
if (decl.type !== "VariableDeclaration") return null;
|
|
316
|
+
const init = decl.declarations[0]?.init;
|
|
317
|
+
if (!init || init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression") {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const params = [];
|
|
321
|
+
const paramPatterns = [];
|
|
322
|
+
for (let i = 0; i < init.params.length; i++) {
|
|
323
|
+
const param = init.params[i];
|
|
324
|
+
if (param.type === "Identifier") {
|
|
325
|
+
params.push(param.name);
|
|
326
|
+
paramPatterns.push(param);
|
|
327
|
+
} else if (param.type === "RestElement" && param.argument.type === "Identifier") {
|
|
328
|
+
params.push(`...${param.argument.name}`);
|
|
329
|
+
paramPatterns.push(param);
|
|
330
|
+
} else if (param.type === "ArrayPattern" || param.type === "ObjectPattern") {
|
|
331
|
+
params.push(`__destructured_${i}`);
|
|
332
|
+
paramPatterns.push(param);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return { params, body: init.body, paramPatterns };
|
|
336
|
+
} catch {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/directives/case-arg.ts
|
|
342
|
+
function absExact(shape) {
|
|
343
|
+
return makeAbs(shape, void 0, void 0, "exact");
|
|
344
|
+
}
|
|
345
|
+
function absUnknown() {
|
|
346
|
+
return makeAbs({ k: "unknown" }, void 0, void 0, "partial");
|
|
347
|
+
}
|
|
235
348
|
var TYPE_EXPR_CHAIN_METHODS = /* @__PURE__ */ new Set([
|
|
236
349
|
"gt",
|
|
237
350
|
"ge",
|
|
@@ -306,7 +419,7 @@ function tryParseConstraint(expr) {
|
|
|
306
419
|
if (!CONSTRAINT_EXPR_RE.test(s)) return void 0;
|
|
307
420
|
if (!isSafeTypeExprSource(s)) {
|
|
308
421
|
emitDirectiveDiag({
|
|
309
|
-
code:
|
|
422
|
+
code: DIAG.directiveSyntax,
|
|
310
423
|
message: `Unsafe constraint expression rejected: ${s.slice(0, 80)} (only builder calls, chain methods, and literals are allowed)`
|
|
311
424
|
});
|
|
312
425
|
return void 0;
|
|
@@ -385,7 +498,7 @@ function constraintToCaseArgAbs(c, depth) {
|
|
|
385
498
|
var MAX_CASE_ARG_DEPTH = 32;
|
|
386
499
|
function depthCapDiag(expr) {
|
|
387
500
|
emitDirectiveDiag({
|
|
388
|
-
code:
|
|
501
|
+
code: DIAG.directiveSyntax,
|
|
389
502
|
message: `Type expression nesting exceeds depth ${MAX_CASE_ARG_DEPTH}: ${expr.trim().slice(0, 40)} \u2026 (deeper structure collapses to unknown)`
|
|
390
503
|
});
|
|
391
504
|
return absUnknown();
|
|
@@ -454,131 +567,6 @@ function parseObjectLiteral(content, depth) {
|
|
|
454
567
|
}
|
|
455
568
|
return absExact({ k: "obj", slots });
|
|
456
569
|
}
|
|
457
|
-
function scanWithStrings(s, onChar, onStructural) {
|
|
458
|
-
let depth = 0;
|
|
459
|
-
let inString = null;
|
|
460
|
-
for (let i = 0; i < s.length; i++) {
|
|
461
|
-
const ch = s[i];
|
|
462
|
-
if (inString) {
|
|
463
|
-
onChar(ch, i);
|
|
464
|
-
if (ch === inString) inString = null;
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
if (ch === '"' || ch === "'") {
|
|
468
|
-
inString = ch;
|
|
469
|
-
onChar(ch, i);
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
if (ch === "(" || ch === "[" || ch === "{") {
|
|
473
|
-
depth++;
|
|
474
|
-
onChar(ch, i);
|
|
475
|
-
onStructural(ch, i, depth);
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
if (ch === ")" || ch === "]" || ch === "}") {
|
|
479
|
-
depth--;
|
|
480
|
-
onChar(ch, i);
|
|
481
|
-
onStructural(ch, i, depth);
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
onChar(ch, i);
|
|
485
|
-
onStructural(ch, i, depth);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
function splitTopLevelArgs(s) {
|
|
489
|
-
const result = [];
|
|
490
|
-
let current = "";
|
|
491
|
-
let depth = 0;
|
|
492
|
-
let inString = null;
|
|
493
|
-
for (let i = 0; i < s.length; i++) {
|
|
494
|
-
const ch = s[i];
|
|
495
|
-
if (inString) {
|
|
496
|
-
current += ch;
|
|
497
|
-
if (ch === inString) inString = null;
|
|
498
|
-
continue;
|
|
499
|
-
}
|
|
500
|
-
if (ch === '"' || ch === "'") {
|
|
501
|
-
inString = ch;
|
|
502
|
-
current += ch;
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
506
|
-
if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
507
|
-
if (ch === "," && depth === 0) {
|
|
508
|
-
result.push(current.trim());
|
|
509
|
-
current = "";
|
|
510
|
-
continue;
|
|
511
|
-
}
|
|
512
|
-
current += ch;
|
|
513
|
-
}
|
|
514
|
-
if (current.trim()) result.push(current.trim());
|
|
515
|
-
return result;
|
|
516
|
-
}
|
|
517
|
-
function findTopLevelColon(s) {
|
|
518
|
-
let found = -1;
|
|
519
|
-
scanWithStrings(
|
|
520
|
-
s,
|
|
521
|
-
() => {
|
|
522
|
-
},
|
|
523
|
-
(ch, i, depth) => {
|
|
524
|
-
if (found === -1 && ch === ":" && depth === 0) found = i;
|
|
525
|
-
}
|
|
526
|
-
);
|
|
527
|
-
return found;
|
|
528
|
-
}
|
|
529
|
-
function findTopLevelArrow(s) {
|
|
530
|
-
let depth = 0;
|
|
531
|
-
let inString = null;
|
|
532
|
-
for (let i = 0; i < s.length - 1; i++) {
|
|
533
|
-
const ch = s[i];
|
|
534
|
-
if (inString) {
|
|
535
|
-
if (ch === "\\") {
|
|
536
|
-
i++;
|
|
537
|
-
continue;
|
|
538
|
-
}
|
|
539
|
-
if (ch === inString) inString = null;
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
542
|
-
if (ch === '"' || ch === "'") {
|
|
543
|
-
inString = ch;
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
547
|
-
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
548
|
-
else if (depth === 0 && ch === "=" && s[i + 1] === ">") return i;
|
|
549
|
-
}
|
|
550
|
-
return -1;
|
|
551
|
-
}
|
|
552
|
-
function parseArrowFunctionExpr(expr) {
|
|
553
|
-
try {
|
|
554
|
-
const wrapped = `const __mock = ${expr};`;
|
|
555
|
-
const ast = parse(wrapped);
|
|
556
|
-
const decl = ast.program.body[0];
|
|
557
|
-
if (decl.type !== "VariableDeclaration") return null;
|
|
558
|
-
const init = decl.declarations[0]?.init;
|
|
559
|
-
if (!init || init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression") {
|
|
560
|
-
return null;
|
|
561
|
-
}
|
|
562
|
-
const params = [];
|
|
563
|
-
const paramPatterns = [];
|
|
564
|
-
for (let i = 0; i < init.params.length; i++) {
|
|
565
|
-
const param = init.params[i];
|
|
566
|
-
if (param.type === "Identifier") {
|
|
567
|
-
params.push(param.name);
|
|
568
|
-
paramPatterns.push(param);
|
|
569
|
-
} else if (param.type === "RestElement" && param.argument.type === "Identifier") {
|
|
570
|
-
params.push(`...${param.argument.name}`);
|
|
571
|
-
paramPatterns.push(param);
|
|
572
|
-
} else if (param.type === "ArrayPattern" || param.type === "ObjectPattern") {
|
|
573
|
-
params.push(`__destructured_${i}`);
|
|
574
|
-
paramPatterns.push(param);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
return { params, body: init.body, paramPatterns };
|
|
578
|
-
} catch {
|
|
579
|
-
return null;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
570
|
function isExplicitUnknownExpr(expr) {
|
|
583
571
|
const s = expr.trim();
|
|
584
572
|
return s === "unknown" || s === "any" || s === "any()";
|
|
@@ -590,42 +578,24 @@ function parseAbsExprDiag(expr, context) {
|
|
|
590
578
|
const abs = parseCaseArgExpr(expr);
|
|
591
579
|
if (abs.shape.k === "unknown" && !abs.term && !isExplicitUnknownExpr(expr)) {
|
|
592
580
|
emitDirectiveDiag({
|
|
593
|
-
code:
|
|
581
|
+
code: DIAG.directiveSyntax,
|
|
594
582
|
message: `Unrecognized type expression in ${context}: ${expr.trim()} (use constraint builders, literals, or structure syntax)`
|
|
595
583
|
});
|
|
596
584
|
}
|
|
597
585
|
return abs;
|
|
598
586
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
return parseAbsExprDiag(expr, "@nudo:skip =>");
|
|
613
|
-
}
|
|
614
|
-
if (s.startsWith("(")) {
|
|
615
|
-
const inner = extractBalancedParens(s, 0);
|
|
616
|
-
if (inner !== null && s.slice(inner.length + 2).trim() === "") {
|
|
617
|
-
return parseAbsExprDiag(inner, "@nudo:skip (...)");
|
|
618
|
-
}
|
|
619
|
-
if (inner === null) {
|
|
620
|
-
emitDirectiveDiag({
|
|
621
|
-
code: "nudo:directive-syntax",
|
|
622
|
-
message: `@nudo:skip type expression has unclosed parenthesis (expected @nudo:skip (<typeExpr>)) \u2014 got: ${s.slice(0, 60)}`
|
|
623
|
-
});
|
|
624
|
-
return void 0;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
return void 0;
|
|
628
|
-
}
|
|
587
|
+
|
|
588
|
+
// src/directives/fn-directives.ts
|
|
589
|
+
import {
|
|
590
|
+
extractBalancedParens,
|
|
591
|
+
listFnDirectiveScopes,
|
|
592
|
+
scanCaseArgSpans,
|
|
593
|
+
scanCaseTags
|
|
594
|
+
} from "@nudojs/core";
|
|
595
|
+
import { DIAG as DIAG2 } from "@nudojs/core/internal";
|
|
596
|
+
|
|
597
|
+
// src/directives/mock-expr.ts
|
|
598
|
+
import { absFunction as absFunction2, mock, spy, stub } from "@nudojs/core";
|
|
629
599
|
function parseSinonExpr(expr) {
|
|
630
600
|
const s = expr.trim();
|
|
631
601
|
const stubReturnsMatch = s.match(/^sinon\.stub\(\)\.returns\((.+)\)$/);
|
|
@@ -690,7 +660,7 @@ function parseSinonExpr(expr) {
|
|
|
690
660
|
if (arrowFn) {
|
|
691
661
|
return {
|
|
692
662
|
type: "stub",
|
|
693
|
-
returnValue:
|
|
663
|
+
returnValue: absFunction2(arrowFn.params, {
|
|
694
664
|
body: arrowFn.body
|
|
695
665
|
})
|
|
696
666
|
};
|
|
@@ -790,6 +760,47 @@ function parseNudoMockExpr(expr) {
|
|
|
790
760
|
}
|
|
791
761
|
return null;
|
|
792
762
|
}
|
|
763
|
+
|
|
764
|
+
// src/directives/fn-directives.ts
|
|
765
|
+
var CASE_TAG_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:case\b[ \t]*([^\n]*)/g;
|
|
766
|
+
var JS_IDENT_RE = /^[\p{L}$_][\p{L}\p{N}$_]*$/u;
|
|
767
|
+
var MOCK_INLINE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:mock\b[ \t]+([^\s=]+)[ \t]*=[ \t]*(.+)/g;
|
|
768
|
+
var MOCK_FROM_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:mock\b[ \t]+([^\s]+)[ \t]+from[ \t]+(?:"([^"\n]+)"|'([^'\n]+)')/g;
|
|
769
|
+
var MOCK_TAG_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:mock\b[ \t]*([^\n]*)/g;
|
|
770
|
+
var PURE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:pure\b/g;
|
|
771
|
+
var SKIP_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:skip\b(?:[ \t]+(\S[^\n]*))?/g;
|
|
772
|
+
var SAMPLE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:sample\b(?:[ \t]+(\S+))?/g;
|
|
773
|
+
var SAMPLE_NUM_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
774
|
+
function parseSkipReturnsExpr(rest) {
|
|
775
|
+
if (rest === void 0) return void 0;
|
|
776
|
+
const s = rest.trim();
|
|
777
|
+
if (s === "") return void 0;
|
|
778
|
+
if (s.startsWith("=>")) {
|
|
779
|
+
const expr = s.slice(2).trim();
|
|
780
|
+
if (expr === "") {
|
|
781
|
+
emitDirectiveDiag({
|
|
782
|
+
code: DIAG2.directiveSyntax,
|
|
783
|
+
message: `@nudo:skip => requires a type expression (e.g. @nudo:skip => number())`
|
|
784
|
+
});
|
|
785
|
+
return void 0;
|
|
786
|
+
}
|
|
787
|
+
return parseAbsExprDiag(expr, "@nudo:skip =>");
|
|
788
|
+
}
|
|
789
|
+
if (s.startsWith("(")) {
|
|
790
|
+
const inner = extractBalancedParens(s, 0);
|
|
791
|
+
if (inner !== null && s.slice(inner.length + 2).trim() === "") {
|
|
792
|
+
return parseAbsExprDiag(inner, "@nudo:skip (...)");
|
|
793
|
+
}
|
|
794
|
+
if (inner === null) {
|
|
795
|
+
emitDirectiveDiag({
|
|
796
|
+
code: DIAG2.directiveSyntax,
|
|
797
|
+
message: `@nudo:skip type expression has unclosed parenthesis (expected @nudo:skip (<typeExpr>)) \u2014 got: ${s.slice(0, 60)}`
|
|
798
|
+
});
|
|
799
|
+
return void 0;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return void 0;
|
|
803
|
+
}
|
|
793
804
|
function parseDirectivesFromCommentTexts(comments) {
|
|
794
805
|
const directives = [];
|
|
795
806
|
for (const { text, startLine: commentStartLine } of comments) {
|
|
@@ -880,7 +891,7 @@ function parseDirectivesFromCommentTexts(comments) {
|
|
|
880
891
|
const raw = sampleMatch[1];
|
|
881
892
|
if (raw === void 0 || !SAMPLE_NUM_RE.test(raw)) {
|
|
882
893
|
emitDirectiveDiag({
|
|
883
|
-
code:
|
|
894
|
+
code: DIAG2.directiveSyntax,
|
|
884
895
|
message: `Malformed @nudo:sample: expected a numeric count (integer, decimal, or negative) \u2014 got: @nudo:sample ${raw ?? "(missing count)"}`
|
|
885
896
|
});
|
|
886
897
|
continue;
|
|
@@ -902,7 +913,7 @@ function scanMalformedCaseNames(text, caseArgSpans) {
|
|
|
902
913
|
const parenIdx = m.index + m[0].length - m[1].length + legal[0].length - 1;
|
|
903
914
|
if (extractBalancedParens(text, parenIdx) === null) {
|
|
904
915
|
emitDirectiveDiag({
|
|
905
|
-
code:
|
|
916
|
+
code: DIAG2.directiveSyntax,
|
|
906
917
|
message: `Malformed @nudo:case: unclosed argument list (missing closing ")") \u2014 got: @nudo:case ${rest.slice(0, 60)}`
|
|
907
918
|
});
|
|
908
919
|
}
|
|
@@ -921,7 +932,7 @@ function scanMalformedCaseNames(text, caseArgSpans) {
|
|
|
921
932
|
reason = `missing quoted name (expected @nudo:case "name" (\u2026))`;
|
|
922
933
|
}
|
|
923
934
|
emitDirectiveDiag({
|
|
924
|
-
code:
|
|
935
|
+
code: DIAG2.directiveSyntax,
|
|
925
936
|
message: `Malformed @nudo:case: ${reason} \u2014 got: @nudo:case ${rest.slice(0, 60)}`
|
|
926
937
|
});
|
|
927
938
|
}
|
|
@@ -936,7 +947,7 @@ function scanMalformedMockNames(text, caseArgSpans) {
|
|
|
936
947
|
const nameMatch = rest.match(/^([^\s=]+)(?:\s*=|\s+from\b)/);
|
|
937
948
|
if (!nameMatch) {
|
|
938
949
|
emitDirectiveDiag({
|
|
939
|
-
code:
|
|
950
|
+
code: DIAG2.directiveSyntax,
|
|
940
951
|
message: `Malformed @nudo:mock: cannot parse name (expected @nudo:mock <ident> = <expr> or @nudo:mock <ident> from "path" / 'path') \u2014 got: ${rest.slice(0, 60)}`
|
|
941
952
|
});
|
|
942
953
|
continue;
|
|
@@ -944,7 +955,7 @@ function scanMalformedMockNames(text, caseArgSpans) {
|
|
|
944
955
|
const name = nameMatch[1];
|
|
945
956
|
if (!JS_IDENT_RE.test(name)) {
|
|
946
957
|
emitDirectiveDiag({
|
|
947
|
-
code:
|
|
958
|
+
code: DIAG2.directiveSyntax,
|
|
948
959
|
message: `Malformed @nudo:mock name '${name}': not a valid identifier (use letters, digits, _, $; Unicode letters allowed)`
|
|
949
960
|
});
|
|
950
961
|
continue;
|
|
@@ -952,7 +963,7 @@ function scanMalformedMockNames(text, caseArgSpans) {
|
|
|
952
963
|
const afterName = rest.slice(name.length);
|
|
953
964
|
if (/^[ \t]+from[ \t]+(?:"[^"\n]*|'[^'\n]*)$/.test(afterName)) {
|
|
954
965
|
emitDirectiveDiag({
|
|
955
|
-
code:
|
|
966
|
+
code: DIAG2.directiveSyntax,
|
|
956
967
|
message: `Malformed @nudo:mock from: unclosed quote in path (expected @nudo:mock ${name} from "path" / 'path') \u2014 got: @nudo:mock ${rest.slice(0, 60)}`
|
|
957
968
|
});
|
|
958
969
|
}
|
|
@@ -960,13 +971,7 @@ function scanMalformedMockNames(text, caseArgSpans) {
|
|
|
960
971
|
}
|
|
961
972
|
function extractDirectives(ast, opts) {
|
|
962
973
|
if (!opts?.diags) return collectFnDirectives(ast);
|
|
963
|
-
|
|
964
|
-
activeDiagSink = { list: opts.diags, seen: /* @__PURE__ */ new Set() };
|
|
965
|
-
try {
|
|
966
|
-
return collectFnDirectives(ast);
|
|
967
|
-
} finally {
|
|
968
|
-
activeDiagSink = prev;
|
|
969
|
-
}
|
|
974
|
+
return runWithDirectiveDiags(opts.diags, () => collectFnDirectives(ast));
|
|
970
975
|
}
|
|
971
976
|
function collectFnDirectives(ast) {
|
|
972
977
|
const results = [];
|
|
@@ -987,6 +992,10 @@ function collectFnDirectives(ast) {
|
|
|
987
992
|
function extractDirectivesQuiet(ast) {
|
|
988
993
|
return extractDirectives(ast, { diags: [] });
|
|
989
994
|
}
|
|
995
|
+
|
|
996
|
+
// src/directives/inline-directives.ts
|
|
997
|
+
import { CONSTRAINT_EXPR_RE as CONSTRAINT_EXPR_RE2 } from "@nudojs/core";
|
|
998
|
+
import { DIAG as DIAG3 } from "@nudojs/core/internal";
|
|
990
999
|
function stripInlineCommentPrefix(line, kind) {
|
|
991
1000
|
const s = kind === "block" ? line.replace(/^\s*\*\s?/, "") : line.replace(/^\//, "");
|
|
992
1001
|
return s.trim();
|
|
@@ -1017,13 +1026,7 @@ function extractInlineDirectives(node, opts) {
|
|
|
1017
1026
|
const comments = node.leadingComments;
|
|
1018
1027
|
if (!comments) return [];
|
|
1019
1028
|
if (!opts?.diags) return collectInlineDirectivesFromComments(comments);
|
|
1020
|
-
|
|
1021
|
-
activeDiagSink = { list: opts.diags, seen: /* @__PURE__ */ new Set() };
|
|
1022
|
-
try {
|
|
1023
|
-
return collectInlineDirectivesFromComments(comments);
|
|
1024
|
-
} finally {
|
|
1025
|
-
activeDiagSink = prev;
|
|
1026
|
-
}
|
|
1029
|
+
return runWithDirectiveDiags(opts.diags, () => collectInlineDirectivesFromComments(comments));
|
|
1027
1030
|
}
|
|
1028
1031
|
function collectInlineDirectivesFromComments(comments) {
|
|
1029
1032
|
const results = [];
|
|
@@ -1039,7 +1042,7 @@ function collectInlineDirectivesFromComments(comments) {
|
|
|
1039
1042
|
if (trailingIdx !== -1) {
|
|
1040
1043
|
const typePart = rawExpr.slice(0, trailingIdx).trim();
|
|
1041
1044
|
emitDirectiveDiag({
|
|
1042
|
-
code:
|
|
1045
|
+
code: DIAG3.directiveSyntax,
|
|
1043
1046
|
message: `Trailing comment in @nudo:as type expression: '${rawExpr}' (remove the comment; the type must be a clean expression)`
|
|
1044
1047
|
});
|
|
1045
1048
|
results.push({ kind: "as", typeAbs: typePart ? parseAbsExprDiag(typePart, "@nudo:as") : absUnknown() });
|
|
@@ -1050,7 +1053,7 @@ function collectInlineDirectivesFromComments(comments) {
|
|
|
1050
1053
|
}
|
|
1051
1054
|
if (AS_BARE_RE.test(line)) {
|
|
1052
1055
|
emitDirectiveDiag({
|
|
1053
|
-
code:
|
|
1056
|
+
code: DIAG3.directiveSyntax,
|
|
1054
1057
|
message: `@nudo:as requires a type expression (e.g. @nudo:as number())`
|
|
1055
1058
|
});
|
|
1056
1059
|
continue;
|
|
@@ -1069,7 +1072,7 @@ function collectInlineDirectivesFromComments(comments) {
|
|
|
1069
1072
|
});
|
|
1070
1073
|
} else {
|
|
1071
1074
|
emitDirectiveDiag({
|
|
1072
|
-
code:
|
|
1075
|
+
code: DIAG3.directiveSyntax,
|
|
1073
1076
|
message: `Malformed @nudo:replace: cannot separate target from type (expected @nudo:replace <target> <typeExpr>) \u2014 got: ${raw.slice(0, 60)}`
|
|
1074
1077
|
});
|
|
1075
1078
|
}
|
|
@@ -1077,7 +1080,7 @@ function collectInlineDirectivesFromComments(comments) {
|
|
|
1077
1080
|
}
|
|
1078
1081
|
if (REPLACE_BARE_RE.test(line)) {
|
|
1079
1082
|
emitDirectiveDiag({
|
|
1080
|
-
code:
|
|
1083
|
+
code: DIAG3.directiveSyntax,
|
|
1081
1084
|
message: `@nudo:replace requires a target and type expression (expected @nudo:replace <target> <typeExpr>)`
|
|
1082
1085
|
});
|
|
1083
1086
|
}
|
|
@@ -1108,20 +1111,44 @@ function findReplaceSeparator(raw) {
|
|
|
1108
1111
|
}
|
|
1109
1112
|
if (ch === " " && depth === 0) {
|
|
1110
1113
|
const rest = raw.slice(i + 1).trimStart();
|
|
1111
|
-
if (
|
|
1114
|
+
if (CONSTRAINT_EXPR_RE2.test(rest) || rest.startsWith("{") || rest.startsWith("[") || rest.startsWith('"') || rest.startsWith("'") || rest === "true" || rest === "false" || rest === "null" || rest === "undefined" || rest === "unknown" || rest === "any" || rest === "never" || /^-?\d+(\.\d+)?$/.test(rest) || rest.includes("=>") || /^function\s*[\w$]*\s*\(/.test(rest)) {
|
|
1112
1115
|
return i;
|
|
1113
1116
|
}
|
|
1114
1117
|
}
|
|
1115
1118
|
}
|
|
1116
1119
|
return -1;
|
|
1117
1120
|
}
|
|
1121
|
+
|
|
1122
|
+
// src/directives/file-directives.ts
|
|
1123
|
+
import { parseEnvPayload, parseMockModulePayload } from "@nudojs/core";
|
|
1124
|
+
import { DIAG as DIAG4 } from "@nudojs/core/internal";
|
|
1118
1125
|
var ENV_REGEX = /^\/?\s*@nudo:env\s+(.+)/;
|
|
1119
1126
|
var MOCK_MODULE_REGEX = /^\/?\s*@nudo:mock-module\s+(.+)/;
|
|
1120
|
-
|
|
1127
|
+
var FILE_TAG_IN_BLOCK_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:(env|mock-module)\b/g;
|
|
1128
|
+
function warnMisplacedFileTags(blockText) {
|
|
1129
|
+
FILE_TAG_IN_BLOCK_REGEX.lastIndex = 0;
|
|
1130
|
+
let m;
|
|
1131
|
+
while (m = FILE_TAG_IN_BLOCK_REGEX.exec(blockText)) {
|
|
1132
|
+
const tag = m[1];
|
|
1133
|
+
emitDirectiveDiag({
|
|
1134
|
+
code: DIAG4.directiveSyntax,
|
|
1135
|
+
message: tag === "env" ? `@nudo:env is a file-level directive \u2014 use '/// @nudo:env node' at the top of the file (JSDoc block form is ignored)` : `@nudo:mock-module is a file-level directive \u2014 use '/// @nudo:mock-module "fs" from "./mock-fs.js"' at the top of the file (JSDoc block form is ignored)`
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
function extractFileDirectives(ast, opts) {
|
|
1140
|
+
if (!opts?.diags) return collectFileDirectives(ast);
|
|
1141
|
+
return runWithDirectiveDiags(opts.diags, () => collectFileDirectives(ast));
|
|
1142
|
+
}
|
|
1143
|
+
function collectFileDirectives(ast) {
|
|
1121
1144
|
const results = [];
|
|
1122
1145
|
if (ast.type !== "File") return results;
|
|
1123
1146
|
const comments = ast.comments ?? [];
|
|
1124
1147
|
for (const comment of comments) {
|
|
1148
|
+
if (comment.type === "CommentBlock") {
|
|
1149
|
+
warnMisplacedFileTags(comment.value);
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1125
1152
|
if (comment.type !== "CommentLine") continue;
|
|
1126
1153
|
const text = comment.value;
|
|
1127
1154
|
const mockModuleMatch = text.match(MOCK_MODULE_REGEX);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nudojs/parser",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"engines": {
|
|
5
|
-
"node": ">=
|
|
5
|
+
"node": ">=22"
|
|
6
6
|
},
|
|
7
7
|
"description": "Babel-based parser and directive extraction for Nudo",
|
|
8
8
|
"type": "module",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"registry": "https://registry.npmjs.org"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@nudojs/core": "1.
|
|
42
|
+
"@nudojs/core": "1.15.0",
|
|
43
43
|
"@babel/types": "^8.0.6"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|