@gitlab/eslint-plugin 21.4.0 → 21.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 +12 -0
- package/docs/rules/vue-no-popover-target-dollar-el.md +69 -0
- package/docs/rules.md +1 -0
- package/lib/configs/vue.js +1 -0
- package/lib/index.js +1 -0
- package/lib/rules/vue-no-popover-target-dollar-el.js +156 -0
- package/package.json +1 -1
- package/scripts/integration_test/bootstrap.sh +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @gitlab/eslint-plugin
|
|
2
2
|
|
|
3
|
+
## 21.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8fa643e: Add `vue-no-popover-target-dollar-el` rule to flag and autofix unnecessary `.$el` in `:target` callbacks on `<gl-popover>` and `<gl-tooltip>`.
|
|
8
|
+
|
|
9
|
+
## 21.4.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- cd391b5: No changes.
|
|
14
|
+
|
|
3
15
|
## 21.4.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @gitlab/vue-no-popover-target-dollar-el
|
|
2
|
+
|
|
3
|
+
Disallows function-form `:target` bindings on `<gl-popover>` and `<gl-tooltip>` where
|
|
4
|
+
the callback body ends in `.$el`.
|
|
5
|
+
|
|
6
|
+
`GlPopover` and `GlTooltip` accept a `:target` prop that is either a reference or a
|
|
7
|
+
zero-argument function returning one. The reference can be a plain DOM element
|
|
8
|
+
[or a Vue component instance](https://gitlab.com/gitlab-org/gitlab-services/design.gitlab.com/-/merge_requests/6006).
|
|
9
|
+
The components use `.$el` automatically when it exists, so accessing it
|
|
10
|
+
manually is unnecessary. This helps to avoid reference errors during teardown,
|
|
11
|
+
or other cases where the ref does not exist at the time the `target` function
|
|
12
|
+
is called.
|
|
13
|
+
|
|
14
|
+
## Rule Details
|
|
15
|
+
|
|
16
|
+
### Examples of **incorrect** code for this rule
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<!-- Arrow function returning .$el -->
|
|
20
|
+
<gl-popover :target="() => $refs.myButton.$el" />
|
|
21
|
+
|
|
22
|
+
<!-- Arrow function with explicit this -->
|
|
23
|
+
<gl-tooltip :target="() => this.$refs.myButton.$el" />
|
|
24
|
+
|
|
25
|
+
<!-- Function expression -->
|
|
26
|
+
<gl-popover :target="function() { return $refs.myButton.$el; }" />
|
|
27
|
+
|
|
28
|
+
<!-- PascalCase component names -->
|
|
29
|
+
<GlPopover :target="() => $refs.myButton.$el" />
|
|
30
|
+
<GlTooltip :target="() => $refs.myButton.$el" />
|
|
31
|
+
|
|
32
|
+
<!-- v-bind: long-form syntax -->
|
|
33
|
+
<gl-popover v-bind:target="() => $refs.myButton.$el" />
|
|
34
|
+
|
|
35
|
+
<!-- Detection is not limited to `$refs`: any callback returning a
|
|
36
|
+
component instance's `.$el` is flagged -->
|
|
37
|
+
<gl-popover :target="() => this.myButton.$el" />
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Examples of **correct** code for this rule
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<!-- Plain ref (no function) — always fine -->
|
|
44
|
+
<gl-popover :target="$refs.myButton" />
|
|
45
|
+
|
|
46
|
+
<!-- Arrow function without .$el — already correct -->
|
|
47
|
+
<gl-popover :target="() => $refs.myButton" />
|
|
48
|
+
|
|
49
|
+
<!-- DOM query — no .$el needed -->
|
|
50
|
+
<gl-popover :target="() => document.querySelector('#my-button')" />
|
|
51
|
+
|
|
52
|
+
<!-- .:target on an unrelated component — not flagged -->
|
|
53
|
+
<my-component :target="() => $refs.foo.$el" />
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Options
|
|
57
|
+
|
|
58
|
+
Nothing
|
|
59
|
+
|
|
60
|
+
## Related rules
|
|
61
|
+
|
|
62
|
+
- [`vue-no-data-toggle`](./vue-no-data-toggle.md)
|
|
63
|
+
|
|
64
|
+
## When Not To Use It
|
|
65
|
+
|
|
66
|
+
If your project is still using a version of `@gitlab/ui` that has **not** yet received the
|
|
67
|
+
companion fix (design.gitlab.com!6006), enabling this rule as `error` may cause runtime
|
|
68
|
+
issues. In that case, land the rule as `warn` or keep it off until the updated `@gitlab/ui`
|
|
69
|
+
version is deployed.
|
package/docs/rules.md
CHANGED
|
@@ -16,6 +16,7 @@ Available rules:
|
|
|
16
16
|
- [vue-no-data-toggle](./rules/vue-no-data-toggle.md): Restrict the use of `data-toggle` bootstrap behaviors within Vue templates
|
|
17
17
|
- [vue-no-hardcoded-urls](./rules/vue-no-hardcoded-urls.md): Disallow hardcoded URLs in Vue templates
|
|
18
18
|
- [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
|
|
19
|
+
- [vue-no-popover-target-dollar-el](./rules/vue-no-popover-target-dollar-el.md): Disallow function-form `:target` bindings ending in `.$el` on `<gl-popover>` and `<gl-tooltip>`
|
|
19
20
|
- [vue-no-undef-apollo-properties](./rules/vue-no-undef-apollo-properties.md): Require Apollo query properties to be initialized in component data.
|
|
20
21
|
- [vue-prefer-dollar-scopedslots](./rules/vue-prefer-dollar-scopedslots.md): Prefer $scopedSlots over $slots for Vue 2.x.
|
|
21
22
|
- [vue-require-i18n-attribute-strings](./rules/vue-require-i18n-attribute-strings.md): Detect non externalized strings in vue `<template>` attributes
|
package/lib/configs/vue.js
CHANGED
|
@@ -45,6 +45,7 @@ module.exports = {
|
|
|
45
45
|
},
|
|
46
46
|
],
|
|
47
47
|
'@gitlab/vue-no-data-toggle': 'error',
|
|
48
|
+
'@gitlab/vue-no-popover-target-dollar-el': 'warn',
|
|
48
49
|
'@gitlab/vue-prefer-dollar-scopedslots': 'error',
|
|
49
50
|
'@gitlab/vue-slot-name-casing': 'error',
|
|
50
51
|
'@gitlab/no-runtime-template-compiler': 'error',
|
package/lib/index.js
CHANGED
|
@@ -15,6 +15,7 @@ module.exports = {
|
|
|
15
15
|
'vue-no-data-toggle': require('./rules/vue-no-data-toggle.js'),
|
|
16
16
|
'vue-no-hardcoded-urls': require('./rules/vue-no-hardcoded-urls.js'),
|
|
17
17
|
'vue-no-new-non-primitive-in-template': require('./rules/vue-no-new-non-primitive-in-template.js'),
|
|
18
|
+
'vue-no-popover-target-dollar-el': require('./rules/vue-no-popover-target-dollar-el.js'),
|
|
18
19
|
'vue-no-undef-apollo-properties': require('./rules/vue-no-undef-apollo-properties.js'),
|
|
19
20
|
'vue-prefer-dollar-scopedslots': require('./rules/vue-prefer-dollar-scopedslots.js'),
|
|
20
21
|
'vue-require-i18n-attribute-strings': require('./rules/vue-require-i18n-attribute-strings.js'),
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// ------------------------------------------------------------------------------
|
|
2
|
+
// Requirements
|
|
3
|
+
// ------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
const { DOCS_BASE_URL } = require('../constants');
|
|
6
|
+
const { defineTemplateBodyVisitor } = require('../utils/index');
|
|
7
|
+
|
|
8
|
+
// ------------------------------------------------------------------------------
|
|
9
|
+
// Constants
|
|
10
|
+
// ------------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Component names (both kebab-case and PascalCase) that use a `:target` prop
|
|
14
|
+
* and where passing a function returning `.$el` is unnecessary.
|
|
15
|
+
*/
|
|
16
|
+
const TARGET_COMPONENT_NAMES = new Set(['gl-popover', 'gl-tooltip', 'GlPopover', 'GlTooltip']);
|
|
17
|
+
|
|
18
|
+
const MESSAGE_ID = 'noPopoverTargetDollarEl';
|
|
19
|
+
|
|
20
|
+
// ------------------------------------------------------------------------------
|
|
21
|
+
// Helpers
|
|
22
|
+
// ------------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns true if the given VAttribute node is a `:target` or `v-bind:target`
|
|
26
|
+
* directive binding.
|
|
27
|
+
*
|
|
28
|
+
* @param {import('vue-eslint-parser').AST.VAttribute | import('vue-eslint-parser').AST.VDirective} node
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
function isTargetBinding(node) {
|
|
32
|
+
if (!node.directive) return false;
|
|
33
|
+
// Must be a v-bind directive
|
|
34
|
+
if (node.key.name.name !== 'bind') return false;
|
|
35
|
+
// Argument must be the identifier "target"
|
|
36
|
+
const arg = node.key.argument;
|
|
37
|
+
return arg != null && arg.type === 'VIdentifier' && arg.name === 'target';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns true if the given VElement node is a gl-popover, gl-tooltip,
|
|
42
|
+
* GlPopover, or GlTooltip component.
|
|
43
|
+
*
|
|
44
|
+
* @param {import('vue-eslint-parser').AST.VElement} element
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function isTargetComponent(element) {
|
|
48
|
+
return TARGET_COMPONENT_NAMES.has(element.rawName);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Given an expression node (the value of the `:target` binding), returns the
|
|
53
|
+
* MemberExpression node for the trailing `.$el` access if one exists, or null.
|
|
54
|
+
*
|
|
55
|
+
* Handles:
|
|
56
|
+
* - `() => $refs.foo.$el`
|
|
57
|
+
* - `() => this.$refs.foo.$el`
|
|
58
|
+
* - `() => { return $refs.foo.$el; }`
|
|
59
|
+
* - `function() { return $refs.foo.$el; }`
|
|
60
|
+
* - `function() { doSomething(); return this.$refs.foo.$el; }`
|
|
61
|
+
*
|
|
62
|
+
* @param {import('eslint').Rule.Node} expr
|
|
63
|
+
* @returns {import('eslint').Rule.Node | null}
|
|
64
|
+
*/
|
|
65
|
+
function getDollarElMemberExpression(expr) {
|
|
66
|
+
if (expr.type !== 'ArrowFunctionExpression' && expr.type !== 'FunctionExpression') {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let body = expr.body;
|
|
71
|
+
|
|
72
|
+
// This doesn't need to catch every case, just the most common ways of
|
|
73
|
+
// writing the target function, which is why this is simple/naive.
|
|
74
|
+
if (body.type === 'BlockStatement') {
|
|
75
|
+
const lastStatement = body.body.at(-1);
|
|
76
|
+
if (!lastStatement || lastStatement.type !== 'ReturnStatement') {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
body = lastStatement.argument;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!body) return null;
|
|
83
|
+
|
|
84
|
+
// The body (or return value) must be a MemberExpression ending in `.$el`
|
|
85
|
+
if (
|
|
86
|
+
body.type === 'MemberExpression' &&
|
|
87
|
+
!body.computed &&
|
|
88
|
+
body.property.type === 'Identifier' &&
|
|
89
|
+
body.property.name === '$el'
|
|
90
|
+
) {
|
|
91
|
+
return body;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ------------------------------------------------------------------------------
|
|
98
|
+
// Rule Definition
|
|
99
|
+
// ------------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
meta: {
|
|
103
|
+
type: 'suggestion',
|
|
104
|
+
docs: {
|
|
105
|
+
description:
|
|
106
|
+
'Disallow function-form `:target` bindings ending in `.$el` on `<gl-popover>` and `<gl-tooltip>`',
|
|
107
|
+
category: undefined,
|
|
108
|
+
url: DOCS_BASE_URL + '/vue-no-popover-target-dollar-el.md',
|
|
109
|
+
},
|
|
110
|
+
fixable: 'code',
|
|
111
|
+
messages: {
|
|
112
|
+
[MESSAGE_ID]:
|
|
113
|
+
'Return the component ref from the `:target` callback on {{component}} instead of its .$el.',
|
|
114
|
+
},
|
|
115
|
+
schema: [],
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
create(context) {
|
|
119
|
+
return defineTemplateBodyVisitor(context, {
|
|
120
|
+
VElement(element) {
|
|
121
|
+
// Must be a gl-popover or gl-tooltip component
|
|
122
|
+
if (!isTargetComponent(element)) return;
|
|
123
|
+
|
|
124
|
+
// Find the `:target` / `v-bind:target` binding among its attributes
|
|
125
|
+
const node = element.startTag.attributes.find(isTargetBinding);
|
|
126
|
+
if (!node) return;
|
|
127
|
+
|
|
128
|
+
// The binding value must be an expression container
|
|
129
|
+
const exprContainer = node.value;
|
|
130
|
+
if (!exprContainer || exprContainer.type !== 'VExpressionContainer') return;
|
|
131
|
+
|
|
132
|
+
const expr = exprContainer.expression;
|
|
133
|
+
if (!expr) return;
|
|
134
|
+
|
|
135
|
+
// Check whether the expression is a function/arrow whose body ends in `.$el`
|
|
136
|
+
const dollarElMember = getDollarElMemberExpression(expr);
|
|
137
|
+
if (!dollarElMember) return;
|
|
138
|
+
|
|
139
|
+
// `dollarElMember` is the full `$refs.foo.$el` MemberExpression.
|
|
140
|
+
// The object part (`$refs.foo`) is what we want to keep.
|
|
141
|
+
const objectNode = dollarElMember.object;
|
|
142
|
+
|
|
143
|
+
context.report({
|
|
144
|
+
node,
|
|
145
|
+
loc: node.loc,
|
|
146
|
+
messageId: MESSAGE_ID,
|
|
147
|
+
data: { component: element.rawName },
|
|
148
|
+
fix(fixer) {
|
|
149
|
+
// Remove the `.$el` suffix: from end of objectNode to end of dollarElMember
|
|
150
|
+
return fixer.removeRange([objectNode.range[1], dollarElMember.range[1]]);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
};
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@ else
|
|
|
35
35
|
fi
|
|
36
36
|
|
|
37
37
|
echo "Installing $CI_REPOSITORY_URL#$SHA"
|
|
38
|
-
yarn add --dev "${CI_REPOSITORY_URL}#${SHA}"
|
|
38
|
+
yarn add --dev --ignore-workspace-root-check "${CI_REPOSITORY_URL}#${SHA}"
|
|
39
39
|
|
|
40
40
|
echo "Deduplicate dependencies"
|
|
41
41
|
# Yarn doesn't seem to be smart enough when using URL installs and might
|