eslint-plugin-formatjs 6.6.3 → 6.7.0-rc.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.
Files changed (3) hide show
  1. package/index.js +591 -124
  2. package/index.js.map +1 -1
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -16,6 +16,56 @@ var __exportAll = (all, no_symbols) => {
16
16
  return target;
17
17
  };
18
18
  //#endregion
19
+ //#region packages/eslint-plugin-formatjs/message-types.ts
20
+ function collectMessageArguments(ast) {
21
+ const argumentsByName = /* @__PURE__ */ new Map();
22
+ function visit(elements) {
23
+ for (const element of elements) {
24
+ if (element.type === TYPE.literal || element.type === TYPE.pound) continue;
25
+ const types = argumentsByName.get(element.value) ?? /* @__PURE__ */ new Set();
26
+ argumentsByName.set(element.value, types);
27
+ switch (element.type) {
28
+ case TYPE.argument: break;
29
+ case TYPE.number:
30
+ case TYPE.plural:
31
+ types.add("number");
32
+ break;
33
+ case TYPE.date:
34
+ case TYPE.time:
35
+ types.add("date");
36
+ break;
37
+ case TYPE.select:
38
+ types.add("select");
39
+ break;
40
+ case TYPE.tag:
41
+ types.add("tag");
42
+ visit(element.children);
43
+ }
44
+ if (element.type === TYPE.select || element.type === TYPE.plural) for (const option of Object.values(element.options)) visit(option.value);
45
+ }
46
+ }
47
+ visit(ast);
48
+ return argumentsByName;
49
+ }
50
+ /** Derive one contract from the runtime parser's AST, including every branch. */
51
+ function messageTypes(ast, module, ignoreList = []) {
52
+ const argumentsByName = collectMessageArguments(ast);
53
+ const ignored = new Set(ignoreList);
54
+ for (const name of ignored) if (!argumentsByName.has(name)) argumentsByName.set(name, /* @__PURE__ */ new Set());
55
+ const fields = [...argumentsByName].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, types]) => {
56
+ let type;
57
+ if (!types.size) type = `import(${JSON.stringify(module)}).MessageValue`;
58
+ else if (types.size === 2 && types.has("number") && types.has("date")) type = "number";
59
+ else if (types.size > 1) throw new Error(`Incompatible uses of argument ${JSON.stringify(name)}`);
60
+ else if (types.has("number")) type = "number | bigint";
61
+ else if (types.has("date")) type = "number | Date";
62
+ else if (types.has("select")) type = "string";
63
+ else type = `import(${JSON.stringify(module)}).MessageTag`;
64
+ return `${JSON.stringify(name)}${ignored.has(name) ? "?" : ""}: ${type}`;
65
+ });
66
+ return fields.length ? `{ ${fields.join("; ")} }` : "{}";
67
+ }
68
+ //#endregion
19
69
  //#region packages/eslint-plugin-formatjs/util.ts
