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.
- package/index.js +94 -26
- package/index.js.map +1 -1
- 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
|
|
4
|
-
import
|
|
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
|
|
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")
|
|
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.
|
|
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
|
|
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
|
|
767
|
-
if (generic && parameters
|
|
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.
|
|
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 = "
|
|
5120
|
+
var version$1 = "7.0.0";
|
|
5053
5121
|
var description = "ESLint plugin for formatjs";
|
|
5054
5122
|
var keywords = [
|
|
5055
5123
|
"eslint",
|