@nudojs/parser 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -188,8 +188,9 @@ declare function takeDirectiveDiagsSince(since: number): DirectiveDiag[];
188
188
  /**
189
189
  * case 实参 / 指令类型表达式唯一文法:约束构建器优先,其余为具体字面量、
190
190
  * 结构字面量与箭头函数。`T.*` 文法已物理删除。
191
+ * depth 按包含关系逐层 +1 线程化传递(兄弟共享父深度,不用共享可变计数器)。
191
192
  */
192
- declare function parseCaseArgExpr(expr: string): Abs;
193
+ declare function parseCaseArgExpr(expr: string, depth?: number): Abs;
193
194
  /**
194
195
  * D6=G2:指令绑定 AST 最近 Function(含 nested function、class method)。
195
196
  * 作用域清单与 refine 的 @nudo:contract 同源(core directive-scan),
package/dist/index.js CHANGED
@@ -170,6 +170,7 @@ function paramName(param) {
170
170
  return void 0;
171
171
  }
172
172
  function unwrapDefaultExport(mod) {
173
+ if (mod == null) return void 0;
173
174
  if (typeof mod === "function") return mod;
174
175
  return mod.default;
175
176
  }
@@ -189,6 +190,7 @@ import {
189
190
  execNudoModule,
190
191
  isNudoConstraint,
191
192
  constraintToEntryAbs,
193
+ CONSTRAINT_BUILDER_NAMES,
192
194
  CONSTRAINT_EXPR_RE,
193
195
  SELF,
194
196
  listFnDirectiveScopes,
@@ -258,18 +260,94 @@ function emitDirectiveDiag(d) {
258
260
  function resetDirectiveDiagSeen() {
259
261
  directiveDiagSeen.clear();
260
262
  }
261
- var CASE_TAG_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:case\s+([^\n]+)/g;
263
+ var CASE_TAG_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:case\b[ \t]*([^\n]*)/g;
262
264
  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;
265
+ var MOCK_INLINE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:mock\b[ \t]+([^\s=]+)[ \t]*=[ \t]*(.+)/g;
266
+ var MOCK_FROM_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:mock\b[ \t]+([^\s]+)[ \t]+from[ \t]+"([^"\n]+)"/g;
267
+ var MOCK_TAG_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:mock\b[ \t]*([^\n]*)/g;
268
+ var PURE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:pure\b/g;
269
+ var SKIP_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:skip\b(?:[ \t]+(\S[^\n]*))?/g;
270
+ var SAMPLE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/[ \t]*)?@nudo:sample\b(?:[ \t]+(\S+))?/g;
269
271
  var SAMPLE_NUM_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
272
+ var TYPE_EXPR_CHAIN_METHODS = /* @__PURE__ */ new Set([
273
+ "gt",
274
+ "ge",
275
+ "lt",
276
+ "le",
277
+ "int",
278
+ "min",
279
+ "max",
280
+ "length",
281
+ "shift",
282
+ "optional"
283
+ ]);
284
+ function isSafeTypeExprNode(node) {
285
+ switch (node.type) {
286
+ case "NumericLiteral":
287
+ case "StringLiteral":
288
+ case "BooleanLiteral":
289
+ case "NullLiteral":
290
+ return true;
291
+ case "Identifier":
292
+ return node.name === "undefined";
293
+ case "UnaryExpression":
294
+ return node.operator === "-" && node.argument.type === "NumericLiteral";
295
+ case "ArrayExpression":
296
+ return node.elements.every(
297
+ (el) => el !== null && el.type !== "SpreadElement" && isSafeTypeExprNode(el)
298
+ );
299
+ case "ObjectExpression":
300
+ return node.properties.every((prop) => {
301
+ if (prop.type !== "ObjectProperty") return false;
302
+ if (prop.computed) return false;
303
+ const key = prop.key;
304
+ if (key.type !== "Identifier" && key.type !== "StringLiteral") return false;
305
+ return isSafeTypeExprNode(prop.value);
306
+ });
307
+ case "CallExpression": {
308
+ const argsOk = node.arguments.every(
309
+ (arg) => arg.type !== "SpreadElement" && arg.type !== "ArgumentPlaceholder" && isSafeTypeExprNode(arg)
310
+ );
311
+ if (!argsOk) return false;
312
+ return isSafeTypeExprCallee(node.callee);
313
+ }
314
+ default:
315
+ return false;
316
+ }
317
+ }
318
+ function isSafeTypeExprCallee(node) {
319
+ if (node.type === "Identifier") {
320
+ return CONSTRAINT_BUILDER_NAMES.includes(node.name);
321
+ }
322
+ if (node.type === "MemberExpression") {
323
+ if (node.computed) return false;
324
+ if (node.property.type !== "Identifier") return false;
325
+ if (!TYPE_EXPR_CHAIN_METHODS.has(node.property.name)) return false;
326
+ return isSafeTypeExprNode(node.object);
327
+ }
328
+ return false;
329
+ }
330
+ function isSafeTypeExprSource(s) {
331
+ try {
332
+ const ast = parse(`(${s});`);
333
+ if (ast.program.body.length !== 1) return false;
334
+ const stmt = ast.program.body[0];
335
+ if (stmt.type !== "ExpressionStatement") return false;
336
+ return isSafeTypeExprNode(stmt.expression);
337
+ } catch {
338
+ return false;
339
+ }
340
+ }
270
341
  function tryParseConstraint(expr) {
271
342
  const s = expr.trim();
272
343
  if (!CONSTRAINT_EXPR_RE.test(s)) return void 0;
344
+ if (!isSafeTypeExprSource(s)) {
345
+ emitDirectiveDiag({
346
+ code: "nudo:directive-syntax",
347
+ message: `Unsafe constraint expression rejected: ${s.slice(0, 80)} (only builder calls, chain methods, and literals are allowed)`
348
+ });
349
+ return void 0;
350
+ }
273
351
  try {
274
352
  const src = `export const __nudo_case_arg = (${s});`;
275
353
  const exports = execNudoModule(src);
@@ -305,9 +383,10 @@ function constraintSelfEqLit(c, selfIds) {
305
383
  if (isSelf(b) && a.op === "lit") return { found: true, value: a.value };
306
384
  return { found: false };
307
385
  }
308
- function constraintToCaseArgAbs(c) {
386
+ function constraintToCaseArgAbs(c, depth) {
387
+ if (depth > MAX_CASE_ARG_DEPTH) return depthCapDiag(constraintPreview(c));
309
388
  if (c.members && c.members.length > 0) {
310
- const parts = c.members.map((m) => constraintToCaseArgAbs(m));
389
+ const parts = c.members.map((m) => constraintToCaseArgAbs(m, depth + 1));
311
390
  if (parts.length === 1) return parts[0];
312
391
  return { shape: { k: "sum", members: parts }, conf: "path" };
313
392
  }
@@ -322,12 +401,12 @@ function constraintToCaseArgAbs(c) {
322
401
  return absUndefLit();
323
402
  }
324
403
  if (c.element) {
325
- return absExact({ k: "arr", element: constraintToCaseArgAbs(c.element) });
404
+ return absExact({ k: "arr", element: constraintToCaseArgAbs(c.element, depth + 1) });
326
405
  }
327
406
  if (c.fields) {
328
407
  const slots = {};
329
408
  for (const [key, field] of Object.entries(c.fields)) {
330
- slots[key] = { value: constraintToCaseArgAbs(field.constraint) };
409
+ slots[key] = { value: constraintToCaseArgAbs(field.constraint, depth + 1) };
331
410
  }
332
411
  return absExact({ k: "obj", slots });
333
412
  }
@@ -340,18 +419,34 @@ function constraintToCaseArgAbs(c) {
340
419
  }
341
420
  return constraintToEntryAbs(c, "__arg");
342
421
  }
343
- function parseCaseArgExpr(expr) {
422
+ var MAX_CASE_ARG_DEPTH = 32;
423
+ function depthCapDiag(expr) {
424
+ emitDirectiveDiag({
425
+ code: "nudo:directive-syntax",
426
+ message: `Type expression nesting exceeds depth ${MAX_CASE_ARG_DEPTH}: ${expr.trim().slice(0, 40)} \u2026 (deeper structure collapses to unknown)`
427
+ });
428
+ return absUnknown();
429
+ }
430
+ function constraintPreview(c) {
431
+ if (c.element) return "array(\u2026)";
432
+ if (c.members) return "union(\u2026)";
433
+ if (c.fields) return "shape({ \u2026 })";
434
+ if (c.fn) return "fn(\u2026)";
435
+ return c.prim ? `${c.prim}()` : "constraint";
436
+ }
437
+ function parseCaseArgExpr(expr, depth = 0) {
438
+ if (depth > MAX_CASE_ARG_DEPTH) return depthCapDiag(expr);
344
439
  const constraint = tryParseConstraint(expr);
345
440
  if (constraint) {
346
441
  try {
347
- return constraintToCaseArgAbs(constraint);
442
+ return constraintToCaseArgAbs(constraint, depth);
348
443
  } catch {
349
444
  return absUnknown();
350
445
  }
351
446
  }
352
- return parseLiteralOrStructure(expr);
447
+ return parseLiteralOrStructure(expr, depth);
353
448
  }
354
- function parseLiteralOrStructure(expr) {
449
+ function parseLiteralOrStructure(expr, depth) {
355
450
  const s = expr.trim();
356
451
  if (s === "true") return absLit(true);
357
452
  if (s === "false") return absLit(false);
@@ -370,17 +465,20 @@ function parseLiteralOrStructure(expr) {
370
465
  return absLit(s.slice(1, -1));
371
466
  }
372
467
  if (s.startsWith("{") && s.endsWith("}")) {
373
- return parseObjectLiteral(s.slice(1, -1).trim());
468
+ return parseObjectLiteral(s.slice(1, -1).trim(), depth);
374
469
  }
375
470
  if (s.startsWith("[") && s.endsWith("]")) {
376
471
  const content = s.slice(1, -1).trim();
377
472
  if (!content) return absExact({ k: "tuple", elements: [] });
378
473
  const elements = splitTopLevelArgs(content);
379
- return absExact({ k: "tuple", elements: elements.map((e) => parseCaseArgExpr(e)) });
474
+ return absExact({
475
+ k: "tuple",
476
+ elements: elements.map((e) => parseCaseArgExpr(e, depth + 1))
477
+ });
380
478
  }
381
479
  return absUnknown();
382
480
  }
383
- function parseObjectLiteral(content) {
481
+ function parseObjectLiteral(content, depth) {
384
482
  if (!content) return absExact({ k: "obj", slots: {} });
385
483
  const entries = splitTopLevelArgs(content);
386
484
  const slots = {};
@@ -389,7 +487,7 @@ function parseObjectLiteral(content) {
389
487
  if (colonIdx === -1) continue;
390
488
  const key = entry.slice(0, colonIdx).trim().replace(/^["']|["']$/g, "");
391
489
  const val = entry.slice(colonIdx + 1).trim();
392
- slots[key] = { value: parseCaseArgExpr(val) };
490
+ slots[key] = { value: parseCaseArgExpr(val, depth + 1) };
393
491
  }
394
492
  return absExact({ k: "obj", slots });
395
493
  }
@@ -555,6 +653,13 @@ function parseSkipReturnsExpr(rest) {
555
653
  if (inner !== null && s.slice(inner.length + 2).trim() === "") {
556
654
  return parseAbsExprDiag(inner, "@nudo:skip (...)");
557
655
  }
656
+ if (inner === null) {
657
+ emitDirectiveDiag({
658
+ code: "nudo:directive-syntax",
659
+ message: `@nudo:skip type expression has unclosed parenthesis (expected @nudo:skip (<typeExpr>)) \u2014 got: ${s.slice(0, 60)}`
660
+ });
661
+ return void 0;
662
+ }
558
663
  }
559
664
  return void 0;
560
665
  }
@@ -648,7 +753,8 @@ function parseSinonExpr(expr) {
648
753
  return null;
649
754
  }
650
755
  function parseNudoMockExpr(expr) {
651
- const s = expr.trim();
756
+ let s = expr.trim();
757
+ while (s.startsWith("sinon.")) s = s.slice("sinon.".length);
652
758
  const stubReturnsOnFirstMatch = s.match(/^stub\(\)\.returns\((.+)\)\.onFirstCall\(\)$/);
653
759
  if (stubReturnsOnFirstMatch) {
654
760
  const helper = stub.returns(parseAbsExpr(stubReturnsOnFirstMatch[1].trim()));
@@ -719,9 +825,6 @@ function parseNudoMockExpr(expr) {
719
825
  if (s === "mock()") {
720
826
  return mock();
721
827
  }
722
- if (s.startsWith("sinon.")) {
723
- return parseNudoMockExpr(s.slice("sinon.".length));
724
- }
725
828
  return null;
726
829
  }
727
830
  function parseDirectivesFromCommentTexts(comments) {
@@ -831,8 +934,17 @@ function scanMalformedCaseNames(text, caseArgSpans) {
831
934
  while ((m = CASE_TAG_REGEX.exec(text)) !== null) {
832
935
  if (inCaseArgs(m.index)) continue;
833
936
  const rest = m[1].trim();
834
- if (/^"[^"]+"\s*\(/.test(rest)) continue;
835
- if (/^"[^"]+"\s*\(/.test(rest)) continue;
937
+ const legal = rest.match(/^("[^"]+")[ \t]*\(/);
938
+ if (legal) {
939
+ const parenIdx = m.index + m[0].length - m[1].length + legal[0].length - 1;
940
+ if (extractBalancedParens(text, parenIdx) === null) {
941
+ emitDirectiveDiag({
942
+ code: "nudo:directive-syntax",
943
+ message: `Malformed @nudo:case: unclosed argument list (missing closing ")") \u2014 got: @nudo:case ${rest.slice(0, 60)}`
944
+ });
945
+ }
946
+ continue;
947
+ }
836
948
  let reason;
837
949
  if (rest.startsWith("'")) {
838
950
  reason = `single-quoted name (use double quotes: @nudo:case "name" (\u2026))`;
@@ -872,6 +984,14 @@ function scanMalformedMockNames(text, caseArgSpans) {
872
984
  code: "nudo:directive-syntax",
873
985
  message: `Malformed @nudo:mock name '${name}': not a valid identifier (use letters, digits, _, $; Unicode letters allowed)`
874
986
  });
987
+ continue;
988
+ }
989
+ const afterName = rest.slice(name.length);
990
+ if (/^[ \t]+from[ \t]+"[^"\n]*$/.test(afterName)) {
991
+ emitDirectiveDiag({
992
+ code: "nudo:directive-syntax",
993
+ message: `Malformed @nudo:mock from: unclosed quote in path (expected @nudo:mock ${name} from "path") \u2014 got: @nudo:mock ${rest.slice(0, 60)}`
994
+ });
875
995
  }
876
996
  }
877
997
  }
@@ -898,9 +1018,32 @@ function extractDirectivesQuiet(ast) {
898
1018
  takeDirectiveDiagsSince(since);
899
1019
  return out;
900
1020
  }
901
- var AS_REGEX = /^\s*@nudo:as\s+(.+)/;
902
- var REPLACE_REGEX = /^\s*@nudo:replace\s+(.+)/;
903
- var AS_TRAILING_COMMENT_RE = /\s+(?:\/\/|\/\*)/;
1021
+ function stripInlineCommentPrefix(line, kind) {
1022
+ const s = kind === "block" ? line.replace(/^\s*\*\s?/, "") : line.replace(/^\//, "");
1023
+ return s.trim();
1024
+ }
1025
+ var AS_LINE_REGEX = /^@nudo:as[ \t]+(.+)$/;
1026
+ var REPLACE_LINE_REGEX = /^@nudo:replace[ \t]+(.+)$/;
1027
+ var AS_BARE_RE = /^@nudo:as[ \t]*$/;
1028
+ var REPLACE_BARE_RE = /^@nudo:replace[ \t]*$/;
1029
+ function findTrailingCommentStart(expr) {
1030
+ let inString = null;
1031
+ for (let i = 0; i < expr.length; i++) {
1032
+ const ch = expr[i];
1033
+ if (inString) {
1034
+ if (ch === inString) inString = null;
1035
+ continue;
1036
+ }
1037
+ if (ch === '"' || ch === "'") {
1038
+ inString = ch;
1039
+ continue;
1040
+ }
1041
+ if ((ch === " " || ch === " ") && /^[ \t]+(?:\/\/|\/\*)/.test(expr.slice(i))) {
1042
+ return i;
1043
+ }
1044
+ }
1045
+ return -1;
1046
+ }
904
1047
  function extractInlineDirectives(node) {
905
1048
  resetDirectiveDiagSeen();
906
1049
  const comments = node.leadingComments;
@@ -908,39 +1051,56 @@ function extractInlineDirectives(node) {
908
1051
  const results = [];
909
1052
  for (const comment of comments) {
910
1053
  if (comment.type !== "CommentLine" && comment.type !== "CommentBlock") continue;
911
- const text = comment.value;
912
- const asMatch = text.match(AS_REGEX);
913
- if (asMatch) {
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();
1054
+ const kind = comment.type === "CommentLine" ? "line" : "block";
1055
+ for (const rawLine of comment.value.split("\n")) {
1056
+ const line = stripInlineCommentPrefix(rawLine, kind);
1057
+ const asMatch = line.match(AS_LINE_REGEX);
1058
+ if (asMatch) {
1059
+ const rawExpr = asMatch[1].trim();
1060
+ const trailingIdx = findTrailingCommentStart(rawExpr);
1061
+ if (trailingIdx !== -1) {
1062
+ const typePart = rawExpr.slice(0, trailingIdx).trim();
1063
+ emitDirectiveDiag({
1064
+ code: "nudo:directive-syntax",
1065
+ message: `Trailing comment in @nudo:as type expression: '${rawExpr}' (remove the comment; the type must be a clean expression)`
1066
+ });
1067
+ results.push({ kind: "as", typeAbs: typePart ? parseAbsExprDiag(typePart, "@nudo:as") : absUnknown() });
1068
+ continue;
1069
+ }
1070
+ results.push({ kind: "as", typeAbs: parseAbsExprDiag(rawExpr, "@nudo:as") });
1071
+ continue;
1072
+ }
1073
+ if (AS_BARE_RE.test(line)) {
918
1074
  emitDirectiveDiag({
919
1075
  code: "nudo:directive-syntax",
920
- message: `Trailing comment in @nudo:as type expression: '${rawExpr}' (remove the comment; the type must be a clean expression)`
1076
+ message: `@nudo:as requires a type expression (e.g. @nudo:as number())`
921
1077
  });
922
- results.push({ kind: "as", typeAbs: typePart ? parseAbsExprDiag(typePart, "@nudo:as") : absUnknown() });
923
1078
  continue;
924
1079
  }
925
- results.push({ kind: "as", typeAbs: parseAbsExprDiag(rawExpr, "@nudo:as") });
926
- continue;
927
- }
928
- const replaceMatch = text.match(REPLACE_REGEX);
929
- if (replaceMatch) {
930
- const raw = replaceMatch[1].trim();
931
- const sepIdx = findReplaceSeparator(raw);
932
- if (sepIdx !== -1) {
933
- const targetSource = raw.slice(0, sepIdx).trim();
934
- const typeExprStr = raw.slice(sepIdx + 1).trim();
935
- results.push({
936
- kind: "replace",
937
- targetSource,
938
- typeAbs: parseAbsExprDiag(typeExprStr, "@nudo:replace")
939
- });
940
- } else {
1080
+ const replaceMatch = line.match(REPLACE_LINE_REGEX);
1081
+ if (replaceMatch) {
1082
+ const raw = replaceMatch[1].trim();
1083
+ const sepIdx = findReplaceSeparator(raw);
1084
+ if (sepIdx !== -1) {
1085
+ const targetSource = raw.slice(0, sepIdx).trim();
1086
+ const typeExprStr = raw.slice(sepIdx + 1).trim();
1087
+ results.push({
1088
+ kind: "replace",
1089
+ targetSource,
1090
+ typeAbs: parseAbsExprDiag(typeExprStr, "@nudo:replace")
1091
+ });
1092
+ } else {
1093
+ emitDirectiveDiag({
1094
+ code: "nudo:directive-syntax",
1095
+ message: `Malformed @nudo:replace: cannot separate target from type (expected @nudo:replace <target> <typeExpr>) \u2014 got: ${raw.slice(0, 60)}`
1096
+ });
1097
+ }
1098
+ continue;
1099
+ }
1100
+ if (REPLACE_BARE_RE.test(line)) {
941
1101
  emitDirectiveDiag({
942
1102
  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)}`
1103
+ message: `@nudo:replace requires a target and type expression (expected @nudo:replace <target> <typeExpr>)`
944
1104
  });
945
1105
  }
946
1106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nudojs/parser",
3
- "version": "1.2.1",
3
+ "version": "1.3.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.5.0",
42
+ "@nudojs/core": "1.6.0",
43
43
  "@babel/types": "^8.0.6"
44
44
  },
45
45
  "scripts": {