20
70
  const FORMAT_FUNCTION_NAMES = /* @__PURE__ */ new Set([
21
71
  "$formatMessage",
@@ -220,11 +270,530 @@ function patchMessage(messageNode, ast, patcher) {
220
270
  return null;
221
271
  }
222
272
  //#endregion
273
+ //#region packages/eslint-plugin-formatjs/placeholder-checks.ts
274
+ function messageIgnoreTag(context, node) {
275
+ let ignoreTag = getSettings(context).ignoreTag ?? false;
276
+ if (node.type !== "CallExpression" || !node.arguments[2]) return ignoreTag;
277
+ const options = node.arguments[2];
278
+ if (options.type !== "ObjectExpression") return;
279
+ for (const property of options.properties) {
280
+ if (property.type !== "Property" || property.computed || property.method || property.kind !== "init") return;
281
+ if ((property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? property.key.value : void 0) !== "ignoreTag") continue;
282
+ if (property.value.type !== "Literal" || typeof property.value.value !== "boolean") return;
283
+ ignoreTag = property.value.value;
284
+ }
285
+ return ignoreTag;
286
+ }
287
+ function checkPlaceholders(context, node) {
288
+ const settings = getSettings(context);
289
+ const ignoreTag = messageIgnoreTag(context, node);
290
+ if (ignoreTag === void 0) return;
291
+ const msgs = extractMessages(node, {
292
+ excludeMessageDeclCalls: true,
293
+ ...settings
294
+ });
295
+ const { options: [opt] } = context;
296
+ const ignoreList = new Set(opt?.ignoreList || []);
297
+ for (const [{ message: { defaultMessage }, messageNode }, values] of msgs) {
298
+ if (!defaultMessage || !messageNode) continue;
299
+ if (values && values.type !== "ObjectExpression") continue;
300
+ if (values?.properties.find((prop) => prop.type === "SpreadElement")) continue;
301
+ const literalElementByLiteralKey = /* @__PURE__ */ new Map();
302
+ if (values) {
303
+ for (const prop of values.properties) if (prop.type === "Property" && !prop.computed) {
304
+ const name = prop.key.type === "Identifier" ? prop.key.name : String(prop.key.value);
305
+ literalElementByLiteralKey.set(name, prop);
306
+ }
307
+ }
308
+ let ast;
309
+ try {
310
+ ast = parse(defaultMessage, { ignoreTag });
311
+ } catch (e) {
312
+ context.report({
313
+ node: messageNode,
314
+ messageId: "parseError",
315
+ data: { error: e instanceof Error ? e.message : String(e) }
316
+ });
317
+ continue;
318
+ }
319
+ const placeholderNames = new Set(collectMessageArguments(ast).keys());
320
+ const missingPlaceholders = [];
321
+ placeholderNames.forEach((name) => {
322
+ if (!ignoreList.has(name) && !literalElementByLiteralKey.has(name)) missingPlaceholders.push(name);
323
+ });
324
+ if (missingPlaceholders.length > 0) context.report({
325
+ node: messageNode,
326
+ messageId: "missingValue",
327
+ data: { list: missingPlaceholders.join(", ") }
328
+ });
329
+ literalElementByLiteralKey.forEach((element, key) => {
330
+ if (!ignoreList.has(key) && !placeholderNames.has(key)) context.report({
331
+ node: element,
332
+ messageId: "unusedValue"
333
+ });
334
+ });
335
+ }
336
+ }
337
+ //#endregion
223
338
  //#region packages/eslint-plugin-formatjs/messages.ts
224
339
  const CORE_MESSAGES = { parseError: `Failed to parse message string {{error}}` };
225
340
  //#endregion
341
+ //#region packages/eslint-plugin-formatjs/rules/enforce-placeholders.ts
342
+ const name$23 = "enforce-placeholders";
343
+ const rule$21 = {
344
+ meta: {
345
+ type: "problem",
346
+ docs: {
347
+ description: "Enforce that all messages with placeholders have enough passed-in values",
348
+ url: "https://formatjs.github.io/docs/tooling/linter#enforce-placeholders"
349
+ },
350
+ schema: [{
351
+ type: "object",
352
+ properties: { ignoreList: {
353
+ type: "array",
354
+ items: { type: "string" }
355
+ } },
356
+ additionalProperties: false
357
+ }],
358
+ messages: {
359
+ ...CORE_MESSAGES,
360
+ missingValue: "Missing value(s) for the following placeholder(s): {{list}}.",
361
+ unusedValue: "Value not used by the message."
362
+ }
363
+ },
364
+ create(context) {
365
+ const callExpressionVisitor = (node) => checkPlaceholders(context, node);
366
+ const parserServices = context.sourceCode.parserServices;
367
+ if (parserServices?.defineTemplateBodyVisitor) return parserServices.defineTemplateBodyVisitor({ CallExpression: callExpressionVisitor }, { CallExpression: callExpressionVisitor });
368
+ return {
369
+ JSXOpeningElement: (node) => checkPlaceholders(context, node),
370
+ CallExpression: callExpressionVisitor
371
+ };
372
+ }
373
+ };
374
+ //#endregion
375
+ //#region packages/eslint-plugin-formatjs/rules/enforce-message-types.ts
376
+ const name$22 = "enforce-message-types";
377
+ const modules = /* @__PURE__ */ new Set([
378
+ "@formatjs/intl",
379
+ "react-intl",
380
+ "react-intl/server",
381
+ "intl-messageformat"
382
+ ]);
383
+ function staticObject(node) {
384
+ return node?.type === "ObjectExpression" && node.properties.every((property) => property.type === "Property" && !property.computed && property.kind === "init" && !property.method);
385
+ }
386
+ function staticMessage(node) {
387
+ if (!node) return false;
388
+ if (node.type === "Literal") return typeof node.value === "string";
389
+ if (node.type === "TemplateLiteral") return node.expressions.length === 0;
390
+ if (node.type === "BinaryExpression") return node.operator === "+" && staticMessage(node.left) && staticMessage(node.right);
391
+ const wrapper = node;
392
+ if ([
393
+ "TSAsExpression",
394
+ "TSSatisfiesExpression",
395
+ "TSNonNullExpression",
396
+ "TSTypeAssertion"
397
+ ].includes(wrapper.type)) return staticMessage(wrapper.expression);
398
+ return false;
399
+ }
400
+ function importedHelper(context, node) {
401
+ const callee = node.callee;
402
+ const identifier = callee.type === "Identifier" ? callee : callee.type === "MemberExpression" && !callee.computed && callee.object.type === "Identifier" ? callee.object : void 0;
403
+ if (!identifier) return;
404
+ let scope = context.sourceCode.getScope(node);
405
+ while (scope) {
406
+ const variable = scope.set.get(identifier.name);
407
+ if (variable) {
408
+ const definition = variable.defs[0];
409
+ if (definition?.type !== "ImportBinding") return;
410
+ const module = definition.parent.source.value;
411
+ if (typeof module !== "string" || !modules.has(module)) return;
412
+ const specifier = definition.node;
413
+ let helper;
414
+ if (callee.type === "Identifier" && specifier.type === "ImportSpecifier") helper = specifier.imported.type === "Identifier" ? specifier.imported.name : String(specifier.imported.value);
415
+ else if (callee.type === "MemberExpression" && callee.property.type === "Identifier" && specifier.type === "ImportNamespaceSpecifier") helper = callee.property.name;
416
+ if (node.type === "NewExpression" && module === "intl-messageformat") {
417
+ if (callee.type === "Identifier" && specifier.type === "ImportDefaultSpecifier" || helper === "IntlMessageFormat" || helper === "default") return {
418
+ module,
419
+ helper: "IntlMessageFormat"
420
+ };
421
+ return;
422
+ }
423
+ if (module === "intl-messageformat") return;
424
+ if (helper === "defineMessage" || helper === "defineMessages") return {
425
+ module,
426
+ helper
427
+ };
428
+ return;
429
+ }
430
+ scope = scope.upper;
431
+ }
432
+ }
433
+ function staticString(node) {
434
+ if (!node) return;
435
+ if (node.type === "Literal") return typeof node.value === "string" ? node.value : void 0;
436
+ if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0].value.cooked ?? void 0;
437
+ if (node.type === "BinaryExpression" && node.operator === "+") {
438
+ const left = staticString(node.left);
439
+ const right = staticString(node.right);
440
+ if (left !== void 0 && right !== void 0) return left + right;
441
+ }
442
+ const wrapper = node;
443
+ if ([
444
+ "TSAsExpression",
445
+ "TSSatisfiesExpression",
446
+ "TSNonNullExpression",
447
+ "TSTypeAssertion"
448
+ ].includes(wrapper.type)) return staticString(wrapper.expression);
449
+ }
450
+ function renderType(node) {
451
+ if (!node) return;
452
+ const keywords = {
453
+ TSNumberKeyword: "number",
454
+ TSBigIntKeyword: "bigint",
455
+ TSStringKeyword: "string"
456
+ };
457
+ if (keywords[node.type]) return keywords[node.type];
458
+ if (node.type === "TSTypeReference" && node.typeName?.type === "Identifier") return node.typeName.name;
459
+ if (node.type === "TSUnionType") {
460
+ const types = node.types?.map(renderType);
461
+ if (types?.every((type) => type !== void 0)) return types.join(" | ");
462
+ }
463
+ if (node.type === "TSImportType" && node.qualifier?.type === "Identifier") {
464
+ const module = node.source?.value ?? node.argument?.literal?.value ?? node.argument?.value;
465
+ if (typeof module === "string") return `import(${JSON.stringify(module)}).${node.qualifier.name}`;
466
+ }
467
+ if (node.type === "TSTypeLiteral" && node.members) {
468
+ const fields = [];
469
+ for (const member of node.members) {
470
+ if (member.type !== "TSPropertySignature" || member.readonly || member.computed) return;
471
+ const key = member.key?.type === "Identifier" ? member.key.name : member.key?.value;
472
+ const type = renderType(member.typeAnnotation?.typeAnnotation);
473
+ if (key === void 0 || type === void 0) return;
474
+ fields.push(`${JSON.stringify(String(key))}${member.optional ? "?" : ""}: ${type}`);
475
+ }
476
+ return fields.length ? `{ ${fields.join("; ")} }` : "{}";
477
+ }
478
+ }
479
+ function hoistTypes(context, node, contract) {
480
+ const source = context.sourceCode;
481
+ const imports = /* @__PURE__ */ new Map();
482
+ const occupied = new Set(source.scopeManager?.scopes.flatMap((scope) => scope.variables.map((variable) => variable.name)));
483
+ const resolved = /* @__PURE__ */ new Map();
484
+ return {
485
+ text: contract.replace(/import\(("(?:[^"\\]|\\.)*")\)\.(MessageTag|MessageValue)/g, (reference, quotedModule, imported) => {
486
+ if (resolved.has(reference)) return resolved.get(reference);
487
+ const module = JSON.parse(quotedModule);
488
+ for (const declaration of source.ast.body) {
489
+ if (declaration.type !== "ImportDeclaration" || declaration.source.value !== module) continue;
490
+ for (const specifier of declaration.specifiers) {
491
+ if (specifier.type !== "ImportSpecifier") continue;
492
+ if ((specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value) !== imported) continue;
493
+ let scope = source.getScope(node);
494
+ while (scope && !scope.set.has(specifier.local.name)) scope = scope.upper;
495
+ if (scope?.set.get(specifier.local.name)?.defs.some((def) => def.node === specifier)) {
496
+ resolved.set(reference, specifier.local.name);
497
+ return specifier.local.name;
498
+ }
499
+ }
500
+ }
501
+ let local = imported;
502
+ for (let suffix = 1; occupied.has(local); suffix++) local = imported + suffix;
503
+ occupied.add(local);
504
+ const specifiers = imports.get(quotedModule) ?? [];
505
+ specifiers.push(imported + (local === imported ? "" : " as " + local));
506
+ imports.set(quotedModule, specifiers);
507
+ resolved.set(reference, local);
508
+ return local;
509
+ }),
510
+ fix(fixer) {
511
+ if (!imports.size) return [];
512
+ let anchor = source.ast.body.filter((statement) => statement.type === "ImportDeclaration").at(-1);
513
+ if (!anchor) for (const statement of source.ast.body) {
514
+ if (statement.type !== "ExpressionStatement" || statement.expression.type !== "Literal" || typeof statement.expression.value !== "string") break;
515
+ anchor = statement;
516
+ }
517
+ const block = [...imports].map(([module, specifiers]) => "import type {" + specifiers.join(", ") + "} from " + module + ";").join("\n");
518
+ return anchor ? [fixer.insertTextAfter(anchor, "\n" + block)] : [fixer.insertTextBefore(source.ast.body[0], block + "\n")];
519
+ }
520
+ };
521
+ }
522
+ const descriptorModules = /* @__PURE__ */ new Set([
523
+ "@formatjs/intl",
524
+ "react-intl",
525
+ "react-intl/server"
526
+ ]);
527
+ function checkInlineMessage(context, node) {
528
+ if (node.type !== "CallExpression") return;
529
+ const settings = getSettings(context);
530
+ if (node.callee.type === "MemberExpression" && node.callee.computed) return;
531
+ if (!isIntlFormatMessageCall({
532
+ ...node,
533
+ arguments: [{
534
+ type: "ObjectExpression",
535
+ properties: []
536
+ }]
537
+ }, settings.additionalFunctionNames)) return;
538
+ const source = context.sourceCode;
539
+ const call = node;
540
+ const generic = call.typeArguments ?? call.typeParameters;
541
+ if (!generic && !context.options[0]?.generateTypes) return;
542
+ const generated = !!generic && /^<\s*\/\* @formatjs-generated \*\//.test(source.getText(generic));
543
+ const descriptor = node.arguments[0];
544
+ if (!staticObject(descriptor)) {
545
+ if (generic) context.report({
546
+ node,
547
+ messageId: "dynamic"
548
+ });
549
+ return;
550
+ }
551
+ const existingModule = source.ast.body.find((statement) => statement.type === "ImportDeclaration" && typeof statement.source.value === "string" && descriptorModules.has(statement.source.value));
552
+ const module = context.options[0]?.moduleSource ?? (existingModule?.type === "ImportDeclaration" ? String(existingModule.source.value) : "@formatjs/intl");
553
+ const ignoreTag = messageIgnoreTag(context, node);
554
+ if (ignoreTag === void 0) {
555
+ if (generic) context.report({
556
+ node,
557
+ messageId: "dynamic"
558
+ });
559
+ return !!generic;
560
+ }
561
+ const messages = extractMessages(node, settings);
562
+ const message = messages[0]?.[0];
563
+ if (messages.length !== 1 || !message || typeof message.message.defaultMessage !== "string" || !staticMessage(message.messageNode ?? void 0)) {
564
+ if (generic) context.report({
565
+ node,
566
+ messageId: "dynamic"
567
+ });
568
+ return;
569
+ }
570
+ let contract;
571
+ try {
572
+ contract = messageTypes(parse(message.message.defaultMessage, { ignoreTag }), module, context.options[0]?.ignoreList);
573
+ } catch (error) {
574
+ context.report({
575
+ node,
576
+ messageId: "invalid",
577
+ data: { error: error instanceof Error ? error.message : String(error) }
578
+ });
579
+ return true;
580
+ }
581
+ const imports = hoistTypes(context, node, contract);
582
+ contract = imports.text;
583
+ const parameters = generic?.params;
584
+ if (parameters && parameters.length >= 1 && parameters.length <= 2 && renderType(parameters[0]) === contract && !generated) return true;
585
+ if (generic && (!parameters || parameters.length > 2)) {
586
+ context.report({
587
+ node: generic,
588
+ messageId: "manual"
589
+ });
590
+ return true;
591
+ }
592
+ context.report({
593
+ node,
594
+ messageId: "contract",
595
+ fix(fixer) {
596
+ return [...imports.fix(fixer), generic && parameters?.[0] ? fixer.replaceTextRange([generic.range[0] + 1, parameters[0].range[1]], contract) : fixer.insertTextAfter(node.optional ? source.getTokenAfter(node.callee) : node.callee, "<" + contract + ">")];
597
+ }
598
+ });
599
+ return true;
600
+ }
601
+ const rule$20 = {
602
+ meta: {
603
+ type: "problem",
604
+ docs: { description: "Check ICU placeholders and optionally generate TypeScript argument contracts" },
605
+ fixable: "code",
606
+ schema: [{
607
+ type: "object",
608
+ properties: {
609
+ generateTypes: { type: "boolean" },
610
+ ignoreList: {
611
+ type: "array",
612
+ items: { type: "string" }
613
+ },
614
+ moduleSource: {
615
+ type: "string",
616
+ enum: [...descriptorModules]
617
+ }
618
+ },
619
+ additionalProperties: false
620
+ }],
621
+ messages: {
622
+ ...rule$21.meta.messages,
623
+ contract: "Generate or refresh the ICU argument contract.",
624
+ invalid: "Cannot generate message types: {{error}}.",
625
+ manual: "Unsupported number of generic arguments; update the call manually.",
626
+ dynamic: "Typed messages require static message strings and parser options for contract verification."
627
+ }
628
+ },
629
+ create(context) {
630
+ const listeners = {
631
+ JSXOpeningElement(node) {
632
+ checkPlaceholders(context, node);
633
+ },
634
+ NewExpression(node) {
635
+ if (node.type !== "NewExpression" || !/\.[cm]?tsx?$/.test(context.filename)) return;
636
+ const imported = importedHelper(context, node);
637
+ if (imported?.helper !== "IntlMessageFormat") return;
638
+ const source = context.sourceCode;
639
+ const call = node;
640
+ const generic = call.typeArguments ?? call.typeParameters;
641
+ if (!generic && !context.options[0]?.generateTypes) return;
642
+ const generated = !!generic && /^<\s*\/\* @formatjs-generated \*\//.test(source.getText(generic));
643
+ const message = staticString(node.arguments[0]);
644
+ const options = node.arguments[3];
645
+ let ignoreTag = false;
646
+ if (message === void 0 || node.arguments.some((argument) => argument.type === "SpreadElement") || options && !staticObject(options)) {
647
+ if (generic) context.report({
648
+ node,
649
+ messageId: "dynamic"
650
+ });
651
+ return;
652
+ }
653
+ if (staticObject(options)) for (const property of options.properties) {
654
+ if (property.type !== "Property") continue;
655
+ if ((property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? property.key.value : void 0) !== "ignoreTag") continue;
656
+ if (property.value.type !== "Literal" || typeof property.value.value !== "boolean") {
657
+ if (generic) context.report({
658
+ node,
659
+ messageId: "dynamic"
660
+ });
661
+ return;
662
+ }
663
+ ignoreTag = property.value.value;
664
+ }
665
+ let contract;
666
+ try {
667
+ contract = messageTypes(parse(message, { ignoreTag }), imported.module, context.options[0]?.ignoreList);
668
+ } catch (error) {
669
+ context.report({
670
+ node,
671
+ messageId: "invalid",
672
+ data: { error: error instanceof Error ? error.message : String(error) }
673
+ });
674
+ return;
675
+ }
676
+ const imports = hoistTypes(context, node, contract);
677
+ contract = imports.text;
678
+ const parameters = generic?.params;
679
+ if (parameters?.length === 1 && renderType(parameters[0]) === contract && !generated) return;
680
+ if (generic && parameters?.length !== 1) {
681
+ context.report({
682
+ node: generic,
683
+ messageId: "manual"
684
+ });
685
+ return;
686
+ }
687
+ context.report({
688
+ node,
689
+ messageId: "contract",
690
+ fix(fixer) {
691
+ const expected = `<${contract}>`;
692
+ return [...imports.fix(fixer), generic ? fixer.replaceText(generic, expected) : fixer.insertTextAfter(node.callee, expected)];
693
+ }
694
+ });
695
+ },
696
+ CallExpression(node) {
697
+ if (node.type !== "CallExpression") return;
698
+ if (!/\.[cm]?tsx?$/.test(context.filename)) {
699
+ checkPlaceholders(context, node);
700
+ return;
701
+ }
702
+ const imported = importedHelper(context, node);
703
+ if (!imported) {
704
+ if (!checkInlineMessage(context, node)) checkPlaceholders(context, node);
705
+ return;
706
+ }
707
+ const source = context.sourceCode;
708
+ const call = node;
709
+ const generic = call.typeArguments ?? call.typeParameters;
710
+ const generated = !!generic && /^<\s*\/\* @formatjs-generated \*\//.test(source.getText(generic));
711
+ const options = node.arguments[1];
712
+ const typed = staticObject(options) && options.properties.length === 1 && options.properties.some((p) => p.type === "Property" && (p.key.type === "Identifier" ? p.key.name : p.key.type === "Literal" ? p.key.value : void 0) === "typed" && p.value.type === "Literal" && p.value.value === true);
713
+ if (!typed && !generated && !context.options[0]?.generateTypes) return;
714
+ if (!typed && generic && !generated && !context.options[0]?.generateTypes) return;
715
+ if (node.arguments.length > 2 || options && !typed) return;
716
+ const descriptor = node.arguments[0];
717
+ if (!staticObject(descriptor)) {
718
+ if (typed || generated) context.report({
719
+ node,
720
+ messageId: "dynamic"
721
+ });
722
+ return;
723
+ }
724
+ if (imported.helper === "defineMessages" && !descriptor.properties.every((p) => p.type === "Property" && staticObject(p.value))) {
725
+ if (typed || generated) context.report({
726
+ node,
727
+ messageId: "dynamic"
728
+ });
729
+ return;
730
+ }
731
+ const messages = extractMessages({
732
+ ...node,
733
+ callee: {
734
+ type: "Identifier",
735
+ name: imported.helper
736
+ }
737
+ }, getSettings(context));
738
+ if (messages.some(([message]) => typeof message.message.defaultMessage !== "string" || !staticMessage(message.messageNode ?? void 0)) || messages.length !== (imported.helper === "defineMessage" ? 1 : descriptor.properties.length)) {
739
+ if (typed || generated) context.report({
740
+ node,
741
+ messageId: "dynamic"
742
+ });
743
+ return;
744
+ }
745
+ let contract;
746
+ try {
747
+ const types = messages.map(([message]) => messageTypes(parse(message.message.defaultMessage, { ignoreTag: getSettings(context).ignoreTag }), imported.module, context.options[0]?.ignoreList));
748
+ contract = imported.helper === "defineMessage" ? types[0] : descriptor.properties.length === 0 ? "{}" : `{ ${descriptor.properties.map((p, i) => {
749
+ if (p.type !== "Property") throw new Error("Unexpected spread");
750
+ const key = p.key.type === "Identifier" ? p.key.name : p.key.type === "Literal" ? String(p.key.value) : void 0;
751
+ if (key === void 0) throw new Error("Unsupported catalog key");
752
+ return `${JSON.stringify(key)}: ${types[i]}`;
753
+ }).join("; ")} }`;
754
+ } catch (error) {
755
+ context.report({
756
+ node,
757
+ messageId: "invalid",
758
+ data: { error: error instanceof Error ? error.message : String(error) }
759
+ });
760
+ return;
761
+ }
762
+ const imports = hoistTypes(context, node, contract);
763
+ contract = imports.text;
764
+ const expected = `<${contract}>`;
765
+ const parameters = generic?.params;
766
+ if (parameters?.length === 1 && renderType(parameters[0]) === contract && typed && !generated) return;
767
+ if (generic && parameters?.length !== 1) {
768
+ context.report({
769
+ node: generic,
770
+ messageId: "manual"
771
+ });
772
+ return;
773
+ }
774
+ context.report({
775
+ node,
776
+ messageId: "contract",
777
+ fix(fixer) {
778
+ const edits = [...imports.fix(fixer), generic ? fixer.replaceText(generic, expected) : fixer.insertTextAfter(node.optional ? source.getTokenAfter(node.callee) : node.callee, expected)];
779
+ if (!typed) {
780
+ const close = source.getLastToken(node);
781
+ const previous = source.getTokenBefore(close);
782
+ edits.push(fixer.insertTextBefore(close, `${previous.value === "," ? "" : ","} {typed: true}`));
783
+ }
784
+ return edits;
785
+ }
786
+ });
787
+ }
788
+ };
789
+ const parserServices = context.sourceCode.parserServices;
790
+ if (parserServices?.defineTemplateBodyVisitor) return parserServices.defineTemplateBodyVisitor({ CallExpression: listeners.CallExpression }, { CallExpression: listeners.CallExpression });
791
+ return listeners;
792
+ }
793
+ };
794
+ //#endregion
226
795
  //#region packages/eslint-plugin-formatjs/rules/blocklist-elements.ts
