@elastic/eslint-plugin-eui 0.0.2 → 0.1.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/README.md +145 -9
- package/lib/cjs/index.d.ts +2 -0
- package/lib/cjs/index.d.ts.map +1 -0
- package/{index.js → lib/cjs/index.js} +19 -6
- package/lib/cjs/rules/href_or_on_click.d.ts +3 -0
- package/lib/cjs/rules/href_or_on_click.d.ts.map +1 -0
- package/lib/cjs/rules/href_or_on_click.js +65 -0
- package/lib/cjs/rules/no_css_color.d.ts +3 -0
- package/lib/cjs/rules/no_css_color.d.ts.map +1 -0
- package/lib/cjs/rules/no_css_color.js +329 -0
- package/lib/cjs/rules/no_restricted_eui_imports.d.ts +8 -0
- package/lib/cjs/rules/no_restricted_eui_imports.d.ts.map +1 -0
- package/lib/cjs/rules/no_restricted_eui_imports.js +76 -0
- package/lib/cjs/rules/prefer_css_attribute_for_eui_components.d.ts +3 -0
- package/lib/cjs/rules/prefer_css_attribute_for_eui_components.d.ts.map +1 -0
- package/lib/cjs/rules/prefer_css_attribute_for_eui_components.js +63 -0
- package/lib/cjs/utils/resolve_member_expression_root.d.ts +3 -0
- package/lib/cjs/utils/resolve_member_expression_root.d.ts.map +1 -0
- package/lib/cjs/utils/resolve_member_expression_root.js +13 -0
- package/lib/esm/index.d.ts +1 -0
- package/lib/esm/index.js +45 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/rules/href_or_on_click.d.ts +2 -0
- package/lib/esm/rules/href_or_on_click.js +61 -0
- package/lib/esm/rules/href_or_on_click.js.map +1 -0
- package/lib/esm/rules/no_css_color.d.ts +2 -0
- package/lib/esm/rules/no_css_color.js +360 -0
- package/lib/esm/rules/no_css_color.js.map +1 -0
- package/lib/esm/rules/no_restricted_eui_imports.d.ts +7 -0
- package/lib/esm/rules/no_restricted_eui_imports.js +76 -0
- package/lib/esm/rules/no_restricted_eui_imports.js.map +1 -0
- package/lib/esm/rules/prefer_css_attribute_for_eui_components.d.ts +2 -0
- package/lib/esm/rules/prefer_css_attribute_for_eui_components.js +63 -0
- package/lib/esm/rules/prefer_css_attribute_for_eui_components.js.map +1 -0
- package/lib/esm/utils/resolve_member_expression_root.d.ts +2 -0
- package/lib/esm/utils/resolve_member_expression_root.js +11 -0
- package/lib/esm/utils/resolve_member_expression_root.js.map +1 -0
- package/package.json +53 -10
- package/rules/href_or_on_click.js +0 -35
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@ This package contains an eslint plugin that enforces some default rules for usin
|
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
7
|
-
1.
|
|
8
|
-
2.
|
|
7
|
+
1. Install `@elastic/eslint-plugin-eui` as a dev dependency.
|
|
8
|
+
2. Extend `plugin:@elastic/eui/recommended` in your ESLint config.
|
|
9
9
|
|
|
10
10
|
## Rules
|
|
11
11
|
|
|
@@ -13,13 +13,149 @@ This package contains an eslint plugin that enforces some default rules for usin
|
|
|
13
13
|
|
|
14
14
|
`<EuiButton />` should either be a button or a link, for a11y purposes. When given an `href` the button behaves as a link, otherwise an `onClick` handler is expected and it will behave as a button.
|
|
15
15
|
|
|
16
|
-
In some cases it makes sense to disable this rule locally, such as when <kbd>cmd</kbd
|
|
16
|
+
In some cases it makes sense to disable this rule locally, such as when <kbd>cmd</kbd> + click should open the link in a new tab, but a standard click should use the `history.pushState()` API to change the URL without triggering a full page load.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
### `@elastic/eui/no-restricted-eui-imports`
|
|
19
|
+
|
|
20
|
+
At times, we deprecate features that may need more highlighting and/or that are not possible to annotate with JSDoc `@deprecated`, e.g. JSON token imports: `@elastic/eui/dist/eui_theme_*.json` (for context: https://github.com/elastic/kibana/issues/199715#json-tokens).
|
|
21
|
+
|
|
22
|
+
We don't use `no-restricted-imports` because ESLint doesn't allow multiple error levels at once and it may conflict with the consumer's existing ESLint configuration for that rule. We need to assure that our rule will produce a warning (as a recommendation).
|
|
23
|
+
|
|
24
|
+
All deprecations still must follow our [deprecation process](../../wiki/eui-team-processes/deprecations.md).
|
|
25
|
+
|
|
26
|
+
### `@elastic/eui/no-css-color`
|
|
27
|
+
|
|
28
|
+
This rule warns engineers to not use literal css color in the codebase, particularly for CSS properties that apply color to either the html element or text nodes, but rather urge users to defer to using the color tokens provided by EUI.
|
|
29
|
+
|
|
30
|
+
This rule kicks in on the following JSXAttributes; `style`, `className` and `css` and supports various approaches to providing styling declarations.
|
|
31
|
+
|
|
32
|
+
#### Example
|
|
33
|
+
|
|
34
|
+
The following code:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Filename: /x-pack/plugins/observability_solution/observability/public/my_component.tsx
|
|
38
|
+
|
|
39
|
+
import React from 'react';
|
|
40
|
+
import { EuiText } from '@elastic/eui';
|
|
41
|
+
|
|
42
|
+
function MyComponent() {
|
|
43
|
+
return (
|
|
44
|
+
<EuiText style={{ color: 'red' }}>You know, for search</EuiText>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// Filename: /x-pack/plugins/observability_solution/observability/public/my_component.tsx
|
|
51
|
+
|
|
52
|
+
import React from 'react';
|
|
53
|
+
import { EuiText } from '@elastic/eui';
|
|
54
|
+
|
|
55
|
+
function MyComponent() {
|
|
56
|
+
|
|
57
|
+
const style = {
|
|
58
|
+
color: 'red'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<EuiText style={{ color: style.color }}>You know, for search</EuiText>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
// Filename: /x-pack/plugins/observability_solution/observability/public/my_component.tsx
|
|
69
|
+
|
|
70
|
+
import React from 'react';
|
|
71
|
+
import { EuiText } from '@elastic/eui';
|
|
72
|
+
|
|
73
|
+
function MyComponent() {
|
|
74
|
+
const colorValue = '#dd4040';
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<EuiText style={{ color: colorValue }}>You know, for search</EuiText>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
will all raise an eslint report with an appropriate message of severity that matches the configuration of the rule, further more all the examples above
|
|
83
|
+
will also match for when the attribute in question is `css`. The `css` attribute will also raise a report the following cases below;
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// Filename: /x-pack/plugins/observability_solution/observability/public/my_component.tsx
|
|
19
87
|
|
|
20
|
-
|
|
88
|
+
import React from 'react';
|
|
89
|
+
import { css } from '@emotion/css';
|
|
90
|
+
import { EuiText } from '@elastic/eui';
|
|
91
|
+
|
|
92
|
+
function MyComponent() {
|
|
93
|
+
return (
|
|
94
|
+
<EuiText css={css`color: '#dd4040' `}>You know, for search</EuiText>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
// Filename: /x-pack/plugins/observability_solution/observability/public/my_component.tsx
|
|
101
|
+
|
|
102
|
+
import React from 'react';
|
|
103
|
+
import { EuiText } from '@elastic/eui';
|
|
104
|
+
|
|
105
|
+
function MyComponent() {
|
|
106
|
+
return (
|
|
107
|
+
<EuiText css={() => ({ color: '#dd4040' })}>You know, for search</EuiText>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
A special case is also covered for the `className` attribute, where the rule will also raise a report for the following case below;
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
// Filename: /x-pack/plugins/observability_solution/observability/public/my_component.tsx
|
|
117
|
+
|
|
118
|
+
import React from 'react';
|
|
119
|
+
import { css } from '@emotion/css';
|
|
120
|
+
import { EuiText } from '@elastic/eui';
|
|
121
|
+
|
|
122
|
+
function MyComponent() {
|
|
123
|
+
return (
|
|
124
|
+
<EuiText className={css`color: '#dd4040'`}>You know, for search</EuiText>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
It's worth pointing out that although the examples provided are specific to EUI components, this rule applies to all JSX elements.
|
|
130
|
+
|
|
131
|
+
### `@elastic/eui/prefer-css-attributes-for-eui-components`
|
|
132
|
+
|
|
133
|
+
This rule warns about the use of `style` attribute and encourages to replace it with `css` attribute. Using the `css` attribute ensures better integration with Emotion's styling capabilities.
|
|
134
|
+
|
|
135
|
+
#### Example
|
|
136
|
+
|
|
137
|
+
The following code:
|
|
138
|
+
|
|
139
|
+
```jsx
|
|
140
|
+
<EuiCode style={{ color: '#dd4040' }}>This is a test</EuiCode>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
will raise an ESLint report and suggest replacing the `style` attribute with `css`:
|
|
144
|
+
|
|
145
|
+
```jsx
|
|
146
|
+
<EuiCode css={{ color: '#dd4040' }}>This is a test</EuiCode>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Testing
|
|
150
|
+
|
|
151
|
+
### Against an existing package
|
|
152
|
+
|
|
153
|
+
To test the local changes to the plugin, you must:
|
|
154
|
+
|
|
155
|
+
1. Run `yarn pack` in the directory.
|
|
156
|
+
2. In your project's `package.json`, point `@elastic/eslint-plugin-eui` to `file:/path/to/package.tgz`.
|
|
157
|
+
3. Install dependencies: `yarn kbn bootstrap --no-validate`.
|
|
158
|
+
|
|
159
|
+
## Publishing
|
|
21
160
|
|
|
22
|
-
|
|
23
|
-
2. commit version bump
|
|
24
|
-
3. `npm publish` in this directory
|
|
25
|
-
4. push the version bump upstream
|
|
161
|
+
Refer to the [wiki](../../wiki/eui-team-processes/releasing-versions.md) for instructions on how to release this package.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":""}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _href_or_on_click = require("./rules/href_or_on_click");
|
|
4
|
+
var _no_restricted_eui_imports = require("./rules/no_restricted_eui_imports");
|
|
5
|
+
var _no_css_color = require("./rules/no_css_color");
|
|
6
|
+
var _prefer_css_attribute_for_eui_components = require("./rules/prefer_css_attribute_for_eui_components");
|
|
1
7
|
/*
|
|
2
8
|
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
3
9
|
* license agreements. See the NOTICE file distributed with
|
|
@@ -17,16 +23,23 @@
|
|
|
17
23
|
* under the License.
|
|
18
24
|
*/
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
const config = {
|
|
21
27
|
rules: {
|
|
22
|
-
'href-or-on-click':
|
|
28
|
+
'href-or-on-click': _href_or_on_click.HrefOnClick,
|
|
29
|
+
'no-restricted-eui-imports': _no_restricted_eui_imports.NoRestrictedEuiImports,
|
|
30
|
+
'no-css-color': _no_css_color.NoCssColor,
|
|
31
|
+
'prefer-css-attributes-for-eui-components': _prefer_css_attribute_for_eui_components.PreferCSSAttributeForEuiComponents
|
|
23
32
|
},
|
|
24
33
|
configs: {
|
|
25
34
|
recommended: {
|
|
26
35
|
plugins: ['@elastic/eslint-plugin-eui'],
|
|
27
36
|
rules: {
|
|
28
|
-
'@elastic/eui/href-or-on-click': '
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
'@elastic/eui/href-or-on-click': 'warn',
|
|
38
|
+
'@elastic/eui/no-restricted-eui-imports': 'warn',
|
|
39
|
+
'@elastic/eui/no-css-color': 'warn',
|
|
40
|
+
'@elastic/eui/prefer-css-attributes-for-eui-components': 'warn'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
32
44
|
};
|
|
45
|
+
module.exports = config;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"href_or_on_click.d.ts","sourceRoot":"","sources":["../../../src/rules/href_or_on_click.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAY,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAIjE,eAAO,MAAM,WAAW,gFA8CtB,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.HrefOnClick = void 0;
|
|
7
|
+
var _utils = require("@typescript-eslint/utils");
|
|
8
|
+
/*
|
|
9
|
+
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
10
|
+
* license agreements. See the NOTICE file distributed with
|
|
11
|
+
* this work for additional information regarding copyright
|
|
12
|
+
* ownership. Elasticsearch B.V. licenses this file to you under
|
|
13
|
+
* the Apache License, Version 2.0 (the "License"); you may
|
|
14
|
+
* not use this file except in compliance with the License.
|
|
15
|
+
* You may obtain a copy of the License at
|
|
16
|
+
*
|
|
17
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
18
|
+
*
|
|
19
|
+
* Unless required by applicable law or agreed to in writing,
|
|
20
|
+
* software distributed under the License is distributed on an
|
|
21
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
22
|
+
* KIND, either express or implied. See the License for the
|
|
23
|
+
* specific language governing permissions and limitations
|
|
24
|
+
* under the License.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const componentNames = ['EuiButton', 'EuiButtonEmpty', 'EuiLink', 'EuiBadge'];
|
|
28
|
+
const HrefOnClick = exports.HrefOnClick = _utils.ESLintUtils.RuleCreator.withoutDocs({
|
|
29
|
+
create(context) {
|
|
30
|
+
return {
|
|
31
|
+
JSXOpeningElement(node) {
|
|
32
|
+
// Ensure node name is one of the valid component names
|
|
33
|
+
if (node.name.type !== 'JSXIdentifier' || !componentNames.includes(node.name.name)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check if the node has both `href` and `onClick` attributes
|
|
38
|
+
const hasHref = node.attributes.some(attr => attr.type === 'JSXAttribute' && attr.name.name === 'href');
|
|
39
|
+
const hasOnClick = node.attributes.some(attr => attr.type === 'JSXAttribute' && attr.name.name === 'onClick');
|
|
40
|
+
|
|
41
|
+
// Report an issue if both attributes are present
|
|
42
|
+
if (hasHref && hasOnClick) {
|
|
43
|
+
context.report({
|
|
44
|
+
node,
|
|
45
|
+
messageId: 'hrefOrOnClick',
|
|
46
|
+
data: {
|
|
47
|
+
name: node.name.name
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
meta: {
|
|
55
|
+
type: 'problem',
|
|
56
|
+
docs: {
|
|
57
|
+
description: 'Discourage supplying both `href` and `onClick` to certain EUI components.'
|
|
58
|
+
},
|
|
59
|
+
schema: [],
|
|
60
|
+
messages: {
|
|
61
|
+
hrefOrOnClick: '<{{name}}> supplied with both `href` and `onClick`; is this intentional? (Valid use cases include programmatic navigation via `onClick` while preserving "Open in new tab" style functionality via `href`.)'
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
defaultOptions: []
|
|
65
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no_css_color.d.ts","sourceRoot":"","sources":["../../../src/rules/no_css_color.ts"],"names":[],"mappings":"AAUA,OAAO,EAAY,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAyNjE,eAAO,MAAM,UAAU,2IAsRrB,CAAC"}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.NoCssColor = void 0;
|
|
7
|
+
var _cssstyle = require("cssstyle");
|
|
8
|
+
var _utils = require("@typescript-eslint/utils");
|
|
9
|
+
var _resolve_member_expression_root = require("../utils/resolve_member_expression_root");
|
|
10
|
+
/*
|
|
11
|
+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
12
|
+
* or more contributor license agreements. Licensed under the "Elastic License
|
|
13
|
+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
|
14
|
+
* Public License v 1"; you may not use this file except in compliance with, at
|
|
15
|
+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
|
16
|
+
* License v3.0 only", or the "Server Side Public License, v 1".
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @description List of superset css properties that can apply color to html box element elements and text nodes, leveraging the
|
|
21
|
+
* css style package allows us to directly singly check for these properties even if the actual declaration was written using the shorthand form
|
|
22
|
+
*/
|
|
23
|
+
const propertiesSupportingCssColor = ['color', 'background', 'border'];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @description Builds off the existing color definition to match css declarations that can apply color to
|
|
27
|
+
* html elements and text nodes for string declarations
|
|
28
|
+
*/
|
|
29
|
+
const htmlElementColorDeclarationRegex = RegExp(String.raw`(${propertiesSupportingCssColor.join('|')})`);
|
|
30
|
+
const checkPropertySpecifiesInvalidCSSColor = ([property, value]) => {
|
|
31
|
+
if (!property || !value) return false;
|
|
32
|
+
const style = new _cssstyle.CSSStyleDeclaration();
|
|
33
|
+
|
|
34
|
+
// @ts-expect-error the types for this packages specifies an index signature of number, alongside other valid CSS properties
|
|
35
|
+
style[property.trim()] = typeof value === 'string' ? value.trim() : value;
|
|
36
|
+
const anchor = propertiesSupportingCssColor.find(resolvedProperty => property.includes(resolvedProperty));
|
|
37
|
+
if (!anchor) return false;
|
|
38
|
+
|
|
39
|
+
// build the resolved color property to check if the value is a string after parsing the style declaration
|
|
40
|
+
const resolvedColorProperty = anchor === 'color' ? 'color' : anchor + 'Color';
|
|
41
|
+
|
|
42
|
+
// in trying to keep this rule simple, it's enough if we get a value back, because if it was an identifier we would have been able to set a value within this invocation
|
|
43
|
+
// @ts-expect-error the types for this packages specifics an index signature of number, alongside other valid CSS properties
|
|
44
|
+
return Boolean(style[resolvedColorProperty]);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @description method to inspect values of interest found on an object
|
|
49
|
+
*/
|
|
50
|
+
const raiseReportIfPropertyHasInvalidCssColor = (context, propertyNode, messageToReport) => {
|
|
51
|
+
let didReport = false;
|
|
52
|
+
if (propertyNode.key.type === 'Identifier' && !htmlElementColorDeclarationRegex.test(propertyNode.key.name)) {
|
|
53
|
+
return didReport;
|
|
54
|
+
}
|
|
55
|
+
if (propertyNode.value.type === 'Literal') {
|
|
56
|
+
if (didReport = checkPropertySpecifiesInvalidCSSColor([
|
|
57
|
+
// @ts-expect-error the key name is present in this scenario
|
|
58
|
+
propertyNode.key.name, propertyNode.value.value])) {
|
|
59
|
+
context.report(messageToReport);
|
|
60
|
+
}
|
|
61
|
+
} else if (propertyNode.value.type === 'Identifier') {
|
|
62
|
+
const identifierDeclaration = context.sourceCode.getScope(propertyNode).variables.find(variable => variable.name === propertyNode.value.name);
|
|
63
|
+
const identifierDeclarationInit = (identifierDeclaration?.defs[0].node).init;
|
|
64
|
+
if (identifierDeclarationInit?.type === 'Literal' && checkPropertySpecifiesInvalidCSSColor([
|
|
65
|
+
// @ts-expect-error the key name is present in this scenario
|
|
66
|
+
propertyNode.key.name, identifierDeclarationInit.value])) {
|
|
67
|
+
context.report({
|
|
68
|
+
loc: propertyNode.value.loc,
|
|
69
|
+
messageId: 'noCSSColorSpecificDeclaredVariable',
|
|
70
|
+
data: {
|
|
71
|
+
// @ts-expect-error the key name is always present else this code will not execute
|
|
72
|
+
property: String(propertyNode.key.name),
|
|
73
|
+
line: String(propertyNode.value.loc.start.line),
|
|
74
|
+
variableName: propertyNode.value.name
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
didReport = true;
|
|
78
|
+
}
|
|
79
|
+
} else if (propertyNode.value.type === 'MemberExpression') {
|
|
80
|
+
// @ts-expect-error we ignore the case where this node could be a private identifier
|
|
81
|
+
const MemberExpressionLeafName = propertyNode.value.property.name;
|
|
82
|
+
const memberExpressionRootName = (0, _resolve_member_expression_root.resolveMemberExpressionRoot)(propertyNode.value).name;
|
|
83
|
+
const expressionRootDeclaration = context.sourceCode.getScope(propertyNode).variables.find(variable => variable.name === memberExpressionRootName);
|
|
84
|
+
const expressionRootDeclarationInit = (expressionRootDeclaration?.defs[0].node).init;
|
|
85
|
+
if (expressionRootDeclarationInit?.type === 'ObjectExpression') {
|
|
86
|
+
expressionRootDeclarationInit.properties.forEach(property => {
|
|
87
|
+
// This is a naive approach expecting the value to be at depth 1, we should actually be traversing the object to the same depth as the expression
|
|
88
|
+
if (property.type === 'Property' && property.key.type === 'Identifier' && property.key?.name === MemberExpressionLeafName) {
|
|
89
|
+
raiseReportIfPropertyHasInvalidCssColor(context, property, {
|
|
90
|
+
loc: propertyNode.value.loc,
|
|
91
|
+
messageId: 'noCSSColorSpecificDeclaredVariable',
|
|
92
|
+
data: {
|
|
93
|
+
// @ts-expect-error the key name is always present else this code will not execute
|
|
94
|
+
property: String(propertyNode.key.name),
|
|
95
|
+
line: String(propertyNode.value.loc.start.line),
|
|
96
|
+
variableName: memberExpressionRootName
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
} else if (expressionRootDeclarationInit?.type === 'CallExpression') {
|
|
102
|
+
// TODO: if this object was returned from invoking a function the best we can do is probably validate that the method invoked is one that returns an euitheme object
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return didReport;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
*
|
|
110
|
+
* @description style object declaration have a depth of 1, this function handles the properties of the object
|
|
111
|
+
*/
|
|
112
|
+
const handleObjectProperties = (context, propertyParentNode, property, reportMessage) => {
|
|
113
|
+
if (property.type === 'Property') {
|
|
114
|
+
raiseReportIfPropertyHasInvalidCssColor(context, property, reportMessage);
|
|
115
|
+
} else if (property.type === 'SpreadElement') {
|
|
116
|
+
const spreadElementIdentifierName = property.argument.name;
|
|
117
|
+
const spreadElementDeclaration = context.sourceCode.getScope(propertyParentNode.value.expression).references.find(ref => ref.identifier.name === spreadElementIdentifierName)?.resolved;
|
|
118
|
+
if (!spreadElementDeclaration) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
reportMessage = {
|
|
122
|
+
loc: propertyParentNode.loc,
|
|
123
|
+
messageId: 'noCSSColorSpecificDeclaredVariable',
|
|
124
|
+
data: {
|
|
125
|
+
// @ts-expect-error the key name is always present else this code will not execute
|
|
126
|
+
property: String(property.argument.name),
|
|
127
|
+
variableName: spreadElementIdentifierName,
|
|
128
|
+
line: String(property.loc.start.line)
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const spreadElementDeclarationNode = 'init' in spreadElementDeclaration.defs[0].node ? spreadElementDeclaration.defs[0].node.init : undefined;
|
|
132
|
+
|
|
133
|
+
// evaluate only statically defined declarations, other possibilities like callExpressions in this context complicate things
|
|
134
|
+
if (spreadElementDeclarationNode?.type === 'ObjectExpression') {
|
|
135
|
+
spreadElementDeclarationNode.properties.forEach(spreadProperty => {
|
|
136
|
+
handleObjectProperties(context, propertyParentNode, spreadProperty, reportMessage);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const NoCssColor = exports.NoCssColor = _utils.ESLintUtils.RuleCreator.withoutDocs({
|
|
142
|
+
create(context) {
|
|
143
|
+
return {
|
|
144
|
+
// accounts for instances where declarations are created using the template tagged css function
|
|
145
|
+
TaggedTemplateExpression(node) {
|
|
146
|
+
if (node.tag.type !== 'Identifier' || node.tag.type === 'Identifier' && node.tag.name !== 'css') {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
for (let i = 0; i < node.quasi.quasis.length; i++) {
|
|
150
|
+
const declarationTemplateNode = node.quasi.quasis[i];
|
|
151
|
+
if (htmlElementColorDeclarationRegex.test(declarationTemplateNode.value.raw)) {
|
|
152
|
+
const cssText = declarationTemplateNode.value.raw.replace(/(\{|\}|\\n)/g, '').trim();
|
|
153
|
+
cssText.split(';').forEach(declaration => {
|
|
154
|
+
if (declaration.length > 0 && checkPropertySpecifiesInvalidCSSColor(declaration.split(':'))) {
|
|
155
|
+
context.report({
|
|
156
|
+
node: declarationTemplateNode,
|
|
157
|
+
messageId: 'noCssColor'
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
JSXAttribute(node) {
|
|
165
|
+
if (!(node.name.name === 'style' || node.name.name === 'css')) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @description Accounts for instances where a variable is used to define a style object
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* const codeStyle = { color: '#dd4040' };
|
|
174
|
+
* <EuiCode style={codeStyle}>This is an example</EuiCode>
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* const codeStyle = { color: '#dd4040' };
|
|
178
|
+
* <EuiCode css={codeStyle}>This is an example</EuiCode>
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* const codeStyle = css({ color: '#dd4040' });
|
|
182
|
+
* <EuiCode css={codeStyle}>This is an example</EuiCode>
|
|
183
|
+
*/
|
|
184
|
+
if (node.value?.type === 'JSXExpressionContainer' && node.value.expression.type === 'Identifier') {
|
|
185
|
+
const styleVariableName = node.value.expression.name;
|
|
186
|
+
const nodeScope = context.sourceCode.getScope(node.value.expression);
|
|
187
|
+
const variableDeclarationMatches = nodeScope.references.find(ref => ref.identifier.name === styleVariableName)?.resolved;
|
|
188
|
+
let variableInitializationNode;
|
|
189
|
+
if (variableInitializationNode = variableDeclarationMatches?.defs?.[0]?.node && 'init' in variableDeclarationMatches.defs[0].node && variableDeclarationMatches.defs[0].node.init) {
|
|
190
|
+
if (variableInitializationNode.type === 'ObjectExpression') {
|
|
191
|
+
variableInitializationNode.properties.forEach(property => {
|
|
192
|
+
handleObjectProperties(context, node, property, {
|
|
193
|
+
loc: property.loc,
|
|
194
|
+
messageId: 'noCSSColorSpecificDeclaredVariable',
|
|
195
|
+
data: {
|
|
196
|
+
property: property.type === 'SpreadElement' ? String(property.argument.name) : String(property.key.name),
|
|
197
|
+
variableName: styleVariableName,
|
|
198
|
+
line: String(property.loc.start.line)
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
} else if (variableInitializationNode.type === 'CallExpression' && variableInitializationNode.callee.name === 'css') {
|
|
203
|
+
const cssFunctionArgument = variableInitializationNode.arguments[0];
|
|
204
|
+
if (cssFunctionArgument.type === 'ObjectExpression') {
|
|
205
|
+
cssFunctionArgument.properties.forEach(property => {
|
|
206
|
+
handleObjectProperties(context, node, property, {
|
|
207
|
+
loc: node.loc,
|
|
208
|
+
messageId: 'noCSSColorSpecificDeclaredVariable',
|
|
209
|
+
data: {
|
|
210
|
+
property: property.type === 'SpreadElement' ? String(property.argument.name) : String(property.key.name),
|
|
211
|
+
variableName: styleVariableName,
|
|
212
|
+
line: String(property.loc.start.line)
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
*
|
|
224
|
+
* @description Accounts for instances where a style object is inlined in the JSX attribute
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* <EuiCode style={{ color: '#dd4040' }}>This is an example</EuiCode>
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* <EuiCode css={{ color: '#dd4040' }}>This is an example</EuiCode>
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* const styleRules = { color: '#dd4040' };
|
|
234
|
+
* <EuiCode style={{ color: styleRules.color }}>This is an example</EuiCode>
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* const styleRules = { color: '#dd4040' };
|
|
238
|
+
* <EuiCode css={{ color: styleRules.color }}>This is an example</EuiCode>
|
|
239
|
+
*/
|
|
240
|
+
if (node.value?.type === 'JSXExpressionContainer' && node.value.expression.type === 'ObjectExpression') {
|
|
241
|
+
const declarationPropertiesNode = node.value.expression.properties;
|
|
242
|
+
declarationPropertiesNode?.forEach(property => {
|
|
243
|
+
handleObjectProperties(context, node, property, {
|
|
244
|
+
loc: property.loc,
|
|
245
|
+
messageId: 'noCssColorSpecific',
|
|
246
|
+
data: {
|
|
247
|
+
property: property.type === 'SpreadElement' ?
|
|
248
|
+
// @ts-expect-error the key name is always present else this code will not execute
|
|
249
|
+
String(property.argument.name) :
|
|
250
|
+
// @ts-expect-error the key name is always present else this code will not execute
|
|
251
|
+
String(property.key.name)
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (node.name.name === 'css' && node.value?.type === 'JSXExpressionContainer') {
|
|
258
|
+
/**
|
|
259
|
+
* @example
|
|
260
|
+
* <EuiCode css={`{ color: '#dd4040' }`}>This is an example</EuiCode>
|
|
261
|
+
*/
|
|
262
|
+
if (node.value.expression.type === 'TemplateLiteral') {
|
|
263
|
+
for (let i = 0; i < node.value.expression.quasis.length; i++) {
|
|
264
|
+
const declarationTemplateNode = node.value.expression.quasis[i];
|
|
265
|
+
if (htmlElementColorDeclarationRegex.test(declarationTemplateNode.value.raw)) {
|
|
266
|
+
const cssText = declarationTemplateNode.value.raw.replace(/(\{|\}|\\n)/g, '').trim();
|
|
267
|
+
cssText.split(';').forEach(declaration => {
|
|
268
|
+
if (declaration.length > 0 && checkPropertySpecifiesInvalidCSSColor(declaration.split(':'))) {
|
|
269
|
+
context.report({
|
|
270
|
+
node: declarationTemplateNode,
|
|
271
|
+
messageId: 'noCssColor'
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @example
|
|
281
|
+
* <EuiCode css={() => ({ color: '#dd4040' })}>This is an example</EuiCode>
|
|
282
|
+
*/
|
|
283
|
+
if (node.value.expression.type === 'FunctionExpression' || node.value.expression.type === 'ArrowFunctionExpression') {
|
|
284
|
+
let declarationPropertiesNode = [];
|
|
285
|
+
if (node.value.expression.body.type === 'ObjectExpression') {
|
|
286
|
+
declarationPropertiesNode = node.value.expression.body.properties;
|
|
287
|
+
}
|
|
288
|
+
if (node.value.expression.body.type === 'BlockStatement') {
|
|
289
|
+
const functionReturnStatementNode = node.value.expression.body.body?.find(_node => {
|
|
290
|
+
return _node.type === 'ReturnStatement';
|
|
291
|
+
});
|
|
292
|
+
if (!functionReturnStatementNode) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
declarationPropertiesNode = functionReturnStatementNode.argument?.properties.filter(property => property.type === 'Property');
|
|
296
|
+
}
|
|
297
|
+
if (!declarationPropertiesNode.length) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
declarationPropertiesNode.forEach(property => {
|
|
301
|
+
handleObjectProperties(context, node, property, {
|
|
302
|
+
loc: property.loc,
|
|
303
|
+
messageId: 'noCssColorSpecific',
|
|
304
|
+
data: {
|
|
305
|
+
// @ts-expect-error the key name is always present else this code will not execute
|
|
306
|
+
property: property.key.name
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
},
|
|
316
|
+
meta: {
|
|
317
|
+
type: 'suggestion',
|
|
318
|
+
docs: {
|
|
319
|
+
description: 'Use color definitions from eui theme as opposed to CSS color values'
|
|
320
|
+
},
|
|
321
|
+
messages: {
|
|
322
|
+
noCSSColorSpecificDeclaredVariable: 'Avoid using a literal CSS color value for "{{property}}", use an EUI theme color instead in declared variable {{variableName}} on line {{line}}',
|
|
323
|
+
noCssColorSpecific: 'Avoid using a literal CSS color value for "{{property}}", use an EUI theme color instead',
|
|
324
|
+
noCssColor: 'Avoid using a literal CSS color value, use an EUI theme color instead'
|
|
325
|
+
},
|
|
326
|
+
schema: []
|
|
327
|
+
},
|
|
328
|
+
defaultOptions: []
|
|
329
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type Option = {
|
|
3
|
+
patterns: string[];
|
|
4
|
+
message: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const NoRestrictedEuiImports: TSESLint.RuleModule<never, Readonly<Option>[]>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=no_restricted_eui_imports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no_restricted_eui_imports.d.ts","sourceRoot":"","sources":["../../../src/rules/no_restricted_eui_imports.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAY,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAE9D,KAAK,MAAM,GAAG;IACZ,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAUF,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAAC,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,CA4CjF,CAAC"}
|