eslint-plugin-formatjs 6.7.0-rc.0 → 7.0.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 +94 -26
  2. package/index.js.map +1 -1
  3. package/package.json +3 -3
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { TYPE, isArgumentElement, isDateElement, isLiteralElement, isNumberElement, isPluralElement, isSelectElement, isTagElement, isTimeElement, parse } from "@formatjs/icu-messageformat-parser";
2
2
  import { interpolateName } from "@formatjs/ts-transformer";
3
- import * as emojiPresentationRegex from "@unicode/unicode-17.0.0/Binary_Property/Emoji_Presentation/regex.js";
4
- import * as emojiPropertyRegex from "@unicode/unicode-17.0.0/Binary_Property/Emoji/regex.js";
3
+ import emojiPresentationRegexCompiled from "@unicode/unicode-17.0.0/Binary_Property/Emoji_Presentation/regex.mjs";
4
+ import emojiPropertyRegexCompiled from "@unicode/unicode-17.0.0/Binary_Property/Emoji/regex.mjs";
5
5
  import * as picomatchNs from "picomatch";
6
6
  import MagicString from "magic-string";
7
7
  //#region \0rolldown/runtime.js
@@ -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
  }
@@ -378,6 +378,8 @@ const modules = /* @__PURE__ */ new Set([
378
378
  "@formatjs/intl",
379
379
  "react-intl",
380
380
  "react-intl/server",
381
+ "@formatjs/svelte-intl",
382
+ "vue-intl",
381
383
  "intl-messageformat"
382
384
  ]);
