eslint-plugin-formatjs 6.1.4 → 6.2.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 +7 -3
- package/package.json +1 -1
- package/rules/prefer-full-sentence.d.ts +3 -0
- package/rules/prefer-full-sentence.js +111 -0
package/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { rule as noUselessMessage, name as noUselessMessageName } from "./rules/
|
|
|
18
18
|
import { rule as preferFormattedMessage, name as preferFormattedMessageName } from "./rules/prefer-formatted-message.js";
|
|
19
19
|
import { rule as preferPoundInPlural, name as preferPoundInPluralName } from "./rules/prefer-pound-in-plural.js";
|
|
20
20
|
import { rule as noLiteralStringInObject, name as noLiteralStringInObjectName } from "./rules/no-literal-string-in-object.js";
|
|
21
|
+
import { rule as preferFullSentence, name as preferFullSentenceName } from "./rules/prefer-full-sentence.js";
|
|
21
22
|
import * as packageJsonNs from "./package.json" with { type: "json" };
|
|
22
23
|
const packageJson = packageJsonNs.default ?? packageJsonNs;
|
|
23
24
|
const { name, version } = packageJson;
|
|
@@ -42,7 +43,8 @@ const rules = {
|
|
|
42
43
|
[preferFormattedMessageName]: preferFormattedMessage,
|
|
43
44
|
[preferPoundInPluralName]: preferPoundInPlural,
|
|
44
45
|
[noMissingIcuPluralOnePlaceholdersName]: noMissingIcuPluralOnePlaceholders,
|
|
45
|
-
[noLiteralStringInObjectName]: noLiteralStringInObject
|
|
46
|
+
[noLiteralStringInObjectName]: noLiteralStringInObject,
|
|
47
|
+
[preferFullSentenceName]: preferFullSentence
|
|
46
48
|
};
|
|
47
49
|
// Base plugin
|
|
48
50
|
const plugin = {
|
|
@@ -76,7 +78,8 @@ const configs = {
|
|
|
76
78
|
other: true
|
|
77
79
|
}],
|
|
78
80
|
"formatjs/no-literal-string-in-jsx": ["error", { props: { include: [["*", "{label,placeholder,title}"]] } }],
|
|
79
|
-
"formatjs/blocklist-elements": ["error", ["selectordinal"]]
|
|
81
|
+
"formatjs/blocklist-elements": ["error", ["selectordinal"]],
|
|
82
|
+
"formatjs/prefer-full-sentence": "error"
|
|
80
83
|
}
|
|
81
84
|
},
|
|
82
85
|
recommended: {
|
|
@@ -99,7 +102,8 @@ const configs = {
|
|
|
99
102
|
other: true
|
|
100
103
|
}],
|
|
101
104
|
"formatjs/no-literal-string-in-jsx": ["warn", { props: { include: [["*", "{label,placeholder,title}"]] } }],
|
|
102
|
-
"formatjs/blocklist-elements": ["error", ["selectordinal"]]
|
|
105
|
+
"formatjs/blocklist-elements": ["error", ["selectordinal"]],
|
|
106
|
+
"formatjs/prefer-full-sentence": "error"
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { parse, TYPE } from "@formatjs/icu-messageformat-parser";
|
|
2
|
+
import { extractMessages, getSettings } from "../util.js";
|
|
3
|
+
import { CORE_MESSAGES } from "../messages.js";
|
|
4
|
+
/**
|
|
5
|
+
* Get the first boundary element, recursing into tags since
|
|
6
|
+
* <b>hello</b> starts with the literal "hello".
|
|
7
|
+
*/
|
|
8
|
+
function getFirstBoundaryElement(ast) {
|
|
9
|
+
if (ast.length === 0) return null;
|
|
10
|
+
const first = ast[0];
|
|
11
|
+
if (first.type === TYPE.tag) {
|
|
12
|
+
return getFirstBoundaryElement(first.children);
|
|
13
|
+
}
|
|
14
|
+
return first;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get the last boundary element, recursing into tags.
|
|
18
|
+
*/
|
|
19
|
+
function getLastBoundaryElement(ast) {
|
|
20
|
+
if (ast.length === 0) return null;
|
|
21
|
+
const last = ast[ast.length - 1];
|
|
22
|
+
if (last.type === TYPE.tag) {
|
|
23
|
+
return getLastBoundaryElement(last.children);
|
|
24
|
+
}
|
|
25
|
+
return last;
|
|
26
|
+
}
|
|
27
|
+
function findWhitespaceIssues(ast) {
|
|
28
|
+
const issues = [];
|
|
29
|
+
const first = getFirstBoundaryElement(ast);
|
|
30
|
+
const last = getLastBoundaryElement(ast);
|
|
31
|
+
// Ignore messages that are only whitespace (edge case)
|
|
32
|
+
if (first === last && first?.type === TYPE.literal && ast.length === 1 && first.value.trim() === "") {
|
|
33
|
+
return issues;
|
|
34
|
+
}
|
|
35
|
+
if (first?.type === TYPE.literal && /^\s/.test(first.value)) {
|
|
36
|
+
issues.push("leadingWhitespace");
|
|
37
|
+
}
|
|
38
|
+
if (last?.type === TYPE.literal && /\s$/.test(last.value)) {
|
|
39
|
+
issues.push("trailingWhitespace");
|
|
40
|
+
}
|
|
41
|
+
// Check each plural/select option branch independently
|
|
42
|
+
for (const element of ast) {
|
|
43
|
+
switch (element.type) {
|
|
44
|
+
case TYPE.plural:
|
|
45
|
+
case TYPE.select: {
|
|
46
|
+
for (const option of Object.values(element.options)) {
|
|
47
|
+
issues.push(...findWhitespaceIssues(option.value));
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return issues;
|
|
54
|
+
}
|
|
55
|
+
function checkNode(context, node) {
|
|
56
|
+
const msgs = extractMessages(node, getSettings(context));
|
|
57
|
+
for (const [{ message: { defaultMessage }, messageNode }] of msgs) {
|
|
58
|
+
if (!defaultMessage || !messageNode) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
let ast;
|
|
62
|
+
try {
|
|
63
|
+
ast = parse(defaultMessage);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
context.report({
|
|
66
|
+
node: messageNode,
|
|
67
|
+
messageId: "parseError",
|
|
68
|
+
data: { error: e instanceof Error ? e.message : String(e) }
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const issues = findWhitespaceIssues(ast);
|
|
73
|
+
// Deduplicate
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
for (const issue of issues) {
|
|
76
|
+
if (seen.has(issue)) continue;
|
|
77
|
+
seen.add(issue);
|
|
78
|
+
context.report({
|
|
79
|
+
node: messageNode,
|
|
80
|
+
messageId: issue
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export const name = "prefer-full-sentence";
|
|
86
|
+
export const rule = {
|
|
87
|
+
meta: {
|
|
88
|
+
type: "suggestion",
|
|
89
|
+
docs: {
|
|
90
|
+
description: "Detects messages with leading/trailing whitespace, which suggests string concatenation instead of full sentences",
|
|
91
|
+
url: "https://formatjs.github.io/docs/tooling/linter#prefer-full-sentence"
|
|
92
|
+
},
|
|
93
|
+
messages: {
|
|
94
|
+
...CORE_MESSAGES,
|
|
95
|
+
leadingWhitespace: "Messages should be full sentences — leading whitespace suggests string concatenation",
|
|
96
|
+
trailingWhitespace: "Messages should be full sentences — trailing whitespace suggests string concatenation"
|
|
97
|
+
},
|
|
98
|
+
schema: []
|
|
99
|
+
},
|
|
100
|
+
create(context) {
|
|
101
|
+
const callExpressionVisitor = (node) => checkNode(context, node);
|
|
102
|
+
const parserServices = context.sourceCode.parserServices;
|
|
103
|
+
if (parserServices?.defineTemplateBodyVisitor) {
|
|
104
|
+
return parserServices.defineTemplateBodyVisitor({ CallExpression: callExpressionVisitor }, { CallExpression: callExpressionVisitor });
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
JSXOpeningElement: (node) => checkNode(context, node),
|
|
108
|
+
CallExpression: callExpressionVisitor
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
};
|