@gitlab/eslint-plugin 19.3.0 → 19.5.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/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ # [19.5.0](https://gitlab.com/gitlab-org/frontend/eslint-plugin/compare/v19.4.0...v19.5.0) (2024-03-27)
2
+
3
+
4
+ ### Features
5
+
6
+ * **require-valid-i18n-helpers:** lint against string interpolation ([268d858](https://gitlab.com/gitlab-org/frontend/eslint-plugin/commit/268d858f8bd02cab06ea74f2c6844166489d0d89))
7
+
8
+ # [19.4.0](https://gitlab.com/gitlab-org/frontend/eslint-plugin/compare/v19.3.0...v19.4.0) (2024-01-10)
9
+
10
+
11
+ ### Features
12
+
13
+ * **vtu-no-wrapper-vm:** Improve reporting with $nextTick ([0cb92b8](https://gitlab.com/gitlab-org/frontend/eslint-plugin/commit/0cb92b870feeea8d889ceec68bb30145b584c823))
14
+ * **vtu-no-wrapper-vm:** Prevent saving off a reference to wrapper.vm ([96a59b0](https://gitlab.com/gitlab-org/frontend/eslint-plugin/commit/96a59b066bed3cca5bc62dcfaeb7c26160eda92a))
15
+
1
16
  # [19.3.0](https://gitlab.com/gitlab-org/frontend/eslint-plugin/compare/v19.2.0...v19.3.0) (2023-12-05)
2
17
 
3
18
 
@@ -5,6 +5,8 @@ following:
5
5
 
6
6
  - All helpers should be called with string-literal arguments to ensure the strings are properly
7
7
  collected by the `gettext:regenerate` script.
8
+ - The helpers' arguments should not use string interpolation as this also prevents the strings from
9
+ being collected.
8
10
  - `__` should not be called with a namespace.
9
11
  - `s__` requires a translation namespace to be provided.
10
12
  - `n__` should use the same namespace, or no namespace at all, for both the singular and plural forms.
@@ -21,6 +23,11 @@ const description = __(getDescription());
21
23
  // Extraneous namespace
22
24
  const globalTranslation = __('Global|Translation');
23
25
 
26
+ // String interpolation
27
+ const withStringInterpolation1 = __(`String ${interpolation}`);
28
+ const withStringInterpolation2 = s__("SomeContext", `String ${interpolation}`);
29
+ const withStringInterpolation3 = n__("%d apple", `%d ${apples}`, appleCount);
30
+
24
31
  // Missing translation namespace
25
32
  const namespacedTranslation = s__('NamespacedTranslation');
26
33
 
@@ -0,0 +1,53 @@
1
+ # @gitlab/vtu-no-wrapper-vm
2
+
3
+ Prevent direct access to `vm` internals for `@vue/test-utils` wrappers.
4
+ This is helpful for writing maintainable Vue tests.
5
+
6
+ See https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html#what-and-how-to-test
7
+
8
+ ## Rule Details
9
+
10
+ ### Examples of **incorrect** code for this rule
11
+
12
+ ```javascript
13
+ it('calls spy when foo-ed', () => {
14
+ // !! Direct access to `vm` properties !!
15
+ wrapper.vm.onFoo();
16
+
17
+ expect(spy).toHaveBeenCalled();
18
+ });
19
+ ```
20
+
21
+ ### Examples of **correct** code for this rule
22
+
23
+ ```javascript
24
+ it('calls spy when foo-ed', () => {
25
+ wrapper.find('button').trigger('click');
26
+
27
+ expect(spy).toHaveBeenCalled();
28
+ });
29
+ ```
30
+
31
+ ## Options
32
+
33
+ ```json
34
+ {
35
+ "@gitlab/vtu-no-wrapper-vm": [
36
+ "error",
37
+ {
38
+ "allow": ["$emit"]
39
+ }
40
+ ]
41
+ }
42
+ ```
43
+
44
+ - `allow` contains a list of allowable property names that can be directly
45
+ accessed under a vm.
46
+
47
+ ## Related Rules
48
+
49
+ N/A.
50
+
51
+ ## When Not To Use It
52
+
53
+ Don't use it if you don't want it.
@@ -13,6 +13,11 @@ This rule is like [require-valid-i18n-helpers](./require-valid-i18n-helpers.md)
13
13
  {{ __(LABEL) }}
14
14
  {{ __(getDescription()) }}
15
15
 
16
+ <!-- String interpolation -->
17
+ {{ __(`String ${interpolation}`) }}
18
+ {{ s__("SomeContext", `String ${interpolation}`) }}
19
+ {{ n__("%d apple", `%d ${apples}`, appleCount) }}
20
+
16
21
  <!-- Missing translation namespace -->
17
22
  {{ s__('NamespacedTranslation') }}
18
23
  </div>
package/docs/rules.md CHANGED
@@ -9,6 +9,7 @@ Available rules:
9
9
  - [require-i18n-strings](./rules/require-i18n-strings.md): Detect a string which has been hard coded and requires externalization.
10
10
  - [require-valid-i18n-helpers](./rules/require-valid-i18n-helpers.md): Enforces valid usage of translation helpers in JavaScript.
11
11
  - [vtu-no-explicit-wrapper-destroy](./rules/vtu-no-explicit-wrapper-destroy.md): Prevents redundant destroy calls and null asignment
12
+ - [vtu-no-wrapper-vm](./rules/vtu-no-wrapper-vm.md): Prevent direct access to `vm` internals for `@vue/test-utils` wrappers.
12
13
  - [vue-no-data-toggle](./rules/vue-no-data-toggle.md): Restrict the use of `data-toggle` bootstrap behaviors within Vue templates
13
14
  - [vue-no-new-non-primitive-in-template](./rules/vue-no-new-non-primitive-in-template.md): Prevents non-primitive values from being declared in templates
14
15
  - [vue-prefer-dollar-scopedslots](./rules/vue-prefer-dollar-scopedslots.md): Prefer $scopedSlots over $slots for Vue 2.x.
package/lib/index.js CHANGED
@@ -8,6 +8,7 @@ module.exports = {
8
8
  'require-i18n-strings': require('./rules/require-i18n-strings.js'),
9
9
  'require-valid-i18n-helpers': require('./rules/require-valid-i18n-helpers.js'),
10
10
  'vtu-no-explicit-wrapper-destroy': require('./rules/vtu-no-explicit-wrapper-destroy.js'),
11
+ 'vtu-no-wrapper-vm': require('./rules/vtu-no-wrapper-vm.js'),
11
12
  'vue-no-data-toggle': require('./rules/vue-no-data-toggle.js'),
12
13
  'vue-no-new-non-primitive-in-template': require('./rules/vue-no-new-non-primitive-in-template.js'),
13
14
  'vue-prefer-dollar-scopedslots': require('./rules/vue-prefer-dollar-scopedslots.js'),
@@ -0,0 +1,111 @@
1
+ // ------------------------------------------------------------------------------
2
+ // Requirements
3
+ // ------------------------------------------------------------------------------
4
+ const { DOCS_BASE_URL } = require('../constants');
5
+
6
+ // ------------------------------------------------------------------------------
7
+ // Helpers
8
+ // ------------------------------------------------------------------------------
9
+
10
+ const getMemberName = (node) => {
11
+ if (node.type === 'Identifier') {
12
+ return node.name;
13
+ }
14
+ if (node.type === 'Literal') {
15
+ return node.value;
16
+ }
17
+
18
+ return null;
19
+ };
20
+
21
+ const isMemberName = (node, name) => {
22
+ return getMemberName(node) === name;
23
+ };
24
+
25
+ const isVmReference = (node) => {
26
+ if (node.type === 'MemberExpression') {
27
+ return isMemberName(node.property, 'vm');
28
+ }
29
+
30
+ return false;
31
+ };
32
+
33
+ const isAllowableVmProperty = (node, allowable) => {
34
+ const name = getMemberName(node);
35
+
36
+ if (!name) {
37
+ return true;
38
+ }
39
+
40
+ return allowable.has(name);
41
+ };
42
+
43
+ // ------------------------------------------------------------------------------
44
+ // Rule Definition
45
+ // ------------------------------------------------------------------------------
46
+
47
+ /**
48
+ *
49
+ * @param {Set} allowable
50
+ */
51
+ const getErrorMessage = (property, allowable) => {
52
+ if (getMemberName(property) === '$nextTick') {
53
+ return `Do not access \`vm\` internals directly. For \`$nextTick\`, import \`nextTick\` directly from the Vue package.`;
54
+ }
55
+ if (allowable.size) {
56
+ const part = Array.from(allowable).join(', ');
57
+
58
+ return `Do not access \`vm\` internals directly. Only the following properties are allowed: ${part}`;
59
+ }
60
+
61
+ return 'Do not access `vm` internals directly.';
62
+ };
63
+
64
+ module.exports = {
65
+ meta: {
66
+ type: 'error',
67
+ docs: {
68
+ description: 'Prevent direct access to `vm` internals for `@vue/test-utils` wrappers.',
69
+ category: undefined,
70
+ url: DOCS_BASE_URL + '/vtu-no-wrapper-vm.md',
71
+ },
72
+ schema: [
73
+ {
74
+ type: 'object',
75
+ properties: {
76
+ allow: {
77
+ type: 'array',
78
+ items: {
79
+ type: 'string',
80
+ },
81
+ },
82
+ },
83
+ additionalProperties: false,
84
+ },
85
+ ],
86
+ },
87
+ create(context) {
88
+ const options = context.options[0] || {};
89
+ const allowable = new Set(options.allow);
90
+
91
+ return {
92
+ AssignmentExpression(node) {
93
+ if (isVmReference(node.right)) {
94
+ context.report({
95
+ node: node.right,
96
+ message:
97
+ 'Do not access `vm` internals directly or work around this by saving a reference to `vm`.',
98
+ });
99
+ }
100
+ },
101
+ MemberExpression(node) {
102
+ if (isVmReference(node.object) && !isAllowableVmProperty(node.property, allowable)) {
103
+ context.report({
104
+ node,
105
+ message: getErrorMessage(node.property, allowable),
106
+ });
107
+ }
108
+ },
109
+ };
110
+ },
111
+ };
@@ -10,6 +10,8 @@ const P_GETTEXT = 's__';
10
10
  const N_GETTEXT = 'n__';
11
11
 
12
12
  const ERROR_MESSAGE_STRING_LITERAL = 'Translation helpers must be called with a string literal.';
13
+ const ERROR_MESSAGE_STRING_INTERPOLATION =
14
+ 'Translation helpers should not use string interpolation.';
13
15
  const ERROR_MESSAGE_EXTRANEOUS_NAMESPACE =
14
16
  '__ should not be used with a translation namespace, consider using s__ instead.';
15
17
  const ERROR_MESSAGE_MISSING_NAMESPACE = 's__ requires a translation namespace to be provided.';
@@ -38,6 +40,16 @@ const mustBeCalledWithStringLiteralArguments = (context, node) => {
38
40
  }
39
41
  };
40
42
 
43
+ const mustNotHaveStringInterpolation = (context, node) => {
44
+ const [arg1, arg2] = node.arguments;
45
+ if ([arg1, arg2].some((arg) => arg?.expressions?.length)) {
46
+ context.report({
47
+ node,
48
+ message: ERROR_MESSAGE_STRING_INTERPOLATION,
49
+ });
50
+ }
51
+ };
52
+
41
53
  const mustHaveNamespace = (context, node) => {
42
54
  if (node.arguments.length === 2) {
43
55
  return;
@@ -94,9 +106,21 @@ const namespacesMustMatch = (context, node) => {
94
106
  };
95
107
 
96
108
  const VALIDATORS = {
97
- [GETTEXT]: [mustBeCalledWithStringLiteralArguments, mustNotHaveNamespace],
98
- [P_GETTEXT]: [mustBeCalledWithStringLiteralArguments, mustHaveNamespace],
99
- [N_GETTEXT]: [mustBeCalledWithStringLiteralArguments, namespacesMustMatch],
109
+ [GETTEXT]: [
110
+ mustBeCalledWithStringLiteralArguments,
111
+ mustNotHaveStringInterpolation,
112
+ mustNotHaveNamespace,
113
+ ],
114
+ [P_GETTEXT]: [
115
+ mustBeCalledWithStringLiteralArguments,
116
+ mustNotHaveStringInterpolation,
117
+ mustHaveNamespace,
118
+ ],
119
+ [N_GETTEXT]: [
120
+ mustBeCalledWithStringLiteralArguments,
121
+ mustNotHaveStringInterpolation,
122
+ namespacesMustMatch,
123
+ ],
100
124
  };
101
125
 
102
126
  const validateI18nHelperCallFactory = (context) => (node) => {
@@ -113,6 +137,7 @@ const validateI18nHelperCallFactory = (context) => (node) => {
113
137
  module.exports = {
114
138
  validateI18nHelperCallFactory,
115
139
  ERROR_MESSAGE_STRING_LITERAL,
140
+ ERROR_MESSAGE_STRING_INTERPOLATION,
116
141
  ERROR_MESSAGE_EXTRANEOUS_NAMESPACE,
117
142
  ERROR_MESSAGE_MISSING_NAMESPACE,
118
143
  ERROR_MESSAGE_NAMESPACES_MISMATCH,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/eslint-plugin",
3
- "version": "19.3.0",
3
+ "version": "19.5.0",
4
4
  "description": "GitLab package for our custom eslint rules",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {