eslint-plugin-formatjs 6.6.3 → 7.0.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 +666 -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 `readonly ${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,605 @@ 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") {
459
+ const parameters = (node.typeArguments ?? node.typeParameters)?.params;
460
+ if (!parameters) return node.typeName.name;
461
+ const types = parameters.map(renderType);
462
+ if (types.every((type) => type !== void 0)) return node.typeName.name + "<" + types.join(", ") + ">";
463
+ return;
464
+ }
465
+ if (node.type === "TSUnionType") {
466
+ const types = node.types?.map(renderType);
467
+ if (types?.every((type) => type !== void 0)) return types.join(" | ");
468
+ }
469
+ if (node.type === "TSImportType" && node.qualifier?.type === "Identifier") {
470
+ const module = node.source?.value ?? node.argument?.literal?.value ?? node.argument?.value;
471
+ if (typeof module === "string") return `import(${JSON.stringify(module)}).${node.qualifier.name}`;
472
+ }
473
+ if (node.type === "TSTypeLiteral" && node.members) {
474
+ const fields = [];
475
+ for (const member of node.members) {
476
+ if (member.type !== "TSPropertySignature" || member.computed) return;
477
+ const key = member.key?.type === "Identifier" ? member.key.name : member.key?.value;
478
+ const type = renderType(member.typeAnnotation?.typeAnnotation);
479
+ if (key === void 0 || type === void 0) return;
480
+ fields.push(`${member.readonly ? "readonly " : ""}${JSON.stringify(String(key))}${member.optional ? "?" : ""}: ${type}`);
481
+ }
482
+ return fields.length ? `{ ${fields.join("; ")} }` : "{}";
483
+ }
484
+ }
485
+ function hoistTypes(context, node, contract) {
486
+ const source = context.sourceCode;
487
+ const imports = /* @__PURE__ */ new Map();
488
+ const occupied = new Set(source.scopeManager?.scopes.flatMap((scope) => scope.variables.map((variable) => variable.name)));
489
+ const resolved = /* @__PURE__ */ new Map();
490
+ return {
491
+ text: contract.replace(/import\(("(?:[^"\\]|\\.)*")\)\.(MessageTag|MessageValue|TypedMessageDescriptor)/g, (reference, quotedModule, imported) => {
492
+ if (resolved.has(reference)) return resolved.get(reference);
493
+ const module = JSON.parse(quotedModule);
494
+ for (const declaration of source.ast.body) {
495
+ if (declaration.type !== "ImportDeclaration" || declaration.source.value !== module) continue;
496
+ for (const specifier of declaration.specifiers) {
497
+ if (specifier.type !== "ImportSpecifier") continue;
498
+ if ((specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value) !== imported) continue;
499
+ let scope = source.getScope(node);
500
+ while (scope && !scope.set.has(specifier.local.name)) scope = scope.upper;
501
+ if (scope?.set.get(specifier.local.name)?.defs.some((def) => def.node === specifier)) {
502
+ resolved.set(reference, specifier.local.name);
503
+ return specifier.local.name;
504
+ }
505
+ }
506
+ }
507
+ let local = imported;
508
+ for (let suffix = 1; occupied.has(local); suffix++) local = imported + suffix;
509
+ occupied.add(local);
510
+ const specifiers = imports.get(quotedModule) ?? [];
511
+ specifiers.push(imported + (local === imported ? "" : " as " + local));
512
+ imports.set(quotedModule, specifiers);
513
+ resolved.set(reference, local);
514
+ return local;
515
+ }),
516
+ fix(fixer) {
517
+ if (!imports.size) return [];
518
+ let anchor = source.ast.body.filter((statement) => statement.type === "ImportDeclaration").at(-1);
519
+ if (!anchor) for (const statement of source.ast.body) {
520
+ if (statement.type !== "ExpressionStatement" || statement.expression.type !== "Literal" || typeof statement.expression.value !== "string") break;
521
+ anchor = statement;
522
+ }
523
+ const block = [...imports].map(([module, specifiers]) => "import type {" + specifiers.join(", ") + "} from " + module + ";").join("\n");
524
+ return anchor ? [fixer.insertTextAfter(anchor, "\n" + block)] : [fixer.insertTextBefore(source.ast.body[0], block + "\n")];
525
+ }
526
+ };
527
+ }
528
+ function catalogAnnotation(context, node) {
529
+ const parent = node.parent;
530
+ if (parent?.type !== "VariableDeclarator" || parent.init !== node) return;
531
+ const annotation = parent.id.typeAnnotation?.typeAnnotation;
532
+ if (!annotation) return;
533
+ function imported(type, name) {
534
+ if (type.type !== "TSTypeReference" || type.typeName?.type !== "Identifier") return false;
535
+ let scope = context.sourceCode.getScope(node);
536
+ while (scope) {
537
+ const binding = scope.set.get(type.typeName.name);
538
+ if (binding) {
539
+ const definition = binding.defs[0];
540
+ return definition?.type === "ImportBinding" && descriptorModules.has(String(definition.parent.source.value)) && definition.node.type === "ImportSpecifier" && (definition.node.imported.type === "Identifier" ? definition.node.imported.name : definition.node.imported.value) === name;
541
+ }
542
+ scope = scope.upper;
543
+ }
544
+ return false;
545
+ }
546
+ function broad(type) {
547
+ if (type.type === "TSTypeLiteral") return !!type.members?.every((member) => member.type === "TSPropertySignature" && !!member.typeAnnotation && imported(member.typeAnnotation.typeAnnotation, "MessageDescriptor"));
548
+ let scope = context.sourceCode.getScope(node);
549
+ while (scope) {
550
+ if (scope.set.get(type.typeName?.name ?? "")?.defs.length) return false;
551
+ scope = scope.upper;
552
+ }
553
+ const parameters = (type.typeArguments ?? type.typeParameters)?.params;
554
+ if (type.typeName?.name === "Readonly" && parameters?.length === 1) return broad(parameters[0]);
555
+ return type.typeName?.name === "Record" && parameters?.length === 2 && imported(parameters[1], "MessageDescriptor");
556
+ }
557
+ function typedMap(type) {
558
+ return type.type === "TSTypeLiteral" && !!type.members?.every((member) => member.type === "TSPropertySignature" && !member.computed && !!member.typeAnnotation && imported(member.typeAnnotation.typeAnnotation, "TypedMessageDescriptor"));
559
+ }
560
+ let base = annotation;
561
+ let generated = typedMap(annotation) ? annotation : void 0;
562
+ if (annotation.type === "TSIntersectionType" && annotation.types?.length === 2) {
563
+ const [left, right] = annotation.types;
564
+ if (right.type === "TSTypeLiteral" && right.members?.every((member) => member.type === "TSPropertySignature" && !!member.typeAnnotation && imported(member.typeAnnotation.typeAnnotation, "TypedMessageDescriptor"))) {
565
+ base = left;
566
+ generated = right;
567
+ }
568
+ }
569
+ return {
570
+ annotation,
571
+ base,
572
+ generated,
573
+ supported: broad(base) || typedMap(base)
574
+ };
575
+ }
576
+ const descriptorModules = /* @__PURE__ */ new Set([
577
+ "@formatjs/intl",
578
+ "react-intl",
579
+ "react-intl/server"
580
+ ]);
581
+ function checkInlineMessage(context, node) {
582
+ if (node.type !== "CallExpression") return;
583
+ const settings = getSettings(context);
584
+ if (node.callee.type === "MemberExpression" && node.callee.computed) return;
585
+ if (!isIntlFormatMessageCall({
586
+ ...node,
587
+ arguments: [{
588
+ type: "ObjectExpression",
589
+ properties: []
590
+ }]
591
+ }, settings.additionalFunctionNames)) return;
592
+ const source = context.sourceCode;
593
+ const call = node;
594
+ const generic = call.typeArguments ?? call.typeParameters;
595
+ if (!generic && !context.options[0]?.generateTypes) return;
596
+ const generated = !!generic && /^<\s*\/\* @formatjs-generated \*\//.test(source.getText(generic));
597
+ const descriptor = node.arguments[0];
598
+ if (!staticObject(descriptor)) {
599
+ if (generic) context.report({
600
+ node,
601
+ messageId: "dynamic"
602
+ });
603
+ return;
604
+ }
605
+ const existingModule = source.ast.body.find((statement) => statement.type === "ImportDeclaration" && typeof statement.source.value === "string" && descriptorModules.has(statement.source.value));
606
+ const module = context.options[0]?.moduleSource ?? (existingModule?.type === "ImportDeclaration" ? String(existingModule.source.value) : "@formatjs/intl");
607
+ const ignoreTag = messageIgnoreTag(context, node);
608
+ if (ignoreTag === void 0) {
609
+ if (generic) context.report({
610
+ node,
611
+ messageId: "dynamic"
612
+ });
613
+ return !!generic;
614
+ }
615
+ const messages = extractMessages(node, settings);
616
+ const message = messages[0]?.[0];
617
+ if (messages.length !== 1 || !message || typeof message.message.defaultMessage !== "string" || !staticMessage(message.messageNode ?? void 0)) {
618
+ if (generic) context.report({
619
+ node,
620
+ messageId: "dynamic"
621
+ });
622
+ return;
623
+ }
624
+ let contract;
625
+ try {
626
+ contract = messageTypes(parse(message.message.defaultMessage, { ignoreTag }), module, context.options[0]?.ignoreList);
627
+ } catch (error) {
628
+ context.report({
629
+ node,
630
+ messageId: "invalid",
631
+ data: { error: error instanceof Error ? error.message : String(error) }
632
+ });
633
+ return true;
634
+ }
635
+ const imports = hoistTypes(context, node, contract);
636
+ contract = imports.text;
637
+ const parameters = generic?.params;
638
+ if (parameters && parameters.length >= 1 && parameters.length <= 2 && renderType(parameters[0]) === contract && !generated) return true;
639
+ if (generic && (!parameters || parameters.length > 2)) {
640
+ context.report({
641
+ node: generic,
642
+ messageId: "manual"
643
+ });
644
+ return true;
645
+ }
646
+ context.report({
647
+ node,
648
+ messageId: "contract",
649
+ fix(fixer) {
650
+ 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 + ">")];
651
+ }
652
+ });
653
+ return true;
654
+ }
655
+ const rule$20 = {
656
+ meta: {
657
+ type: "problem",
658
+ docs: { description: "Check ICU placeholders and optionally generate TypeScript argument contracts" },
659
+ fixable: "code",
660
+ schema: [{
661
+ type: "object",
662
+ properties: {
663
+ generateTypes: { type: "boolean" },
664
+ ignoreList: {
665
+ type: "array",
666
+ items: { type: "string" }
667
+ },
668
+ moduleSource: {
669
+ type: "string",
670
+ enum: [...descriptorModules]
671
+ }
672
+ },
673
+ additionalProperties: false
674
+ }],
675
+ messages: {
676
+ ...rule$21.meta.messages,
677
+ contract: "Generate or refresh the ICU argument contract.",
678
+ invalid: "Cannot generate message types: {{error}}.",
679
+ manual: "Unsupported number of generic arguments; update the call manually.",
680
+ annotation: "Catalog annotation may erase ICU contracts; use per-message TypedMessageDescriptor types.",
681
+ dynamic: "Typed messages require static message strings and parser options for contract verification."
682
+ }
683
+ },
684
+ create(context) {
685
+ const listeners = {
686
+ JSXOpeningElement(node) {
687
+ checkPlaceholders(context, node);
688
+ },
689
+ NewExpression(node) {
690
+ if (node.type !== "NewExpression" || !/\.[cm]?tsx?$/.test(context.filename)) return;
691
+ const imported = importedHelper(context, node);
692
+ if (imported?.helper !== "IntlMessageFormat") return;
693
+ const source = context.sourceCode;
694
+ const call = node;
695
+ const generic = call.typeArguments ?? call.typeParameters;
696
+ if (!generic && !context.options[0]?.generateTypes) return;
697
+ const generated = !!generic && /^<\s*\/\* @formatjs-generated \*\//.test(source.getText(generic));
698
+ const message = staticString(node.arguments[0]);
699
+ const options = node.arguments[3];
700
+ let ignoreTag = false;
701
+ if (message === void 0 || node.arguments.some((argument) => argument.type === "SpreadElement") || options && !staticObject(options)) {
702
+ if (generic) context.report({
703
+ node,
704
+ messageId: "dynamic"
705
+ });
706
+ return;
707
+ }
708
+ if (staticObject(options)) for (const property of options.properties) {
709
+ if (property.type !== "Property") continue;
710
+ if ((property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? property.key.value : void 0) !== "ignoreTag") continue;
711
+ if (property.value.type !== "Literal" || typeof property.value.value !== "boolean") {
712
+ if (generic) context.report({
713
+ node,
714
+ messageId: "dynamic"
715
+ });
716
+ return;
717
+ }
718
+ ignoreTag = property.value.value;
719
+ }
720
+ let contract;
721
+ try {
722
+ contract = messageTypes(parse(message, { ignoreTag }), imported.module, context.options[0]?.ignoreList);
723
+ } catch (error) {
724
+ context.report({
725
+ node,
726
+ messageId: "invalid",
727
+ data: { error: error instanceof Error ? error.message : String(error) }
728
+ });
729
+ return;
730
+ }
731
+ const imports = hoistTypes(context, node, contract);
732
+ contract = imports.text;
733
+ const parameters = generic?.params;
734
+ if (parameters?.length === 1 && renderType(parameters[0]) === contract && !generated) return;
735
+ if (generic && parameters?.length !== 1) {
736
+ context.report({
737
+ node: generic,
738
+ messageId: "manual"
739
+ });
740
+ return;
741
+ }
742
+ context.report({
743
+ node,
744
+ messageId: "contract",
745
+ fix(fixer) {
746
+ const expected = `<${contract}>`;
747
+ return [...imports.fix(fixer), generic ? fixer.replaceText(generic, expected) : fixer.insertTextAfter(node.callee, expected)];
748
+ }
749
+ });
750
+ },
751
+ CallExpression(node) {
752
+ if (node.type !== "CallExpression") return;
753
+ if (!/\.[cm]?tsx?$/.test(context.filename)) {
754
+ checkPlaceholders(context, node);
755
+ return;
756
+ }
757
+ const imported = importedHelper(context, node);
758
+ if (!imported) {
759
+ if (!checkInlineMessage(context, node)) checkPlaceholders(context, node);
760
+ return;
761
+ }
762
+ const source = context.sourceCode;
763
+ const call = node;
764
+ const generic = call.typeArguments ?? call.typeParameters;
765
+ const generated = !!generic && /^<\s*\/\* @formatjs-generated \*\//.test(source.getText(generic));
766
+ const options = node.arguments[1];
767
+ 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);
768
+ if (!typed && !generated && !context.options[0]?.generateTypes) return;
769
+ if (!typed && generic && !generated && !context.options[0]?.generateTypes) return;
770
+ if (node.arguments.length > 2 || options && !typed) return;
771
+ const descriptor = node.arguments[0];
772
+ if (!staticObject(descriptor)) {
773
+ if (typed || generated) context.report({
774
+ node,
775
+ messageId: "dynamic"
776
+ });
777
+ return;
778
+ }
779
+ if (imported.helper === "defineMessages" && !descriptor.properties.every((p) => p.type === "Property" && staticObject(p.value))) {
780
+ if (typed || generated) context.report({
781
+ node,
782
+ messageId: "dynamic"
783
+ });
784
+ return;
785
+ }
786
+ const messages = extractMessages({
787
+ ...node,
788
+ callee: {
789
+ type: "Identifier",
790
+ name: imported.helper
791
+ }
792
+ }, getSettings(context));
793
+ if (messages.some(([message]) => typeof message.message.defaultMessage !== "string" || !staticMessage(message.messageNode ?? void 0)) || messages.length !== (imported.helper === "defineMessage" ? 1 : descriptor.properties.length)) {
794
+ if (typed || generated) context.report({
795
+ node,
796
+ messageId: "dynamic"
797
+ });
798
+ return;
799
+ }
800
+ const annotation = imported.helper === "defineMessages" ? catalogAnnotation(context, node) : void 0;
801
+ if (annotation && !annotation.supported) {
802
+ context.report({
803
+ node: annotation.annotation,
804
+ messageId: "annotation"
805
+ });
806
+ return;
807
+ }
808
+ let contract;
809
+ let catalogType;
810
+ try {
811
+ const types = messages.map(([message]) => messageTypes(parse(message.message.defaultMessage, { ignoreTag: getSettings(context).ignoreTag }), imported.module, context.options[0]?.ignoreList));
812
+ if (annotation) {
813
+ catalogType = "{ " + descriptor.properties.map((property, index) => {
814
+ if (property.type !== "Property") throw new Error("Unexpected spread");
815
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : void 0;
816
+ if (key === void 0) throw new Error("Unsupported catalog key");
817
+ return "readonly " + JSON.stringify(key) + ": import(" + JSON.stringify(imported.module) + ").TypedMessageDescriptor<" + types[index] + ">";
818
+ }).join("; ") + " }";
819
+ if (!descriptor.properties.length) catalogType = "{}";
820
+ }
821
+ contract = imported.helper === "defineMessage" ? types[0] : descriptor.properties.length === 0 ? "{}" : `{ ${descriptor.properties.map((p, i) => {
822
+ if (p.type !== "Property") throw new Error("Unexpected spread");
823
+ const key = p.key.type === "Identifier" ? p.key.name : p.key.type === "Literal" ? String(p.key.value) : void 0;
824
+ if (key === void 0) throw new Error("Unsupported catalog key");
825
+ return `readonly ${JSON.stringify(key)}: ${types[i]}`;
826
+ }).join("; ")} }`;
827
+ } catch (error) {
828
+ context.report({
829
+ node,
830
+ messageId: "invalid",
831
+ data: { error: error instanceof Error ? error.message : String(error) }
832
+ });
833
+ return;
834
+ }
835
+ const imports = hoistTypes(context, node, contract + (catalogType ? "\n" + catalogType : ""));
836
+ [contract, catalogType] = imports.text.split("\n");
837
+ const annotationMatches = !annotation || annotation.annotation === annotation.generated && renderType(annotation.generated) === catalogType;
838
+ const expected = `<${contract}>`;
839
+ const parameters = generic?.params;
840
+ if (parameters && parameters.length >= 1 && parameters.length <= 2 && renderType(parameters[0]) === contract && typed && !generated && annotationMatches) return;
841
+ if (generic && (!parameters || parameters.length < 1 || parameters.length > 2)) {
842
+ context.report({
843
+ node: generic,
844
+ messageId: "manual"
845
+ });
846
+ return;
847
+ }
848
+ context.report({
849
+ node,
850
+ messageId: "contract",
851
+ fix(fixer) {
852
+ const edits = [...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, expected)];
853
+ if (annotation && !annotationMatches) edits.push(fixer.replaceText(annotation.annotation, catalogType));
854
+ if (!typed) {
855
+ const close = source.getLastToken(node);
856
+ const previous = source.getTokenBefore(close);
857
+ edits.push(fixer.insertTextBefore(close, `${previous.value === "," ? "" : ","} {typed: true}`));
858
+ }
859
+ return edits;
860
+ }
861
+ });
862
+ }
863
+ };
864
+ const parserServices = context.sourceCode.parserServices;
865
+ if (parserServices?.defineTemplateBodyVisitor) return parserServices.defineTemplateBodyVisitor({ CallExpression: listeners.CallExpression }, { CallExpression: listeners.CallExpression });
866
+ return listeners;
867
+ }
868
+ };
869
+ //#endregion
226
870
  //#region packages/eslint-plugin-formatjs/rules/blocklist-elements.ts
227
- const name$22 = "blocklist-elements";
871
+ const name$21 = "blocklist-elements";
228
872
  function getMessage(type) {
229
873
  return {
230
874
  messageId: "blocklist",
@@ -277,7 +921,7 @@ function verifyAst$7(blocklist, ast) {
277
921
  }
278
922
  return errors;
279
923
  }
280
- function checkNode$17(context, node) {
924
+ function checkNode$16(context, node) {
281
925
  const settings = getSettings(context);
282
926
  const msgs = extractMessages(node, settings);
283
927
  if (!msgs.length) return;
@@ -303,7 +947,7 @@ function checkNode$17(context, node) {
303
947
  });
304
948
  }
305
949
  }
306
- const rule$20 = {
950
+ const rule$19 = {
307
951
  meta: {
308
952
  type: "problem",
309
953
  docs: {
@@ -347,11 +991,11 @@ const rule$20 = {
347
991
  }
348
992
  },
349
993
  create(context) {
350
- const callExpressionVisitor = (node) => checkNode$17(context, node);
994
+ const callExpressionVisitor = (node) => checkNode$16(context, node);
351
995
  const parserServices = context.sourceCode.parserServices;
352
996
  if (parserServices?.defineTemplateBodyVisitor) return parserServices.defineTemplateBodyVisitor({ CallExpression: callExpressionVisitor }, { CallExpression: callExpressionVisitor });
353
997
  return {
354
- JSXOpeningElement: (node) => checkNode$17(context, node),
998
+ JSXOpeningElement: (node) => checkNode$16(context, node),
355
999
  CallExpression: callExpressionVisitor
356
1000
  };
357
1001
  }
@@ -363,8 +1007,8 @@ let Option$1 = /* @__PURE__ */ function(Option) {
363
1007
  Option["anything"] = "anything";
364
1008
  return Option;
365
1009
  }({});
366
- const name$21 = "enforce-default-message";
367
- function checkNode$16(context, node) {
1010
+ const name$20 = "enforce-default-message";
1011
+ function checkNode$15(context, node) {
368
1012
  const msgs = extractMessages(node, getSettings(context));
369
1013
  const { options: [type] } = context;
370
1014
  for (const [{ message: { defaultMessage }, messageNode, messageDescriptorNode }] of msgs) if (!defaultMessage) {
@@ -378,7 +1022,7 @@ function checkNode$16(context, node) {
378
1022
  });
379
1023
  }
380
1024
  }
381
- const rule$19 = {
1025
+ const rule$18 = {
382
1026
  meta: {
383
1027
  type: "problem",
384
1028
  docs: {
@@ -398,11 +1042,11 @@ const rule$19 = {
398
1042
  }
399
1043
  },
400
1044
  create(context) {
401
- const callExpressionVisitor = (node) => checkNode$16(context, node);
1045
+ const callExpressionVisitor = (node) => checkNode$15(context, node);
402
1046
  const parserServices = context.sourceCode.parserServices;
403
1047
  if (parserServices?.defineTemplateBodyVisitor) return parserServices.defineTemplateBodyVisitor({ CallExpression: callExpressionVisitor }, { CallExpression: callExpressionVisitor });
404
1048
  return {
405
- JSXOpeningElement: (node) => checkNode$16(context, node),
1049
+ JSXOpeningElement: (node) => checkNode$15(context, node),
406
1050
  CallExpression: callExpressionVisitor
407
1051
  };
408
1052
  }
@@ -428,7 +1072,7 @@ function normalizeOptions(raw) {
428
1072
  minLength: void 0
429
1073
  };
430
1074
  }
431
- function checkNode$15(context, node) {
1075
+ function checkNode$14(context, node) {
432
1076
  const msgs = extractMessages(node, getSettings(context));
433
1077
  const { mode: type, minLength } = normalizeOptions(context.options[0]);
434
1078
  for (const [{ message: { description }, descriptionNode, messageDescriptorNode }] of msgs) if (!description) {
@@ -449,8 +1093,8 @@ function checkNode$15(context, node) {
449
1093
  }
450
1094
  });
451
1095
  }
452
- const name$20 = "enforce-description";
453
- const rule$18 = {
1096
+ const name$19 = "enforce-description";
1097
+ const rule$17 = {
454
1098
  meta: {
455
1099
  type: "problem",
456
1100
  docs: {
@@ -483,18 +1127,18 @@ const rule$18 = {
483
1127
  }
484
1128
  },
485
1129
  create(context) {
486
- const callExpressionVisitor = (node) => checkNode$15(context, node);
1130
+ const callExpressionVisitor = (node) => checkNode$14(context, node);
487
1131
  const parserServices = context.sourceCode.parserServices;
488
1132
  if (parserServices?.defineTemplateBodyVisitor) return parserServices.defineTemplateBodyVisitor({ CallExpression: callExpressionVisitor }, { CallExpression: callExpressionVisitor });
489
1133
  return {
490
- JSXOpeningElement: (node) => checkNode$15(context, node),
1134
+ JSXOpeningElement: (node) => checkNode$14(context, node),
491
1135
  CallExpression: callExpressionVisitor
492
1136
  };
493
1137
  }
494
1138
  };
495
1139
  //#endregion
496
1140
  //#region packages/eslint-plugin-formatjs/rules/enforce-id.ts
497
- function checkNode$14(context, node, { idInterpolationPattern, idWhitelistRegexps, quoteStyle }) {
1141
+ function checkNode$13(context, node, { idInterpolationPattern, idWhitelistRegexps, quoteStyle }) {
498
1142
  const msgs = extractMessages(node, getSettings(context));
499
1143
  for (const [{ message: { defaultMessage, description, id }, idPropNode, descriptionNode, messagePropNode, messageDescriptorNode }] of msgs) {
500
1144
  if (!idInterpolationPattern && !idPropNode) {
@@ -552,8 +1196,8 @@ function checkNode$14(context, node, { idInterpolationPattern, idWhitelistRegexp
552
1196
  }
553
1197
  }
554
1198
  }
555
- const name$19 = "enforce-id";
556
- const rule$17 = {
1199
+ const name$18 = "enforce-id";
1200
+ const rule$16 = {
557
1201
  meta: {
558
1202
  type: "problem",
559
1203
  docs: {
@@ -604,114 +1248,11 @@ Actual: {{actual}}`
604
1248
  const { idWhitelist } = tmp;
