eslint-plugin-formatjs 6.4.20 → 6.6.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 CHANGED
@@ -119,7 +119,6 @@ function extractMessageDescriptor(node) {
119
119
  result.message.id = value;
120
120
  result.idValueNode = valueNode;
121
121
  result.idPropNode = prop;
122
- break;
123
122
  }
124
123
  }
125
124
  return result;
@@ -166,9 +165,7 @@ function extractMessageDescriptorFromJSXElement(node) {
166
165
  result.idPropNode = prop;
167
166
  if (value) result.message.id = value;
168
167
  break;
169
- case "values":
170
- if (valueNode?.type === "JSXExpressionContainer" && valueNode.expression.type === "ObjectExpression") values = valueNode.expression;
171
- break;
168
+ case "values": if (valueNode?.type === "JSXExpressionContainer" && valueNode.expression.type === "ObjectExpression") values = valueNode.expression;
172
169
  }
173
170
  }
174
171
  if (!result.messagePropNode && !result.descriptionNode && !result.idPropNode && hasSpreadAttribute) return;
@@ -237,23 +234,36 @@ let Element = /* @__PURE__ */ function(Element) {
237
234
  Element["tag"] = "tag";
238
235
  return Element;
239
236
  }({});
237
+ function isBlocklisted(blocklist, type, element) {
238
+ return blocklist.some((entry) => {
239
+ if (typeof entry === "string") return entry === type;
240
+ if (entry.type !== type) return false;
241
+ if (!("value" in element) || element.value !== entry.allow.variable) return true;
242
+ const allowedOptions = entry.allow.options;
243
+ if (!allowedOptions) return false;
244
+ if (!isSelectElement(element) && !isPluralElement(element)) return true;
245
+ const options = Object.keys(element.options);
246
+ return options.length !== allowedOptions.length || options.some((option) => !allowedOptions.includes(option));
247
+ });
248
+ }
240
249
  function verifyAst$7(blocklist, ast) {
241
250
  const errors = [];
242
251
  for (const el of ast) {
243
- if (isLiteralElement(el) && blocklist.includes("literal")) errors.push(getMessage("literal"));
244
- if (isArgumentElement(el) && blocklist.includes("argument")) errors.push(getMessage("argument"));
245
- if (isNumberElement(el) && blocklist.includes("number")) errors.push(getMessage("number"));
246
- if (isDateElement(el) && blocklist.includes("date")) errors.push(getMessage("date"));
247
- if (isTimeElement(el) && blocklist.includes("time")) errors.push(getMessage("time"));
248
- if (isSelectElement(el) && blocklist.includes("select")) errors.push(getMessage("select"));
249
- if (isTagElement(el) && blocklist.includes("tag")) errors.push(getMessage("tag"));
252
+ if (isLiteralElement(el) && isBlocklisted(blocklist, "literal", el)) errors.push(getMessage("literal"));
253
+ if (isArgumentElement(el) && isBlocklisted(blocklist, "argument", el)) errors.push(getMessage("argument"));
254
+ if (isNumberElement(el) && isBlocklisted(blocklist, "number", el)) errors.push(getMessage("number"));
255
+ if (isDateElement(el) && isBlocklisted(blocklist, "date", el)) errors.push(getMessage("date"));
256
+ if (isTimeElement(el) && isBlocklisted(blocklist, "time", el)) errors.push(getMessage("time"));
257
+ if (isSelectElement(el) && isBlocklisted(blocklist, "select", el)) errors.push(getMessage("select"));
258
+ if (isTagElement(el) && isBlocklisted(blocklist, "tag", el)) errors.push(getMessage("tag"));
259
+ if (isTagElement(el)) errors.push(...verifyAst$7(blocklist, el.children));
250
260
  if (isPluralElement(el)) {
251
- if (blocklist.includes("plural")) errors.push(getMessage("argument"));
252
- if (el.pluralType === "ordinal" && blocklist.includes("selectordinal")) errors.push(getMessage("selectordinal"));
261
+ if (isBlocklisted(blocklist, "plural", el)) errors.push(getMessage("argument"));
262
+ if (el.pluralType === "ordinal" && isBlocklisted(blocklist, "selectordinal", el)) errors.push(getMessage("selectordinal"));
253
263
  }
254
264
  if (isSelectElement(el) || isPluralElement(el)) {
255
265
  const { options } = el;
256
- for (const selector of Object.keys(options)) verifyAst$7(blocklist, options[selector].value);
266
+ for (const selector of Object.keys(options)) errors.push(...verifyAst$7(blocklist, options[selector].value));
257
267
  }
258
268
  }
259
269
  return errors;
@@ -294,10 +304,33 @@ const rule$20 = {
294
304
  fixable: "code",
295
305
  schema: [{
296
306
  type: "array",
297
- items: {
307
+ items: { oneOf: [{
298
308
  type: "string",
299
309
  enum: Object.keys(Element)
300
- }
310
+ }, {
311
+ type: "object",
312
+ additionalProperties: false,
313
+ required: ["type", "allow"],
314
+ properties: {
315
+ type: {
316
+ type: "string",
317
+ enum: Object.keys(Element)
318
+ },
319
+ allow: {
320
+ type: "object",
321
+ additionalProperties: false,
322
+ required: ["variable"],
323
+ properties: {
324
+ variable: { type: "string" },
325
+ options: {
326
+ type: "array",
327
+ uniqueItems: true,
328
+ items: { type: "string" }
329
+ }
330
+ }
331
+ }
332
+ }
333
+ }] }
301
334
  }],
302
335
  messages: {
303
336
  ...CORE_MESSAGES,
@@ -462,48 +495,50 @@ function checkNode$14(context, node, { idInterpolationPattern, idWhitelistRegexp
462
495
  });
463
496
  return;
464
497
  }
465
- if (idInterpolationPattern) if (!defaultMessage) context.report({
466
- node: messageDescriptorNode ?? node,
467
- messageId: "enforceIdDefaultMessage"
468
- });
469
- else if (!description && descriptionNode) context.report({
470
- node: messageDescriptorNode ?? node,
471
- messageId: "enforceIdDescription"
472
- });
473
- else {
474
- if (idWhitelistRegexps && id && idWhitelistRegexps.some((r) => r.test(id))) continue;
475
- const correctId = interpolateName({ resourcePath: context.filename }, idInterpolationPattern, { content: description ? `${defaultMessage}#${description}` : defaultMessage });
476
- if (id !== correctId) {
477
- let messageId = "enforceIdMatching";
478
- let messageData = {
479
- idInterpolationPattern,
480
- expected: correctId,
481
- actual: id
482
- };
483
- if (idWhitelistRegexps) {
484
- messageId = "enforceIdMatchingAllowlisted";
485
- messageData = {
486
- ...messageData,
487
- idWhitelist: idWhitelistRegexps.map((r) => `"${r.toString()}"`).join(", ")
498
+ if (idInterpolationPattern) {
499
+ if (!defaultMessage) context.report({
500
+ node: messageDescriptorNode ?? node,
501
+ messageId: "enforceIdDefaultMessage"
502
+ });
503
+ else if (!description && descriptionNode) context.report({
504
+ node: messageDescriptorNode ?? node,
505
+ messageId: "enforceIdDescription"
506
+ });
507
+ else {
508
+ if (idWhitelistRegexps && id && idWhitelistRegexps.some((r) => r.test(id))) continue;
509
+ const correctId = interpolateName({ resourcePath: context.filename }, idInterpolationPattern, { content: description ? `${defaultMessage}#${description}` : defaultMessage });
510
+ if (id !== correctId) {
511
+ let messageId = "enforceIdMatching";
512
+ let messageData = {
513
+ idInterpolationPattern,
514
+ expected: correctId,
515
+ actual: id
488
516
  };
489
- }
490
- const quote = quoteStyle === "double" ? "\"" : "'";
491
- context.report({
492
- node: idPropNode ?? messageDescriptorNode ?? node,
493
- messageId,
494
- data: messageData,
495
- fix(fixer) {
496
- if (idPropNode) {
497
- if (idPropNode.type === "JSXAttribute") return fixer.replaceText(idPropNode, `id="${correctId}"`);
498
- return fixer.replaceText(idPropNode, `id: ${quote}${correctId}${quote}`);
499
- }
500
- if (messagePropNode) {
501
- if (messagePropNode.type === "JSXAttribute") return fixer.insertTextAfter(messagePropNode, ` id="${correctId}"`);
502
- return fixer.insertTextAfter(messagePropNode, `, id: ${quote}${correctId}${quote}`);
503
- }
504
- return null;
517
+ if (idWhitelistRegexps) {
518
+ messageId = "enforceIdMatchingAllowlisted";
519
+ messageData = {
520
+ ...messageData,
521
+ idWhitelist: idWhitelistRegexps.map((r) => `"${r.toString()}"`).join(", ")
522
+ };
505
523
  }
506
- });
524
+ const quote = quoteStyle === "double" ? "\"" : "'";
525
+ context.report({
526
+ node: idPropNode ?? messageDescriptorNode ?? node,
527
+ messageId,
528
+ data: messageData,
529
+ fix(fixer) {
530
+ if (idPropNode) {
531
+ if (idPropNode.type === "JSXAttribute") return fixer.replaceText(idPropNode, `id="${correctId}"`);
532
+ return fixer.replaceText(idPropNode, `id: ${quote}${correctId}${quote}`);
533
+ }
534
+ if (messagePropNode) {
535
+ if (messagePropNode.type === "JSXAttribute") return fixer.insertTextAfter(messagePropNode, ` id="${correctId}"`);
536
+ return fixer.insertTextAfter(messagePropNode, `, id: ${quote}${correctId}${quote}`);
537
+ }
538
+ return null;
539
+ }
540
+ });
541
+ }
507
542
  }
508
543
  }
509
544
  }
@@ -588,9 +623,7 @@ function collectPlaceholderNames(ast) {
588
623
  placeholderNames.add(element.value);
589
624
  for (const { value } of Object.values(element.options)) _traverse(value);
590
625
  break;
591
- default:
592
- placeholderNames.add(element.value);
593
- break;
626
+ default: placeholderNames.add(element.value);
594
627
  }
595
628
  }
596
629
  }
@@ -857,9 +890,7 @@ function calculateComplexity(ast) {
857
890
  case TYPE.tag:
858
891
  complexity = _calculate(element.children, 0) * _calculate(ast, index + 1);
859
892
  break;
860
- default:
861
- complexity = _calculate(ast, index + 1);
862
- break;
893
+ default: complexity = _calculate(ast, index + 1);
863
894
  }
864
895
  complexityByNode.set(element, complexity);
865
896
  return complexity;
@@ -3578,20 +3609,22 @@ function checkNode$9(context, node) {
3578
3609
  if (emojiConfig?.versionAbove && isValidEmojiVersion(emojiConfig.versionAbove)) versionAbove = emojiConfig.versionAbove;
3579
3610
  for (const [{ message: { defaultMessage }, messageNode }] of msgs) {
3580
3611
  if (!defaultMessage || !messageNode) continue;
3581
- if (hasEmoji(defaultMessage)) if (versionAbove) {
3582
- const filter = filterEmojis(versionAbove);
3583
- for (const emoji of extractEmojis(defaultMessage)) if (!filter(emoji)) context.report({
3612
+ if (hasEmoji(defaultMessage)) {
3613
+ if (versionAbove) {
3614
+ const filter = filterEmojis(versionAbove);
3615
+ for (const emoji of extractEmojis(defaultMessage)) if (!filter(emoji)) context.report({
3616
+ node: messageNode,
3617
+ messageId: "notAllowedAboveVersion",
3618
+ data: {
3619
+ versionAbove,
3620
+ emoji
3621
+ }
3622
+ });
3623
+ } else context.report({
3584
3624
  node: messageNode,
3585
- messageId: "notAllowedAboveVersion",
3586
- data: {
3587
- versionAbove,
3588
- emoji
3589
- }
3625
+ messageId: "notAllowed"
3590
3626
  });
3591
- } else context.report({
3592
- node: messageNode,
3593
- messageId: "notAllowed"
3594
- });
3627
+ }
3595
3628
  }
3596
3629
  }
3597
3630
  const rule$12 = {
@@ -4030,9 +4063,7 @@ function trimMultiWhitespaces(message, ast) {
4030
4063
  case TYPE.select:
4031
4064
  for (const option of Object.values(element.options)) collectLiteralElements(option.value);
4032
4065
  break;
4033
- case TYPE.tag:
4034
- collectLiteralElements(element.children);
4035
- break;
4066
+ case TYPE.tag: collectLiteralElements(element.children);
4036
4067
  }
4037
4068
  };
4038
4069
  collectLiteralElements(ast);
@@ -4290,10 +4321,7 @@ function verifyAst(context, messageNode, ast) {
4290
4321
  case TYPE.select:
4291
4322
  for (const { value } of Object.values(current.options)) _verifyAst(value);
4292
4323
  break;
4293
- case TYPE.tag:
4294
- _verifyAst(current.children);
4295
- break;
4296
- default: break;
4324
+ case TYPE.tag: _verifyAst(current.children);
4297
4325
  }
4298
4326
  }
4299
4327
  }
@@ -4311,10 +4339,7 @@ function verifyAst(context, messageNode, ast) {
4311
4339
  case TYPE.tag:
4312
4340
  _replacementArgumentWithPound(name, element.children);
4313
4341
  break;
4314
- case TYPE.select:
4315
- for (const { value } of Object.values(element.options)) _replacementArgumentWithPound(name, value);
4316
- break;
4317
- default: break;
4342
+ case TYPE.select: for (const { value } of Object.values(element.options)) _replacementArgumentWithPound(name, value);
4318
4343
  }
4319
4344
  }
4320
4345
  function _removeRangeAndPrependPluralClauses(rangeToRemoveStart, rangeToRemoveEnd, pluralElement, prependText) {
@@ -4450,18 +4475,27 @@ function getLastBoundaryElement(ast) {
4450
4475
  if (last.type === TYPE.tag) return getLastBoundaryElement(last.children);
4451
4476
  return last;
4452
4477
  }
4453
- function findWhitespaceIssues(ast) {
4478
+ function isPlaceholder(element) {
4479
+ return element.type !== TYPE.literal && element.type !== TYPE.tag;
4480
+ }
4481
+ function findMessageIssues(ast) {
4454
4482
  const issues = [];
4455
4483
  const first = getFirstBoundaryElement(ast);
4456
4484
  const last = getLastBoundaryElement(ast);
4457
4485
  if (first === last && first?.type === TYPE.literal && ast.length === 1 && first.value.trim() === "") return issues;
4458
4486
  if (first?.type === TYPE.literal && /^\s/.test(first.value)) issues.push("leadingWhitespace");
4459
4487
  if (last?.type === TYPE.literal && /\s$/.test(last.value)) issues.push("trailingWhitespace");
4488
+ for (let index = 0; index < ast.length - 1; index++) {
4489
+ const current = ast[index];
4490
+ const next = ast[index + 1];
4491
+ if (current.type === TYPE.literal && /\p{L}$/u.test(current.value) && isPlaceholder(next) || isPlaceholder(current) && next.type === TYPE.literal && /^\p{L}/u.test(next.value)) issues.push("placeholderAdjacentToWord");
4492
+ }
4460
4493
  for (const element of ast) switch (element.type) {
4461
4494
  case TYPE.plural:
4462
4495
  case TYPE.select:
4463
- for (const option of Object.values(element.options)) issues.push(...findWhitespaceIssues(option.value));
4496
+ for (const option of Object.values(element.options)) issues.push(...findMessageIssues(option.value));
4464
4497
  break;
4498
+ case TYPE.tag: issues.push(...findMessageIssues(element.children));
4465
4499
  }
4466
4500
  return issues;
4467
4501
  }
@@ -4480,7 +4514,7 @@ function checkNode(context, node) {
4480
4514
  });
4481
4515
  return;
4482
4516
  }
4483
- const issues = findWhitespaceIssues(ast);
4517
+ const issues = findMessageIssues(ast);
4484
4518
  const seen = /* @__PURE__ */ new Set();
4485
4519
  for (const issue of issues) {
4486
4520
  if (seen.has(issue)) continue;
@@ -4503,7 +4537,8 @@ const rule = {
4503
4537
  messages: {
4504
4538
  ...CORE_MESSAGES,
4505
4539
  leadingWhitespace: "Messages should be full sentences — leading whitespace suggests string concatenation",
4506
- trailingWhitespace: "Messages should be full sentences — trailing whitespace suggests string concatenation"
4540
+ trailingWhitespace: "Messages should be full sentences — trailing whitespace suggests string concatenation",
4541
+ placeholderAdjacentToWord: "Placeholders should not be joined directly to words or units — use ICU syntax or an Intl formatter"
4507
4542
  },
4508
4543
  schema: []
4509
4544
  },
@@ -4538,7 +4573,7 @@ var package_exports = /* @__PURE__ */ __exportAll({
4538
4573
  version: () => version$1
4539
4574
  });
4540
4575
  var name$1 = "eslint-plugin-formatjs";
4541
- var version$1 = "6.4.20";
4576
+ var version$1 = "6.6.0";
4542
4577
  var description = "ESLint plugin for formatjs";
4543
4578
  var keywords = [
4544
4579
  "eslint",
@@ -4564,7 +4599,7 @@ var dependencies = {
4564
4599
  "@types/estree-jsx": "1",
4565
4600
  "@types/picomatch": "4",
4566
4601
  "@unicode/unicode-17.0.0": "1",
4567
- "magic-string": "0",
4602
+ "magic-string": "1",
4568
4603
  "picomatch": "2 || 3 || 4"
4569
4604
  };
4570
4605
  var peerDependencies = { "eslint": "9 || 10" };
@@ -4640,7 +4675,17 @@ plugin.configs = {
4640
4675
  other: true
4641
4676
  }],
4642
4677
  "formatjs/no-literal-string-in-jsx": ["error", { props: { include: [["*", "{label,placeholder,title}"]] } }],
4643
- "formatjs/blocklist-elements": ["error", ["selectordinal"]],
4678
+ "formatjs/blocklist-elements": ["error", ["selectordinal", {
4679
+ type: "select",
4680
+ allow: {
4681
+ variable: "gender",
4682
+ options: [
4683
+ "male",
4684
+ "female",
4685
+ "other"
4686
+ ]
4687
+ }
4688
+ }]],
4644
4689
  "formatjs/prefer-full-sentence": "error"
4645
4690
  }
4646
4691
  },
@@ -4664,7 +4709,17 @@ plugin.configs = {
4664
4709
  other: true
4665
4710
  }],
4666
4711
  "formatjs/no-literal-string-in-jsx": ["warn", { props: { include: [["*", "{label,placeholder,title}"]] } }],
4667
- "formatjs/blocklist-elements": ["error", ["selectordinal"]],
4712
+ "formatjs/blocklist-elements": ["error", ["selectordinal", {
4713
+ type: "select",
4714
+ allow: {
4715
+ variable: "gender",
4716
+ options: [
4717
+ "male",
4718
+ "female",
4719
+ "other"
4720
+ ]
4721
+ }
4722
+ }]],
4668
4723
  "formatjs/prefer-full-sentence": "error"
4669
4724
  }
4670
4725
  }