@gitlab/eslint-plugin 9.2.0 → 10.0.1
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/CHANGELOG.md +36 -0
- package/CONTRIBUTING.md +40 -0
- package/docs/development.md +15 -0
- package/docs/rules/require-valid-i18n-helpers.md +41 -0
- package/docs/rules/vue-require-valid-i18n-helpers.md +42 -0
- package/docs/rules.md +2 -2
- package/gitlab_schema_apollo.graphql +29355 -0
- package/jest.config.js +6 -0
- package/lib/configs/default.js +0 -1
- package/lib/configs/i18n.js +2 -1
- package/lib/index.js +2 -1
- package/lib/rules/require-i18n-strings.js +5 -11
- package/lib/rules/require-valid-i18n-helpers.js +29 -0
- package/lib/rules/vue-require-valid-i18n-helpers.js +30 -0
- package/lib/utils/i18n-utils.js +68 -0
- package/lib/utils/rule-utils.js +27 -5
- package/package.json +3 -3
- package/docs/rules/require-string-literal-i18n-helpers.md +0 -26
- package/lib/rules/require-string-literal-i18n-helpers.js +0 -61
package/jest.config.js
ADDED
package/lib/configs/default.js
CHANGED
package/lib/configs/i18n.js
CHANGED
|
@@ -4,6 +4,7 @@ module.exports = {
|
|
|
4
4
|
'@gitlab/require-i18n-strings': 'error',
|
|
5
5
|
'@gitlab/vue-require-i18n-attribute-strings': 'error',
|
|
6
6
|
'@gitlab/vue-require-i18n-strings': 'error',
|
|
7
|
-
'@gitlab/require-
|
|
7
|
+
'@gitlab/require-valid-i18n-helpers': 'error',
|
|
8
|
+
'@gitlab/vue-require-valid-i18n-helpers': 'error',
|
|
8
9
|
},
|
|
9
10
|
};
|
package/lib/index.js
CHANGED
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
module.exports = {
|
|
5
5
|
rules: {
|
|
6
|
-
'require-string-literal-i18n-helpers': require('./rules/require-string-literal-i18n-helpers.js'),
|
|
7
6
|
'no-global-event-off': require('./rules/no-global-event-off.js'),
|
|
8
7
|
'no-runtime-template-compiler': require('./rules/no-runtime-template-compiler.js'),
|
|
9
8
|
'require-i18n-strings': require('./rules/require-i18n-strings.js'),
|
|
9
|
+
'require-valid-i18n-helpers': require('./rules/require-valid-i18n-helpers.js'),
|
|
10
10
|
'vue-emit-events-with-two-args': require('./rules/vue-emit-events-with-two-args.js'),
|
|
11
11
|
'vue-no-data-toggle': require('./rules/vue-no-data-toggle.js'),
|
|
12
12
|
'vue-no-new-non-primitive-in-template': require('./rules/vue-no-new-non-primitive-in-template.js'),
|
|
13
13
|
'vue-require-i18n-attribute-strings': require('./rules/vue-require-i18n-attribute-strings.js'),
|
|
14
14
|
'vue-require-i18n-strings': require('./rules/vue-require-i18n-strings.js'),
|
|
15
15
|
'vue-require-required-key': require('./rules/vue-require-required-key.js'),
|
|
16
|
+
'vue-require-valid-i18n-helpers': require('./rules/vue-require-valid-i18n-helpers.js'),
|
|
16
17
|
'vue-slot-name-casing': require('./rules/vue-slot-name-casing.js'),
|
|
17
18
|
},
|
|
18
19
|
configs: {
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
|
+
// ------------------------------------------------------------------------------
|
|
8
|
+
// Requirements
|
|
9
|
+
// ------------------------------------------------------------------------------
|
|
10
|
+
const { isTemplateLiteral, getTemplateLiteralString } = require('../utils/rule-utils');
|
|
11
|
+
|
|
7
12
|
//------------------------------------------------------------------------------
|
|
8
13
|
// Rule Definition
|
|
9
14
|
//------------------------------------------------------------------------------
|
|
@@ -269,21 +274,10 @@ module.exports = {
|
|
|
269
274
|
return node.type === 'Literal';
|
|
270
275
|
}
|
|
271
276
|
|
|
272
|
-
function isTemplateLiteral(node) {
|
|
273
|
-
return node.type === 'TemplateLiteral';
|
|
274
|
-
}
|
|
275
|
-
|
|
276
277
|
function stripVariables(value) {
|
|
277
278
|
return value ? value.replace(/(\%{[\w-]+})/g, '') : '';
|
|
278
279
|
}
|
|
279
280
|
|
|
280
|
-
function getTemplateLiteralString(node) {
|
|
281
|
-
return node.quasis
|
|
282
|
-
.filter((quasi) => quasi.type === 'TemplateElement')
|
|
283
|
-
.map((quasi) => quasi.value.raw)
|
|
284
|
-
.join('');
|
|
285
|
-
}
|
|
286
|
-
|
|
287
281
|
function getCallExpressionString(node, callee) {
|
|
288
282
|
if (callee.name === 'sprintf') {
|
|
289
283
|
return stripVariables(node.value);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// ------------------------------------------------------------------------------
|
|
2
|
+
// Requirements
|
|
3
|
+
// ------------------------------------------------------------------------------
|
|
4
|
+
const { DOCS_BASE_URL } = require('../constants');
|
|
5
|
+
const { validateI18nHelperCallFactory } = require('../utils/i18n-utils');
|
|
6
|
+
|
|
7
|
+
// ------------------------------------------------------------------------------
|
|
8
|
+
// Rule Definition
|
|
9
|
+
// ------------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'error',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Enforces valid usage of translation helpers in JavaScript.',
|
|
16
|
+
category: 'i18n',
|
|
17
|
+
url: DOCS_BASE_URL + '/require-valid-i18n-helpers.md',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
const validateI18nHelperCall = validateI18nHelperCallFactory(context);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
CallExpression(node) {
|
|
25
|
+
validateI18nHelperCall(node);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// ------------------------------------------------------------------------------
|
|
2
|
+
// Requirements
|
|
3
|
+
// ------------------------------------------------------------------------------
|
|
4
|
+
const { DOCS_BASE_URL } = require('../constants');
|
|
5
|
+
const { defineTemplateBodyVisitor } = require('../utils/index');
|
|
6
|
+
const { validateI18nHelperCallFactory } = require('../utils/i18n-utils');
|
|
7
|
+
|
|
8
|
+
// ------------------------------------------------------------------------------
|
|
9
|
+
// Rule Definition
|
|
10
|
+
// ------------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'error',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Enforces valid usage of translation helpers in Vue templates.',
|
|
17
|
+
category: 'i18n',
|
|
18
|
+
url: DOCS_BASE_URL + '/vue-require-valid-i18n-helpers.md',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
const validateI18nHelperCall = validateI18nHelperCallFactory(context);
|
|
23
|
+
|
|
24
|
+
return defineTemplateBodyVisitor(context, {
|
|
25
|
+
CallExpression(node) {
|
|
26
|
+
validateI18nHelperCall(node);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const {
|
|
2
|
+
getFunctionName,
|
|
3
|
+
isString,
|
|
4
|
+
isTemplateLiteral,
|
|
5
|
+
getTemplateLiteralString,
|
|
6
|
+
} = require('../utils/rule-utils');
|
|
7
|
+
|
|
8
|
+
const GETTEXT = '__';
|
|
9
|
+
const P_GETTEXT = 's__';
|
|
10
|
+
const N_GETTEXT = 'n__';
|
|
11
|
+
|
|
12
|
+
const ERROR_MESSAGE_STRING_LITERAL = 'Translation helpers must be called with a string literal.';
|
|
13
|
+
const ERROR_MESSAGE_MISSING_NAMESPACE = 's__ requires a translation namespace to be provided.';
|
|
14
|
+
|
|
15
|
+
const mustBeCalledWithStringLiteralArguments = (context, node) => {
|
|
16
|
+
const report = (node) => {
|
|
17
|
+
context.report({
|
|
18
|
+
node,
|
|
19
|
+
message: ERROR_MESSAGE_STRING_LITERAL,
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (!node.arguments.length) {
|
|
24
|
+
report(node);
|
|
25
|
+
}
|
|
26
|
+
const [arg1, arg2] = node.arguments;
|
|
27
|
+
if ([arg1, arg2].some((arg) => arg && !isString(arg))) {
|
|
28
|
+
report(node);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const mustHaveNamespace = (context, node) => {
|
|
33
|
+
if (node.arguments.length === 2) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const [arg] = node.arguments;
|
|
38
|
+
const value = isTemplateLiteral(arg) ? getTemplateLiteralString(arg).trim() : arg.value;
|
|
39
|
+
if (!/^\w.+\|/.test(value)) {
|
|
40
|
+
context.report({
|
|
41
|
+
node,
|
|
42
|
+
message: ERROR_MESSAGE_MISSING_NAMESPACE,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const VALIDATORS = {
|
|
48
|
+
[GETTEXT]: [mustBeCalledWithStringLiteralArguments],
|
|
49
|
+
[P_GETTEXT]: [mustBeCalledWithStringLiteralArguments, mustHaveNamespace],
|
|
50
|
+
[N_GETTEXT]: [mustBeCalledWithStringLiteralArguments],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const validateI18nHelperCallFactory = (context) => (node) => {
|
|
54
|
+
const functionName = getFunctionName(node);
|
|
55
|
+
if (!Object.keys(VALIDATORS).includes(functionName)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
VALIDATORS[functionName].forEach((validator) => {
|
|
60
|
+
validator(context, node);
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
validateI18nHelperCallFactory,
|
|
66
|
+
ERROR_MESSAGE_STRING_LITERAL,
|
|
67
|
+
ERROR_MESSAGE_MISSING_NAMESPACE,
|
|
68
|
+
};
|
package/lib/utils/rule-utils.js
CHANGED
|
@@ -7,6 +7,7 @@ const V_TEXT = 'VText';
|
|
|
7
7
|
const V_ELEMENT = 'VElement';
|
|
8
8
|
const TEMPLATE_LITERAL = 'TemplateLiteral';
|
|
9
9
|
const LITERAL = 'Literal';
|
|
10
|
+
const IDENTIFIER = 'Identifier';
|
|
10
11
|
|
|
11
12
|
const NODE_TYPES = {
|
|
12
13
|
CALL_EXPRESSION,
|
|
@@ -18,6 +19,7 @@ const NODE_TYPES = {
|
|
|
18
19
|
V_ELEMENT,
|
|
19
20
|
TEMPLATE_LITERAL,
|
|
20
21
|
LITERAL,
|
|
22
|
+
IDENTIFIER,
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
function closest(node, predicate) {
|
|
@@ -39,6 +41,29 @@ function isString(node) {
|
|
|
39
41
|
return [LITERAL, TEMPLATE_LITERAL].includes(node.type);
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Checks whether a node is a template literal.
|
|
46
|
+
*
|
|
47
|
+
* @param {ASTNode} node The node to type check
|
|
48
|
+
* @returns {boolean} `true` if node is a template literal.
|
|
49
|
+
*/
|
|
50
|
+
function isTemplateLiteral(node) {
|
|
51
|
+
return node.type === TEMPLATE_LITERAL;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extracts the string out of a template literal node.
|
|
56
|
+
*
|
|
57
|
+
* @param {ASTNode} node The template literal node
|
|
58
|
+
* @returns {string} The extracted string.
|
|
59
|
+
*/
|
|
60
|
+
function getTemplateLiteralString(node) {
|
|
61
|
+
return node.quasis
|
|
62
|
+
.filter((quasi) => quasi.type === 'TemplateElement')
|
|
63
|
+
.map((quasi) => quasi.value.raw)
|
|
64
|
+
.join('');
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
/**
|
|
43
68
|
* Check that a node is a `VLiteral type`
|
|
44
69
|
* @param {ASTNode} node The node to type check
|
|
@@ -54,10 +79,6 @@ function isVLiteral(node) {
|
|
|
54
79
|
* @returns {array} In form [prefix, postfix] with the content to prepend and append to the string
|
|
55
80
|
*/
|
|
56
81
|
function selectFixFunction({ type, value }) {
|
|
57
|
-
// TODO: needs tests
|
|
58
|
-
// TODO: these are gitlab specific fixes...
|
|
59
|
-
// TODO: Need a config for the default externalization function to use
|
|
60
|
-
// TODO: probably need withExpressionWithQuotes....
|
|
61
82
|
const noQuotes = ['__(', ')'];
|
|
62
83
|
const withQuotes = ['__("', '")'];
|
|
63
84
|
const withExpression = ['{{ __(', ') }}'];
|
|
@@ -65,7 +86,6 @@ function selectFixFunction({ type, value }) {
|
|
|
65
86
|
|
|
66
87
|
const hasQuotes = isWrappedWithQuotes(value);
|
|
67
88
|
|
|
68
|
-
// TODO: double check this is still needed
|
|
69
89
|
if (type === NODE_TYPES.V_TEXT) {
|
|
70
90
|
return hasQuotes ? withExpression : withQuotesExpression;
|
|
71
91
|
} else {
|
|
@@ -170,6 +190,8 @@ module.exports = {
|
|
|
170
190
|
clearSplitLines,
|
|
171
191
|
closest,
|
|
172
192
|
isString,
|
|
193
|
+
isTemplateLiteral,
|
|
194
|
+
getTemplateLiteralString,
|
|
173
195
|
isVLiteral,
|
|
174
196
|
removeNewLines,
|
|
175
197
|
selectFixFunction,
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/eslint-plugin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.1",
|
|
4
4
|
"description": "GitLab package for our custom eslint rules",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"createRule": "./scripts/createRule.js && yarn update",
|
|
8
8
|
"update": "./scripts/updateFiles.js && prettier --write lib/index.js",
|
|
9
9
|
"commit": "npx git-cz",
|
|
10
|
-
"test": "
|
|
10
|
+
"test": "jest tests"
|
|
11
11
|
},
|
|
12
12
|
"repository": "https://gitlab.com/gitlab-org/frontend/eslint-plugin.git",
|
|
13
13
|
"keywords": [
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"eslint": "^7.27.0",
|
|
50
50
|
"glob": "^7.1.3",
|
|
51
|
-
"
|
|
51
|
+
"jest": "^26.6.3",
|
|
52
52
|
"prettier": "^2.2.1",
|
|
53
53
|
"pretty-quick": "^3.1.0"
|
|
54
54
|
},
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# @gitlab/require-string-literal-i18n-helpers
|
|
2
|
-
|
|
3
|
-
Prevents translation helpers from being called with non-string-literal arguments.
|
|
4
|
-
|
|
5
|
-
## Rule Details
|
|
6
|
-
|
|
7
|
-
### Examples of **incorrect** code for this rule
|
|
8
|
-
|
|
9
|
-
```js
|
|
10
|
-
const label = __(LABEL);
|
|
11
|
-
const description = __(getDescription());
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
### Examples of **correct** code for this rule
|
|
15
|
-
|
|
16
|
-
```js
|
|
17
|
-
const label = __('Some label');
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Options
|
|
21
|
-
|
|
22
|
-
Nothing
|
|
23
|
-
|
|
24
|
-
## Related rules
|
|
25
|
-
|
|
26
|
-
- TBA
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// ------------------------------------------------------------------------------
|
|
2
|
-
// Requirements
|
|
3
|
-
// ------------------------------------------------------------------------------
|
|
4
|
-
const { DOCS_BASE_URL } = require('../constants');
|
|
5
|
-
const { getFunctionName, isString } = require('../utils/rule-utils');
|
|
6
|
-
|
|
7
|
-
// ------------------------------------------------------------------------------
|
|
8
|
-
// Helpers
|
|
9
|
-
// ------------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
const I18N_HELPERS = new Set(['__', 's__', 'n__']);
|
|
12
|
-
const ERROR_MESSAGE = 'Translation helpers must be called with a string literal.';
|
|
13
|
-
|
|
14
|
-
const isI18nCall = (node) => {
|
|
15
|
-
const functionName = getFunctionName(node);
|
|
16
|
-
return functionName && I18N_HELPERS.has(functionName);
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const checkFunctionArgumentFactory = (context) => (node) => {
|
|
20
|
-
if (!isI18nCall(node)) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
const report = (node) => {
|
|
24
|
-
context.report({
|
|
25
|
-
node,
|
|
26
|
-
message: ERROR_MESSAGE,
|
|
27
|
-
});
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
if (!node.arguments.length) {
|
|
31
|
-
report(node);
|
|
32
|
-
}
|
|
33
|
-
const [arg1, arg2] = node.arguments;
|
|
34
|
-
if ([arg1, arg2].some((arg) => arg && !isString(arg))) {
|
|
35
|
-
report(node);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// ------------------------------------------------------------------------------
|
|
40
|
-
// Rule Definition
|
|
41
|
-
// ------------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
module.exports = {
|
|
44
|
-
meta: {
|
|
45
|
-
type: 'error',
|
|
46
|
-
docs: {
|
|
47
|
-
description: 'Prevents translation helpers from being called with non-string-literal arguments.',
|
|
48
|
-
category: 'i18n',
|
|
49
|
-
url: DOCS_BASE_URL + '/require-string-literal-i18n-helpers.md',
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
create(context) {
|
|
53
|
-
const checkFunctionArgument = checkFunctionArgumentFactory(context);
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
CallExpression(node) {
|
|
57
|
-
checkFunctionArgument(node);
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
},
|
|
61
|
-
};
|