Package not found. Please check the package name and try again.

@nudojs/parser 1.1.8 → 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 = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:case\s+"([^"]+)"\s*\(/g;
214
- var MOCK_INLINE_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:mock\s+(\w+)\s*=\s*(.+)/g;
215
- var MOCK_FROM_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*)?@nudo:mock\s+(\w+)\s+from\s+"([^"]+)"/g;
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]+(\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*\(/;
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 parseDirectivesFromComments(comments) {
727
+ function parseDirectivesFromCommentTexts(comments) {
661
728
  const directives = [];
662
- for (const comment of comments) {
663
- 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);
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: mockFromMatch[1],
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: mockMatch[1],
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
- CASE_NAME_REGEX.lastIndex = 0;
702
- let match;
703
- const commentStartLine = comment.loc?.start.line ?? 0;
704
- while ((match = CASE_NAME_REGEX.exec(text)) !== null) {
705
- const name = match[1];
706
- const parenStart = match.index + match[0].length - 1;
707
- const argsStr = extractBalancedParens(text, parenStart);
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
- if (PURE_REGEX.test(text)) {
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
- const returnsExpr = skipMatch[1]?.trim();
803
+ if (inCaseArgs(skipMatch.index)) continue;
804
+ const returns = parseSkipReturnsExpr(skipMatch[1]);
734
805
  directives.push({
735
806
  kind: "skip",
736
- returns: returnsExpr ? parseAbsExpr(returnsExpr) : void 0
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
- 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) });
743
823
  }
744
824
  }
745
825
  return directives;
746
826
  }
747
- function getFunctionName(node) {
748
- if (node.type === "FunctionDeclaration" && node.id) return node.id.name;
749
- if (node.type === "ExportNamedDeclaration") {
750
- return node.declaration ? getFunctionName(node.declaration) : "<anonymous>";
751
- }
752
- if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
753
- 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
+ });
754
852
  }
755
- if (node.type === "VariableDeclaration") {
756
- const decl = node.declarations[0];
757
- 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
+ }
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 body = ast.program.body;
765
- for (const stmt of body) {
766
- const leadingComments = stmt.leadingComments;
767
- if (!leadingComments || leadingComments.length === 0) continue;
768
- 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
+ );
769
890
  if (directives.length === 0) continue;
770
- const name = getFunctionName(stmt);
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
- 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") });
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: 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)}`
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 = /^\/\s*@nudo:env\s+(.+)/;
837
- var MOCK_MODULE_REGEX = /^\/\s*@nudo:mock-module\s+"([^"]+)"\s+from\s+"([^"]+)"/;
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
- results.push({
860
- kind: "mock-module",
861
- source: mockModuleMatch[1],
862
- fromPath: mockModuleMatch[2]
863
- });
864
- 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
+ }
865
1001
  }
866
1002
  const envMatch = text.match(ENV_REGEX);
867
1003
  if (envMatch) {
868
- const envs = envMatch[1].split(",").map((e) => e.trim()).filter(Boolean);
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.8",
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.3.0",
42
+ "@nudojs/core": "1.4.0",
43
43
  "@babel/types": "^8.0.6"
44
44
  },
45
45
  "scripts": {