605
1249
  opts.idWhitelistRegexps = idWhitelist.map((str) => new RegExp(str, "i"));
606
1250
  }
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);
1251
+ const callExpressionVisitor = (node) => checkNode$13(context, node, opts);
711
1252
  const parserServices = context.sourceCode.parserServices;
712
1253
  if (parserServices?.defineTemplateBodyVisitor) return parserServices.defineTemplateBodyVisitor({ CallExpression: callExpressionVisitor }, { CallExpression: callExpressionVisitor });
713
1254
  return {
714
- JSXOpeningElement: (node) => checkNode$13(context, node),
1255
+ JSXOpeningElement: (node) => checkNode$13(context, node, opts),
715
1256
  CallExpression: callExpressionVisitor
716
1257
  };
717
1258
  }
@@ -4583,7 +5124,7 @@ var package_exports = /* @__PURE__ */ __exportAll({
4583
5124
  version: () => version$1
4584
5125
  });
4585
5126
  var name$1 = "eslint-plugin-formatjs";
4586
- var version$1 = "6.6.3";
5127
+ var version$1 = "7.0.0-rc.0";
4587
5128
  var description = "ESLint plugin for formatjs";
4588
5129
  var keywords = [
4589
5130
  "eslint",
@@ -4644,6 +5185,7 @@ const plugin = {
4644
5185
  [name$20]: rule$18,
4645
5186
  [name$19]: rule$17,
4646
5187
  [name$18]: rule$16,
5188
+ [name$23]: rule$21,
4647
5189
  [name$17]: rule$15,
4648
5190
  [name$16]: rule$14,
4649
5191
  [name$15]: rule$13,