227
- const name$22 = "blocklist-elements";
796
+ const name$21 = "blocklist-elements";
228
797
  function getMessage(type) {
229
798
  return {
230
799
  messageId: "blocklist",
@@ -277,7 +846,7 @@ function verifyAst$7(blocklist, ast) {
277
846
  }
278
847
  return errors;
279
848
  }
280
- function checkNode$17(context, node) {
849
+ function checkNode$16(context, node) {
281
850
  const settings = getSettings(context);
282
851
  const msgs = extractMessages(node, settings);
283
852
  if (!msgs.length) return;
@@ -303,7 +872,7 @@ function checkNode$17(context, node) {
303
872
  });
304
873
  }
305
874
  }
306
- const rule$20 = {
875
+ const rule$19 = {
307
876
  meta: {
308
877
  type: "problem",
309
878
  docs: {
@@ -347,11 +916,11 @@ const rule$20 = {
347
916
  }
348
917
  },
349
918
  create(context) {
350
- const callExpressionVisitor = (node) => checkNode$17(context, node);
919
+ const callExpressionVisitor = (node) => checkNode$16(context, node);
351
920
  const parserServices = context.sourceCode.parserServices;
352
921
  if (parserServices?.defineTemplateBodyVisitor) return parserServices.defineTemplateBodyVisitor({ CallExpression: callExpressionVisitor }, { CallExpression: callExpressionVisitor });
353
922
  return {
354
- JSXOpeningElement: (node) => checkNode$17(context, node),
923
+ JSXOpeningElement: (node) => checkNode$16(context, node),
355
924
  CallExpression: callExpressionVisitor
356
925
  };
357
926
  }
@@ -363,8 +932,8 @@ let Option$1 = /* @__PURE__ */ function(Option) {
363
932
  Option["anything"] = "anything";
364
933
  return Option;
365
934
  }({});
366
- const name$21 = "enforce-default-message";
367
- function checkNode$16(context, node) {
935
+ const name$20 = "enforce-default-message";
936
+ function checkNode$15(context, node) {
368
937
  const msgs = extractMessages(node, getSettings(context));
369
938
  const { options: [type] } = context;
370
939
  for (const [{ message: { defaultMessage }, messageNode, messageDescriptorNode }] of msgs) if (!defaultMessage) {
@@ -378,7 +947,7 @@ function checkNode$16(context, node) {
378
947
  });
379
948
  }
380
949
  }
381
- const rule$19 = {
950
+ const rule$18 = {
382
951
  meta: {
383
952
  type: "problem",
384
953
  docs: {
@@ -398,11 +967,11 @@ const rule$19 = {
398
967
  }
399
968
  },
400
969
  create(context) {
401
- const callExpressionVisitor = (node) => checkNode$16(context, node);
970
+ const callExpressionVisitor = (node) => checkNode$15(context, node);
402
971
  const parserServices = context.sourceCode.parserServices;
403
972
  if (parserServices?.defineTemplateBodyVisitor) return parserServices.defineTemplateBodyVisitor({ CallExpression: callExpressionVisitor }, { CallExpression: callExpressionVisitor });
404
973
  return {
405
- JSXOpeningElement: (node) => checkNode$16(context, node),
974
+ JSXOpeningElement: (node) => checkNode$15(context, node),
406
975
  CallExpression: callExpressionVisitor
407
976
  };
408
977
  }
@@ -428,7 +997,7 @@ function normalizeOptions(raw) {
428
997
  minLength: void 0
429
998
  };
430
999
  }
431
- function checkNode$15(context, node) {
1000
+ function checkNode$14(context, node) {
432
1001
  const msgs = extractMessages(node, getSettings(context));
433
1002
  const { mode: type, minLength } = normalizeOptions(context.options[0]);
434
1003
  for (const [{ message: { description }, descriptionNode, messageDescriptorNode }] of msgs) if (!description) {
@@ -449,8 +1018,8 @@ function checkNode$15(context, node) {
449
1018
  }
450
1019
  });
451
1020
  }
452
- const name$20 = "enforce-description";
453
- const rule$18 = {
1021
+ const name$19 = "enforce-description";
1022
+ const rule$17 = {
454
1023
  meta: {
455
1024
  type: "problem",
456
1025
  docs: {
@@ -483,18 +1052,18 @@ const rule$18 = {
483
1052
  }
484
1053
  },
485
1054
  create(context) {
486
- const callExpressionVisitor = (node) => checkNode$15(context, node);
1055
+ const callExpressionVisitor = (node) => checkNode$14(context, node);
487
1056
  const parserServices = context.sourceCode.parserServices;
488
1057
  if (parserServices?.defineTemplateBodyVisitor) return parserServices.defineTemplateBodyVisitor({ CallExpression: callExpressionVisitor }, { CallExpression: callExpressionVisitor });
489
1058
  return {
490
- JSXOpeningElement: (node) => checkNode$15(context, node),
1059
+ JSXOpeningElement: (node) => checkNode$14(context, node),
491
1060
  CallExpression: callExpressionVisitor
492
1061
  };
493
1062
  }
494
1063
  };
495
1064
  //#endregion
496
1065
  //#region packages/eslint-plugin-formatjs/rules/enforce-id.ts
497
- function checkNode$14(context, node, { idInterpolationPattern, idWhitelistRegexps, quoteStyle }) {
1066
+ function checkNode$13(context, node, { idInterpolationPattern, idWhitelistRegexps, quoteStyle }) {
498
1067
  const msgs = extractMessages(node, getSettings(context));
499
1068
  for (const [{ message: { defaultMessage, description, id }, idPropNode, descriptionNode, messagePropNode, messageDescriptorNode }] of msgs) {
500
1069
  if (!idInterpolationPattern && !idPropNode) {
@@ -552,8 +1121,8 @@ function checkNode$14(context, node, { idInterpolationPattern, idWhitelistRegexp
552
1121
  }
553
1122
  }
554
1123
  }
555
- const name$19 = "enforce-id";
556
- const rule$17 = {
1124
+ const name$18 = "enforce-id";
1125
+ const rule$16 = {
557
1126
  meta: {
558
1127
  type: "problem",
559
1128
  docs: {
@@ -604,114 +1173,11 @@ Actual: {{actual}}`
604
1173
  const { idWhitelist } = tmp;
605
1174
  opts.idWhitelistRegexps = idWhitelist.map((str) => new RegExp(str, "i"));
606
1175
  }
607
- const callExpressionVisitor = (node) => checkNode$14(context, node, opts);
608
- const parserServices = context.sourceCode.parserServices;
609
- if (parserServices?.defineTemplateBodyVisitor) return parserServices.defineTemplateBodyVisitor({ CallExpression: callExpressionVisitor }, { CallExpression: callExpressionVisitor });
610
- return {
611
- JSXOpeningElement: (node) => checkNode$14(context, node, opts),
612
- CallExpression: callExpressionVisitor
613
- };
614
- }
615
- };
616
- //#endregion
617
- //#region packages/eslint-plugin-formatjs/rules/enforce-placeholders.ts
618
- function collectPlaceholderNames(ast) {
619
- const placeholderNames = /* @__PURE__ */ new Set();
620
- _traverse(ast);
621
- return placeholderNames;
622
- function _traverse(ast) {
623
- for (const element of ast) switch (element.type) {
624
- case TYPE.literal:
625
- case TYPE.pound: break;
626
- case TYPE.tag:
627
- placeholderNames.add(element.value);
628
- _traverse(element.children);
629
- break;
630
- case TYPE.plural:
631
- case TYPE.select:
632
- placeholderNames.add(element.value);
633
- for (const { value } of Object.values(element.options)) _traverse(value);
634
- break;
635
- default: placeholderNames.add(element.value);
636
- }
637
- }
638
- }
639
- function checkNode$13(context, node) {
640
- const settings = getSettings(context);
641
- const msgs = extractMessages(node, {
642
- excludeMessageDeclCalls: true,
643
- ...settings
644
- });
645
- const { options: [opt] } = context;
646
- const ignoreList = new Set(opt?.ignoreList || []);
647
- for (const [{ message: { defaultMessage }, messageNode }, values] of msgs) {
648
- if (!defaultMessage || !messageNode) continue;
649
- if (values && values.type !== "ObjectExpression") continue;
650
- if (values?.properties.find((prop) => prop.type === "SpreadElement")) continue;
651
- const literalElementByLiteralKey = /* @__PURE__ */ new Map();
652
- if (values) {
653
- for (const prop of values.properties) if (prop.type === "Property" && !prop.computed) {
654
- const name = prop.key.type === "Identifier" ? prop.key.name : String(prop.key.value);
655
- literalElementByLiteralKey.set(name, prop);
656
- }
657
- }
658
- let ast;
659
- try {
660
- ast = parse(defaultMessage, { ignoreTag: settings.ignoreTag });
661
- } catch (e) {
662
- context.report({
663
- node: messageNode,
664
- messageId: "parseError",
665
- data: { error: e instanceof Error ? e.message : String(e) }
666
- });
667
- continue;
668
- }
669
- const placeholderNames = collectPlaceholderNames(ast);
670
- const missingPlaceholders = [];
671
- placeholderNames.forEach((name) => {
672
- if (!ignoreList.has(name) && !literalElementByLiteralKey.has(name)) missingPlaceholders.push(name);
673
- });
674
- if (missingPlaceholders.length > 0) context.report({
675
- node: messageNode,
676
- messageId: "missingValue",
677
- data: { list: missingPlaceholders.join(", ") }
678
- });
679
- literalElementByLiteralKey.forEach((element, key) => {
680
- if (!ignoreList.has(key) && !placeholderNames.has(key)) context.report({
681
- node: element,
682
- messageId: "unusedValue"
683
- });
684
- });
685
- }
686
- }
687
- const name$18 = "enforce-placeholders";
688
- const rule$16 = {
689
- meta: {
690
- type: "problem",
691
- docs: {
692
- description: "Enforce that all messages with placeholders have enough passed-in values",
693
- url: "https://formatjs.github.io/docs/tooling/linter#enforce-placeholders"
694
- },
695
- schema: [{
696
- type: "object",
697
- properties: { ignoreList: {
698
- type: "array",
699
- items: { type: "string" }
700
- } },
701
- additionalProperties: false
702
- }],
703
- messages: {
704
- ...CORE_MESSAGES,
705
- missingValue: "Missing value(s) for the following placeholder(s): {{list}}.",
706
- unusedValue: "Value not used by the message."
707
- }
708
- },
709
- create(context) {
710
- const callExpressionVisitor = (node) => checkNode$13(context, node);
1176
+ const callExpressionVisitor = (node) => checkNode$13(context, node, opts);
711
1177
  const parserServices = context.sourceCode.parserServices;
712
1178
  if (parserServices?.defineTemplateBodyVisitor) return parserServices.defineTemplateBodyVisitor({ CallExpression: callExpressionVisitor }, { CallExpression: callExpressionVisitor });
713
1179
  return {
714
- JSXOpeningElement: (node) => checkNode$13(context, node),
1180
+ JSXOpeningElement: (node) => checkNode$13(context, node, opts),
715
1181
  CallExpression: callExpressionVisitor
716
1182
  };
717
1183
  }
@@ -4583,7 +5049,7 @@ var package_exports = /* @__PURE__ */ __exportAll({
4583
5049
  version: () => version$1
4584
5050
  });
4585
5051
  var name$1 = "eslint-plugin-formatjs";
4586
- var version$1 = "6.6.3";
5052
+ var version$1 = "6.7.0-rc.0";
4587
5053
  var description = "ESLint plugin for formatjs";
4588
5054
  var keywords = [
4589
5055
  "eslint",
@@ -4644,6 +5110,7 @@ const plugin = {
4644
5110
  [name$20]: rule$18,
4645
5111
  [name$19]: rule$17,
4646
5112
  [name$18]: rule$16,
5113
+ [name$23]: rule$21,
4647
5114
  [name$17]: rule$15,
4648
5115
  [name$16]: rule$14,
4649
5116
  [name$15]: rule$13,