383
385
  function staticObject(node) {
@@ -455,7 +457,13 @@ function renderType(node) {
455
457
  TSStringKeyword: "string"
456
458
  };
457
459
  if (keywords[node.type]) return keywords[node.type];
458
- if (node.type === "TSTypeReference" && node.typeName?.type === "Identifier") return node.typeName.name;
460
+ if (node.type === "TSTypeReference" && node.typeName?.type === "Identifier") {
461
+ const parameters = (node.typeArguments ?? node.typeParameters)?.params;
462
+ if (!parameters) return node.typeName.name;
463
+ const types = parameters.map(renderType);
464
+ if (types.every((type) => type !== void 0)) return node.typeName.name + "<" + types.join(", ") + ">";
465
+ return;
466
+ }
459
467
  if (node.type === "TSUnionType") {
460
468
  const types = node.types?.map(renderType);
461
469
  if (types?.every((type) => type !== void 0)) return types.join(" | ");
@@ -467,11 +475,11 @@ function renderType(node) {
467
475
  if (node.type === "TSTypeLiteral" && node.members) {
468
476
  const fields = [];
469
477
  for (const member of node.members) {
470
- if (member.type !== "TSPropertySignature" || member.readonly || member.computed) return;
478
+ if (member.type !== "TSPropertySignature" || member.computed) return;
471
479
  const key = member.key?.type === "Identifier" ? member.key.name : member.key?.value;
472
480
  const type = renderType(member.typeAnnotation?.typeAnnotation);
473
481
  if (key === void 0 || type === void 0) return;
474
- fields.push(`${JSON.stringify(String(key))}${member.optional ? "?" : ""}: ${type}`);
482
+ fields.push(`${member.readonly ? "readonly " : ""}${JSON.stringify(String(key))}${member.optional ? "?" : ""}: ${type}`);
475
483
  }
476
484
  return fields.length ? `{ ${fields.join("; ")} }` : "{}";
477
485
  }
@@ -482,7 +490,7 @@ function hoistTypes(context, node, contract) {
482
490
  const occupied = new Set(source.scopeManager?.scopes.flatMap((scope) => scope.variables.map((variable) => variable.name)));
483
491
  const resolved = /* @__PURE__ */ new Map();
484
492
  return {
485
- text: contract.replace(/import\(("(?:[^"\\]|\\.)*")\)\.(MessageTag|MessageValue)/g, (reference, quotedModule, imported) => {
493
+ text: contract.replace(/import\(("(?:[^"\\]|\\.)*")\)\.(MessageTag|MessageValue|TypedMessageDescriptor)/g, (reference, quotedModule, imported) => {
486
494
  if (resolved.has(reference)) return resolved.get(reference);
487
495
  const module = JSON.parse(quotedModule);
488
496
  for (const declaration of source.ast.body) {
@@ -519,10 +527,60 @@ function hoistTypes(context, node, contract) {
519
527
  }
520
528
  };
521
529
  }
530
+ function catalogAnnotation(context, node) {
531
+ const parent = node.parent;
532
+ if (parent?.type !== "VariableDeclarator" || parent.init !== node) return;
533
+ const annotation = parent.id.typeAnnotation?.typeAnnotation;
534
+ if (!annotation) return;
535
+ function imported(type, name) {
536
+ if (type.type !== "TSTypeReference" || type.typeName?.type !== "Identifier") return false;
537
+ let scope = context.sourceCode.getScope(node);
538
+ while (scope) {
539
+ const binding = scope.set.get(type.typeName.name);
540
+ if (binding) {
541
+ const definition = binding.defs[0];
542
+ 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;
543
+ }
544
+ scope = scope.upper;
545
+ }
546
+ return false;
547
+ }
548
+ function broad(type) {
549
+ if (type.type === "TSTypeLiteral") return !!type.members?.every((member) => member.type === "TSPropertySignature" && !!member.typeAnnotation && imported(member.typeAnnotation.typeAnnotation, "MessageDescriptor"));
550
+ let scope = context.sourceCode.getScope(node);
551
+ while (scope) {
552
+ if (scope.set.get(type.typeName?.name ?? "")?.defs.length) return false;
553
+ scope = scope.upper;
554
+ }
555
+ const parameters = (type.typeArguments ?? type.typeParameters)?.params;
556
+ if (type.typeName?.name === "Readonly" && parameters?.length === 1) return broad(parameters[0]);
557
+ return type.typeName?.name === "Record" && parameters?.length === 2 && imported(parameters[1], "MessageDescriptor");
558
+ }
559
+ function typedMap(type) {
560
+ return type.type === "TSTypeLiteral" && !!type.members?.every((member) => member.type === "TSPropertySignature" && !member.computed && !!member.typeAnnotation && imported(member.typeAnnotation.typeAnnotation, "TypedMessageDescriptor"));
561
+ }
562
+ let base = annotation;
563
+ let generated = typedMap(annotation) ? annotation : void 0;
564
+ if (annotation.type === "TSIntersectionType" && annotation.types?.length === 2) {
565
+ const [left, right] = annotation.types;
566
+ if (right.type === "TSTypeLiteral" && right.members?.every((member) => member.type === "TSPropertySignature" && !!member.typeAnnotation && imported(member.typeAnnotation.typeAnnotation, "TypedMessageDescriptor"))) {
567
+ base = left;
568
+ generated = right;
569
+ }
570
+ }
571
+ return {
572
+ annotation,
573
+ base,
574
+ generated,
575
+ supported: broad(base) || typedMap(base)
576
+ };
577
+ }
522
578
  const descriptorModules = /* @__PURE__ */ new Set([
523
579
  "@formatjs/intl",
524
580
  "react-intl",
525
- "react-intl/server"
581
+ "react-intl/server",
582
+ "@formatjs/svelte-intl",
583
+ "vue-intl"
526
584
  ]);
527
585
  function checkInlineMessage(context, node) {
528
586
  if (node.type !== "CallExpression") return;
@@ -623,6 +681,7 @@ const rule$20 = {
623
681
  contract: "Generate or refresh the ICU argument contract.",
624
682
  invalid: "Cannot generate message types: {{error}}.",
625
683
  manual: "Unsupported number of generic arguments; update the call manually.",
684
+ annotation: "Catalog annotation may erase ICU contracts; use per-message TypedMessageDescriptor types.",
626
685
  dynamic: "Typed messages require static message strings and parser options for contract verification."
627
686
  }
628
687
  },
@@ -742,14 +801,32 @@ const rule$20 = {
742
801
  });
743
802
  return;
744
803
  }
804
+ const annotation = imported.helper === "defineMessages" ? catalogAnnotation(context, node) : void 0;
805
+ if (annotation && !annotation.supported) {
806
+ context.report({
807
+ node: annotation.annotation,
808
+ messageId: "annotation"
809
+ });
810
+ return;
811
+ }
745
812
  let contract;
813
+ let catalogType;
746
814
  try {
747
815
  const types = messages.map(([message]) => messageTypes(parse(message.message.defaultMessage, { ignoreTag: getSettings(context).ignoreTag }), imported.module, context.options[0]?.ignoreList));
816
+ if (annotation) {
817
+ catalogType = "{ " + descriptor.properties.map((property, index) => {
818
+ if (property.type !== "Property") throw new Error("Unexpected spread");
819
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : void 0;
820
+ if (key === void 0) throw new Error("Unsupported catalog key");
821
+ return "readonly " + JSON.stringify(key) + ": import(" + JSON.stringify(imported.module) + ").TypedMessageDescriptor<" + types[index] + ">";
822
+ }).join("; ") + " }";
823
+ if (!descriptor.properties.length) catalogType = "{}";
824
+ }
748
825
  contract = imported.helper === "defineMessage" ? types[0] : descriptor.properties.length === 0 ? "{}" : `{ ${descriptor.properties.map((p, i) => {
749
826
  if (p.type !== "Property") throw new Error("Unexpected spread");
750
827
  const key = p.key.type === "Identifier" ? p.key.name : p.key.type === "Literal" ? String(p.key.value) : void 0;
751
828
  if (key === void 0) throw new Error("Unsupported catalog key");
752
- return `${JSON.stringify(key)}: ${types[i]}`;
829
+ return `readonly ${JSON.stringify(key)}: ${types[i]}`;
753
830
  }).join("; ")} }`;
754
831
  } catch (error) {
755
832
  context.report({
@@ -759,12 +836,13 @@ const rule$20 = {
759
836
  });
760
837
  return;
761
838
  }
762
- const imports = hoistTypes(context, node, contract);
763
- contract = imports.text;
839
+ const imports = hoistTypes(context, node, contract + (catalogType ? "\n" + catalogType : ""));
840
+ [contract, catalogType] = imports.text.split("\n");
841
+ const annotationMatches = !annotation || annotation.annotation === annotation.generated && renderType(annotation.generated) === catalogType;
764
842
  const expected = `<${contract}>`;
765
843
  const parameters = generic?.params;
766
- if (parameters?.length === 1 && renderType(parameters[0]) === contract && typed && !generated) return;
767
- if (generic && parameters?.length !== 1) {
844
+ if (parameters && parameters.length >= 1 && parameters.length <= 2 && renderType(parameters[0]) === contract && typed && !generated && annotationMatches) return;
845
+ if (generic && (!parameters || parameters.length < 1 || parameters.length > 2)) {
768
846
  context.report({
769
847
  node: generic,
770
848
  messageId: "manual"
@@ -775,7 +853,8 @@ const rule$20 = {
775
853
  node,
776
854
  messageId: "contract",
777
855
  fix(fixer) {
778
- const edits = [...imports.fix(fixer), generic ? fixer.replaceText(generic, expected) : fixer.insertTextAfter(node.optional ? source.getTokenAfter(node.callee) : node.callee, expected)];
856
+ 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)];
857
+ if (annotation && !annotationMatches) edits.push(fixer.replaceText(annotation.annotation, catalogType));
779
858
  if (!typed) {
780
859
  const close = source.getLastToken(node);
781
860
  const previous = source.getTokenBefore(close);
@@ -3987,17 +4066,6 @@ const EMOJI_RANGES = [
3987
4066
  */
3988
4067
  const graphemeSegmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
3989
4068
  /**
3990
- * Regex for detecting emoji using Unicode 17.0.0 Emoji_Presentation data
3991
- * Generated from @unicode/unicode-17.0.0/Binary_Property/Emoji_Presentation
3992
- * This avoids false positives from #, *, digits, and text symbols like ©
3993
- */
3994
- const emojiPresentationRegexCompiled = emojiPresentationRegex.default ?? emojiPresentationRegex;
3995
- /**
3996
- * Regex for detecting emoji-capable characters using Unicode 17.0.0 Emoji property
3997
- * This includes characters that can have emoji presentation (like ❤, ☀)
3998
- */
3999
- const emojiPropertyRegexCompiled = emojiPropertyRegex.default ?? emojiPropertyRegex;
4000
- /**
4001
4069
  * Check if a string contains any emoji
4002
4070
  * Uses Unicode 17.0.0 Emoji_Presentation data and variation selector (U+FE0F)
4003
4071
  * to properly detect emoji while avoiding false positives from characters like
@@ -5049,7 +5117,7 @@ var package_exports = /* @__PURE__ */ __exportAll({
5049
5117
  version: () => version$1
5050
5118
  });
5051
5119
  var name$1 = "eslint-plugin-formatjs";
5052
- var version$1 = "6.7.0-rc.0";
5120
+ var version$1 = "7.0.0";
5053
5121
  var description = "ESLint plugin for formatjs";
5054
5122
  var keywords = [
5055
5123
  "eslint",