@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 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
- SELF
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 CASE_NAME_REGEX = /@nudo:case\s+"([^"]+)"\s*\(/g;
214
- var MOCK_INLINE_REGEX = /@nudo:mock\s+(\w+)\s*=\s*(.+)/g;
215
- var MOCK_FROM_REGEX = /@nudo:mock\s+(\w+)\s+from\s+"([^"]+)"/g;
216
- var PURE_REGEX = /@nudo:pure\b/g;
217
- var SKIP_REGEX = /@nudo:skip(?:\s+(.+))?/g;
218
- var SAMPLE_REGEX = /@nudo:sample\s+(\d+)/g;
219
- var CONSTRAINT_EXPR_RE = /^(number|string|boolean|any|array|shape|lit|union|fn|partial|pick|omit|record|required|readonly|nonNullable|and)\s*\(/;
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 parseDirectivesFromComments(comments) {
727
+ function parseDirectivesFromCommentTexts(comments) {
675
728
  const directives = [];
676
- for (const comment of comments) {
677
- const text = comment.value;
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: mockFromMatch[1],
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: mockMatch[1],
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
- CASE_NAME_REGEX.lastIndex = 0;
716
- let match;
717
- const commentStartLine = comment.loc?.start.line ?? 0;
718
- while ((match = CASE_NAME_REGEX.exec(text)) !== null) {
719
- const name = match[1];
720
- const parenStart = match.index + match[0].length - 1;
721
- const argsStr = extractBalancedParens(text, parenStart);
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
- if (PURE_REGEX.test(text)) {
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
- const returnsExpr = skipMatch[1]?.trim();
803
+ if (inCaseArgs(skipMatch.index)) continue;
804
+ const returns = parseSkipReturnsExpr(skipMatch[1]);
747
805
  directives.push({
748
806
  kind: "skip",
749
- returns: returnsExpr ? parseAbsExpr(returnsExpr) : void 0
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
- directives.push({ kind: "sample", count: Number(sampleMatch[1]) });
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 getFunctionName(node) {
761
- if (node.type === "FunctionDeclaration" && node.id) return node.id.name;
762
- if (node.type === "ExportNamedDeclaration") {
763
- return node.declaration ? getFunctionName(node.declaration) : "<anonymous>";
764
- }
765
- if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
766
- return node.declaration.id.name;
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
- if (node.type === "VariableDeclaration") {
769
- const decl = node.declarations[0];
770
- if (decl.id.type === "Identifier") return decl.id.name;
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 body = ast.program.body;
778
- for (const stmt of body) {
779
- const leadingComments = stmt.leadingComments;
780
- if (!leadingComments || leadingComments.length === 0) continue;
781
- const directives = parseDirectivesFromComments(leadingComments);
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
- const name = getFunctionName(stmt);
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
- results.push({ kind: "as", typeAbs: parseAbsExpr(asMatch[1].trim()) });
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: parseAbsExpr(typeExprStr)
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 && raw[i - 1] !== "\\") inString = null;
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 = /^\/\s*@nudo:env\s+(.+)/;
850
- var MOCK_MODULE_REGEX = /^\/\s*@nudo:mock-module\s+"([^"]+)"\s+from\s+"([^"]+)"/;
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
- results.push({
873
- kind: "mock-module",
874
- source: mockModuleMatch[1],
875
- fromPath: mockModuleMatch[2]
876
- });
877
- continue;
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].split(",").map((e) => e.trim()).filter(Boolean);
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.1.7",
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.2.2",
42
+ "@nudojs/core": "1.4.0",
43
43
  "@babel/types": "^8.0.6"
44
44
  },
45
45
  "scripts": {