eslint-plugin-formatjs 8.0.5 → 8.1.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 -7
  2. package/index.js.map +1 -1
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -4952,9 +4952,60 @@ const rule$4 = {
4952
4952
  //#endregion
4953
4953
  //#region packages/eslint-plugin-formatjs/rules/prefer-formatted-message.ts
4954
4954
  const name$5 = "prefer-formatted-message";
4955
+ function binding(context, node, name) {
4956
+ let scope = context.sourceCode.getScope(node);
4957
+ while (scope) {
4958
+ const variable = scope.set.get(name);
4959
+ if (variable) return variable;
4960
+ scope = scope.upper;
4961
+ }
4962
+ }
4963
+ function importedName(specifier) {
4964
+ return specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
4965
+ }
4966
+ function isHookFormatter(context, callee) {
4967
+ const identifier = callee.type === "Identifier" ? callee : callee.type === "MemberExpression" && !callee.computed && !callee.optional && callee.property.type === "Identifier" && [
4968
+ "formatMessage",
4969
+ "$formatMessage",
4970
+ "$t"
4971
+ ].includes(callee.property.name) && callee.object.type === "Identifier" ? callee.object : void 0;
4972
+ if (!identifier) return false;
4973
+ const variable = binding(context, callee, identifier.name);
4974
+ const definition = variable?.defs[0];
4975
+ if (definition?.type !== "Variable" || definition.parent?.kind !== "const" || variable?.references.some((reference) => reference.isWrite() && !reference.init)) return false;
4976
+ const { id, init } = definition.node;
4977
+ if (init?.type !== "CallExpression" || init.optional || init.arguments.length || init.callee.type !== "Identifier") return false;
4978
+ const hook = binding(context, init, init.callee.name)?.defs[0];
4979
+ if (hook?.type !== "ImportBinding" || hook.node.type !== "ImportSpecifier" || importedName(hook.node) !== "useIntl" || hook.node.importKind === "type" || hook.parent.importKind === "type" || hook.parent.source.value !== "react-intl") return false;
4980
+ if (callee.type === "MemberExpression") return id.type === "Identifier";
4981
+ return id.type === "ObjectPattern" && id.properties.some((property) => property.type === "Property" && !property.computed && property.key.type === "Identifier" && [
4982
+ "formatMessage",
4983
+ "$formatMessage",
4984
+ "$t"
4985
+ ].includes(property.key.name) && property.value.type === "Identifier" && property.value.name === identifier.name);
4986
+ }
4987
+ function component(context, node) {
4988
+ const source = context.sourceCode;
4989
+ for (const declaration of source.ast.body) {
4990
+ if (declaration.type !== "ImportDeclaration" || declaration.source.value !== "react-intl" || declaration.importKind === "type") continue;
4991
+ for (const specifier of declaration.specifiers) if (specifier.type === "ImportSpecifier" && importedName(specifier) === "FormattedMessage" && /^[A-Z]/.test(specifier.local.name) && specifier.importKind !== "type" && binding(context, node, specifier.local.name)?.defs.some((definition) => definition.node === specifier)) return { name: specifier.local.name };
4992
+ }
4993
+ const occupied = new Set(source.scopeManager?.scopes.flatMap((scope) => [...scope.variables.map((variable) => variable.name), ...scope.references.map((reference) => reference.identifier.name)]));
4994
+ let local = "FormattedMessage";
4995
+ for (let suffix = 1; occupied.has(local); suffix++) local = "FormattedMessage" + suffix;
4996
+ const specifier = local === "FormattedMessage" ? local : "FormattedMessage as " + local;
4997
+ const declaration = source.ast.body.find((statement) => statement.type === "ImportDeclaration" && statement.source.value === "react-intl" && statement.importKind !== "type" && statement.specifiers.some((specifier) => specifier.type === "ImportSpecifier"));
4998
+ if (declaration?.type !== "ImportDeclaration") return;
4999
+ const anchor = declaration.specifiers.find((specifier) => specifier.type === "ImportSpecifier");
5000
+ return {
5001
+ name: local,
5002
+ insert: (fixer) => fixer.insertTextBefore(anchor, specifier + ", ")
5003
+ };
5004
+ }
4955
5005
  const rule$3 = {
4956
5006
  meta: {
4957
5007
  type: "suggestion",
5008
+ fixable: "code",
4958
5009
  docs: {
4959
5010
  description: "Prefer `FormattedMessage` component over `intl.formatMessage` if applicable.",
4960
5011
  url: "https://formatjs.github.io/docs/tooling/linter#prefer-formatted-message"
@@ -4964,15 +5015,44 @@ const rule$3 = {
4964
5015
  },
4965
5016
  create(context) {
4966
5017
  const { additionalFunctionNames } = getSettings(context);
4967
- return { JSXElement: (node) => {
4968
- node.children.forEach((child) => {
4969
- if (child.type !== "JSXExpressionContainer" || !isIntlFormatMessageCall(child.expression, additionalFunctionNames)) return;
5018
+ const source = context.sourceCode;
5019
+ function checkChildren(node) {
5020
+ for (const child of node.children) {
5021
+ if (child.type !== "JSXExpressionContainer" || !isIntlFormatMessageCall(child.expression, additionalFunctionNames)) continue;
4970
5022
  context.report({
4971
5023
  node: child,
4972
- messageId: "jsxChildren"
5024
+ messageId: "jsxChildren",
5025
+ fix(fixer) {
5026
+ const call = child.expression;
5027
+ if (call.type !== "CallExpression" || call.optional || call.arguments.length > 2 || !isHookFormatter(context, call.callee) || source.getCommentsInside(child).length || call.typeArguments || call.typeParameters) return null;
5028
+ const [descriptor, values] = call.arguments;
5029
+ if (descriptor?.type !== "ObjectExpression" || values?.type === "SpreadElement") return null;
5030
+ const attributes = [];
5031
+ const keys = /* @__PURE__ */ new Set();
5032
+ for (const property of descriptor.properties) {
5033
+ if (property.type !== "Property" || property.computed || property.method || property.kind !== "init") return null;
5034
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? property.key.value : void 0;
5035
+ if (typeof key !== "string" || ![
5036
+ "id",
5037
+ "defaultMessage",
5038
+ "description"
5039
+ ].includes(key) || keys.has(key)) return null;
5040
+ keys.add(key);
5041
+ attributes.push(key + "={(" + source.getText(property.value) + ")}");
5042
+ }
5043
+ if (values) attributes.push("values={(" + source.getText(values) + ")}");
5044
+ const target = component(context, child);
5045
+ if (!target) return null;
5046
+ const replacement = "<" + target.name + (attributes.length ? " " + attributes.join(" ") : "") + " />";
5047
+ return [...target.insert ? [target.insert(fixer)] : [], fixer.replaceText(child, replacement)];
5048
+ }
4973
5049
  });
4974
- });
4975
- } };
5050
+ }
5051
+ }
5052
+ return {
5053
+ JSXElement: checkChildren,
5054
+ JSXFragment: checkChildren
5055
+ };
4976
5056
  }
4977
5057
  };
4978
5058
  //#endregion
@@ -5269,7 +5349,7 @@ var package_exports = /* @__PURE__ */ __exportAll({
5269
5349
  version: () => version$1
5270
5350
  });
5271
5351
  var name$1 = "eslint-plugin-formatjs";
5272
- var version$1 = "8.0.5";
5352
+ var version$1 = "8.1.0";
5273
5353
  var description = "ESLint plugin for formatjs";
5274
5354
  var keywords = [
5275
5355
  "eslint",