eslint-plugin-formatjs 6.7.0-rc.0 → 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 +87 -12
  2. package/index.js.map +1 -1
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -61,7 +61,7 @@ function messageTypes(ast, module, ignoreList = []) {
61
61
  else if (types.has("date")) type = "number | Date";
62
62
  else if (types.has("select")) type = "string";
63
63
  else type = `import(${JSON.stringify(module)}).MessageTag`;
64
- return `${JSON.stringify(name)}${ignored.has(name) ? "?" : ""}: ${type}`;
64
+ return `readonly ${JSON.stringify(name)}${ignored.has(name) ? "?" : ""}: ${type}`;
65
65
  });
66
66
  return fields.length ? `{ ${fields.join("; ")} }` : "{}";
67
67
  }
@@ -455,7 +455,13 @@ function renderType(node) {
455
455
  TSStringKeyword: "string"
456
456
  };
457
457
  if (keywords[node.type]) return keywords[node.type];
458
- if (node.type === "TSTypeReference" && node.typeName?.type === "Identifier") return node.typeName.name;
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
+ }
459
465
  if (node.type === "TSUnionType") {
460
466
  const types = node.types?.map(renderType);
461
467
  if (types?.every((type) => type !== void 0)) return types.join(" | ");
@@ -467,11 +473,11 @@ function renderType(node) {
467
473
  if (node.type === "TSTypeLiteral" && node.members) {
468
474
  const fields = [];
469
475
  for (const member of node.members) {
470
- if (member.type !== "TSPropertySignature" || member.readonly || member.computed) return;
476
+ if (member.type !== "TSPropertySignature" || member.computed) return;
471
477
  const key = member.key?.type === "Identifier" ? member.key.name : member.key?.value;
472
478
  const type = renderType(member.typeAnnotation?.typeAnnotation);
473
479
  if (key === void 0 || type === void 0) return;
474
- fields.push(`${JSON.stringify(String(key))}${member.optional ? "?" : ""}: ${type}`);
480
+ fields.push(`${member.readonly ? "readonly " : ""}${JSON.stringify(String(key))}${member.optional ? "?" : ""}: ${type}`);
475
481
  }
476
482
  return fields.length ? `{ ${fields.join("; ")} }` : "{}";
477
483
  }
@@ -482,7 +488,7 @@ function hoistTypes(context, node, contract) {
482
488
  const occupied = new Set(source.scopeManager?.scopes.flatMap((scope) => scope.variables.map((variable) => variable.name)));
483
489
  const resolved = /* @__PURE__ */ new Map();
484
490
  return {
485
- text: contract.replace(/import\(("(?:[^"\\]|\\.)*")\)\.(MessageTag|MessageValue)/g, (reference, quotedModule, imported) => {
491
+ text: contract.replace(/import\(("(?:[^"\\]|\\.)*")\)\.(MessageTag|MessageValue|TypedMessageDescriptor)/g, (reference, quotedModule, imported) => {
486
492
  if (resolved.has(reference)) return resolved.get(reference);
487
493
  const module = JSON.parse(quotedModule);
488
494
  for (const declaration of source.ast.body) {
@@ -519,6 +525,54 @@ function hoistTypes(context, node, contract) {
519
525
  }
520
526
  };
521
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
+ }
522
576
  const descriptorModules = /* @__PURE__ */ new Set([
523
577
  "@formatjs/intl",
524
578
  "react-intl",
@@ -623,6 +677,7 @@ const rule$20 = {
623
677
  contract: "Generate or refresh the ICU argument contract.",
624
678
  invalid: "Cannot generate message types: {{error}}.",
625
679
  manual: "Unsupported number of generic arguments; update the call manually.",
680
+ annotation: "Catalog annotation may erase ICU contracts; use per-message TypedMessageDescriptor types.",
626
681
  dynamic: "Typed messages require static message strings and parser options for contract verification."
627
682
  }
628
683
  },
@@ -742,14 +797,32 @@ const rule$20 = {
742
797
  });
743
798
  return;
744
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
+ }
745
808
  let contract;
809
+ let catalogType;
746
810
  try {
747
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
+ }
748
821
  contract = imported.helper === "defineMessage" ? types[0] : descriptor.properties.length === 0 ? "{}" : `{ ${descriptor.properties.map((p, i) => {
749
822
  if (p.type !== "Property") throw new Error("Unexpected spread");
750
823
  const key = p.key.type === "Identifier" ? p.key.name : p.key.type === "Literal" ? String(p.key.value) : void 0;
751
824
  if (key === void 0) throw new Error("Unsupported catalog key");
752
- return `${JSON.stringify(key)}: ${types[i]}`;
825
+ return `readonly ${JSON.stringify(key)}: ${types[i]}`;
753
826
  }).join("; ")} }`;
754
827
  } catch (error) {
755
828
  context.report({
@@ -759,12 +832,13 @@ const rule$20 = {
759
832
  });
760
833
  return;
761
834
  }
762
- const imports = hoistTypes(context, node, contract);
763
- contract = imports.text;
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;
764
838
  const expected = `<${contract}>`;
765
839
  const parameters = generic?.params;
766
- if (parameters?.length === 1 && renderType(parameters[0]) === contract && typed && !generated) return;
767
- if (generic && parameters?.length !== 1) {
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)) {
768
842
  context.report({
769
843
  node: generic,
770
844
  messageId: "manual"
@@ -775,7 +849,8 @@ const rule$20 = {
775
849
  node,
776
850
  messageId: "contract",
777
851
  fix(fixer) {
778
- const edits = [...imports.fix(fixer), generic ? fixer.replaceText(generic, expected) : fixer.insertTextAfter(node.optional ? source.getTokenAfter(node.callee) : node.callee, expected)];
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));
779
854
  if (!typed) {
780
855
  const close = source.getLastToken(node);
781
856
  const previous = source.getTokenBefore(close);
@@ -5049,7 +5124,7 @@ var package_exports = /* @__PURE__ */ __exportAll({
5049
5124
  version: () => version$1
5050
5125
  });
5051
5126
  var name$1 = "eslint-plugin-formatjs";
5052
- var version$1 = "6.7.0-rc.0";
5127
+ var version$1 = "7.0.0-rc.0";
5053
5128
  var description = "ESLint plugin for formatjs";
5054
5129
  var keywords = [
5055
5130
  "eslint",