@atlaskit/eslint-plugin-design-system 8.25.2 → 8.26.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 +17 -0
- package/README.md +4 -0
- package/constellation/index/usage.mdx +362 -0
- package/dist/cjs/presets/all.codegen.js +5 -1
- package/dist/cjs/presets/recommended.codegen.js +5 -1
- package/dist/cjs/rules/index.codegen.js +9 -1
- package/dist/cjs/rules/no-empty-styled-expression/index.js +75 -0
- package/dist/cjs/rules/no-exported-css/index.js +37 -0
- package/dist/cjs/rules/no-exported-keyframes/index.js +37 -0
- package/dist/cjs/rules/no-invalid-css-map/index.js +102 -0
- package/dist/cjs/rules/no-invalid-css-map/utils.js +193 -0
- package/dist/cjs/rules/utils/create-no-exported-rule/check-if-supported-export.js +158 -0
- package/dist/cjs/rules/utils/create-no-exported-rule/is-styled-component.js +84 -0
- package/dist/cjs/rules/utils/create-no-exported-rule/main.js +66 -0
- package/dist/cjs/rules/utils/is-supported-import.js +51 -15
- package/dist/es2019/presets/all.codegen.js +5 -1
- package/dist/es2019/presets/recommended.codegen.js +5 -1
- package/dist/es2019/rules/index.codegen.js +9 -1
- package/dist/es2019/rules/no-empty-styled-expression/index.js +65 -0
- package/dist/es2019/rules/no-exported-css/index.js +31 -0
- package/dist/es2019/rules/no-exported-keyframes/index.js +31 -0
- package/dist/es2019/rules/no-invalid-css-map/index.js +95 -0
- package/dist/es2019/rules/no-invalid-css-map/utils.js +134 -0
- package/dist/es2019/rules/utils/create-no-exported-rule/check-if-supported-export.js +142 -0
- package/dist/es2019/rules/utils/create-no-exported-rule/is-styled-component.js +74 -0
- package/dist/es2019/rules/utils/create-no-exported-rule/main.js +59 -0
- package/dist/es2019/rules/utils/is-supported-import.js +48 -14
- package/dist/esm/presets/all.codegen.js +5 -1
- package/dist/esm/presets/recommended.codegen.js +5 -1
- package/dist/esm/rules/index.codegen.js +9 -1
- package/dist/esm/rules/no-empty-styled-expression/index.js +68 -0
- package/dist/esm/rules/no-exported-css/index.js +31 -0
- package/dist/esm/rules/no-exported-keyframes/index.js +31 -0
- package/dist/esm/rules/no-invalid-css-map/index.js +96 -0
- package/dist/esm/rules/no-invalid-css-map/utils.js +186 -0
- package/dist/esm/rules/utils/create-no-exported-rule/check-if-supported-export.js +151 -0
- package/dist/esm/rules/utils/create-no-exported-rule/is-styled-component.js +78 -0
- package/dist/esm/rules/utils/create-no-exported-rule/main.js +60 -0
- package/dist/esm/rules/utils/is-supported-import.js +49 -14
- package/dist/types/index.codegen.d.ts +8 -0
- package/dist/types/presets/all.codegen.d.ts +5 -1
- package/dist/types/presets/recommended.codegen.d.ts +5 -1
- package/dist/types/rules/index.codegen.d.ts +4 -0
- package/dist/types/rules/no-empty-styled-expression/index.d.ts +3 -0
- package/dist/types/rules/no-exported-css/index.d.ts +3 -0
- package/dist/types/rules/no-exported-keyframes/index.d.ts +3 -0
- package/dist/types/rules/no-invalid-css-map/index.d.ts +3 -0
- package/dist/types/rules/no-invalid-css-map/utils.d.ts +14 -0
- package/dist/types/rules/utils/create-no-exported-rule/check-if-supported-export.d.ts +15 -0
- package/dist/types/rules/utils/create-no-exported-rule/is-styled-component.d.ts +14 -0
- package/dist/types/rules/utils/create-no-exported-rule/main.d.ts +19 -0
- package/dist/types/rules/utils/create-rule.d.ts +1 -1
- package/dist/types/rules/utils/is-supported-import.d.ts +25 -8
- package/dist/types-ts4.5/index.codegen.d.ts +8 -0
- package/dist/types-ts4.5/presets/all.codegen.d.ts +5 -1
- package/dist/types-ts4.5/presets/recommended.codegen.d.ts +5 -1
- package/dist/types-ts4.5/rules/index.codegen.d.ts +4 -0
- package/dist/types-ts4.5/rules/no-empty-styled-expression/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/no-exported-css/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/no-exported-keyframes/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/no-invalid-css-map/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/no-invalid-css-map/utils.d.ts +14 -0
- package/dist/types-ts4.5/rules/utils/create-no-exported-rule/check-if-supported-export.d.ts +15 -0
- package/dist/types-ts4.5/rules/utils/create-no-exported-rule/is-styled-component.d.ts +14 -0
- package/dist/types-ts4.5/rules/utils/create-no-exported-rule/main.d.ts +19 -0
- package/dist/types-ts4.5/rules/utils/create-rule.d.ts +1 -1
- package/dist/types-ts4.5/rules/utils/is-supported-import.d.ts +25 -8
- package/package.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
3
4
|
Object.defineProperty(exports, "__esModule", {
|
|
4
5
|
value: true
|
|
5
6
|
});
|
|
6
|
-
exports.isCxFunction = exports.isCss = exports.CSS_IN_JS_IMPORTS = void 0;
|
|
7
|
+
exports.isStyled = exports.isKeyframes = exports.isCxFunction = exports.isCssMap = exports.isCss = exports.getImportSources = exports.DEFAULT_IMPORT_SOURCES = exports.CSS_IN_JS_IMPORTS = void 0;
|
|
8
|
+
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
|
|
7
9
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
8
10
|
|
|
9
11
|
var CSS_IN_JS_IMPORTS = exports.CSS_IN_JS_IMPORTS = {
|
|
@@ -14,32 +16,65 @@ var CSS_IN_JS_IMPORTS = exports.CSS_IN_JS_IMPORTS = {
|
|
|
14
16
|
atlaskitCss: '@atlaskit/css'
|
|
15
17
|
};
|
|
16
18
|
|
|
17
|
-
//
|
|
19
|
+
// A CSS-in-JS library an import of a valid css, cx, cssMap, etc.
|
|
18
20
|
// function might originate from, e.g. @compiled/react, @emotion/core.
|
|
19
21
|
|
|
22
|
+
// All ESLint rules originating from `@compiled/eslint-plugin` should apply to these libraries.
|
|
23
|
+
var DEFAULT_IMPORT_SOURCES = exports.DEFAULT_IMPORT_SOURCES = [CSS_IN_JS_IMPORTS.compiled, CSS_IN_JS_IMPORTS.atlaskitCss];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Given the ESLint rule context, extract and parse the value of the importSources rule option.
|
|
27
|
+
* The importSources option is used to define additional libraries for which an ESLint rule
|
|
28
|
+
* should apply to.
|
|
29
|
+
*
|
|
30
|
+
* Note that `@compiled/react` and `@atlaskit/css` are always included in importSources, regardless
|
|
31
|
+
* of what importSources is configured to by the user.
|
|
32
|
+
*
|
|
33
|
+
* @param context The rule context.
|
|
34
|
+
* @returns An array of strings representing what CSS-in-JS packages that should be checked, based
|
|
35
|
+
* on the rule options configuration.
|
|
36
|
+
*/
|
|
37
|
+
var getImportSources = exports.getImportSources = function getImportSources(context) {
|
|
38
|
+
var options = context.options;
|
|
39
|
+
if (!options.length) {
|
|
40
|
+
return DEFAULT_IMPORT_SOURCES;
|
|
41
|
+
}
|
|
42
|
+
if (options[0].importSources && Array.isArray(options[0].importSources)) {
|
|
43
|
+
return [].concat(DEFAULT_IMPORT_SOURCES, (0, _toConsumableArray2.default)(options[0].importSources));
|
|
44
|
+
}
|
|
45
|
+
return DEFAULT_IMPORT_SOURCES;
|
|
46
|
+
};
|
|
20
47
|
var isSupportedImportWrapper = function isSupportedImportWrapper(functionName) {
|
|
21
|
-
// This will need to be extended to support default imports once we start
|
|
22
|
-
// checking cases like `import css from '@emotion/css'`
|
|
23
48
|
var checkDefinitionHasImport = function checkDefinitionHasImport(def, importSources) {
|
|
24
|
-
|
|
25
|
-
|
|
49
|
+
if (def.type !== 'ImportBinding') {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (!def.parent || !importSources.includes(def.parent.source.value)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return (
|
|
56
|
+
// import { functionName } from 'import-source';
|
|
57
|
+
def.node.type === 'ImportSpecifier' && def.node.imported.name === functionName ||
|
|
58
|
+
// import functionName from 'import-source';
|
|
59
|
+
def.node.type === 'ImportDefaultSpecifier' && def.node.local.name === functionName
|
|
60
|
+
);
|
|
26
61
|
};
|
|
27
62
|
|
|
28
63
|
/**
|
|
29
64
|
* Checks whether:
|
|
30
65
|
*
|
|
31
|
-
* 1.
|
|
66
|
+
* 1. A function name `nodeToCheck` matches the name of the function we
|
|
32
67
|
* want to check for (e.g. `cx`, `css`, `cssMap`, or `keyframes`), and
|
|
33
|
-
* 2.
|
|
68
|
+
* 2. Whether `nodeToCheck` originates from one of the libraries listed
|
|
34
69
|
* in `importSources`.
|
|
35
70
|
*
|
|
36
|
-
* @param nodeToCheck
|
|
37
|
-
* @param referencesInScope
|
|
71
|
+
* @param nodeToCheck The function callee we are checking (e.g. The `css` in `css()`).
|
|
72
|
+
* @param referencesInScope List of references that are in scope. We'll use this
|
|
38
73
|
* to check where the function callee is imported from.
|
|
39
|
-
* @param importSources
|
|
40
|
-
* comes from
|
|
74
|
+
* @param importSources List of libraries that we want to ensure `nodeToCheck`
|
|
75
|
+
* comes from.
|
|
41
76
|
*
|
|
42
|
-
* @returns
|
|
77
|
+
* @returns Whether the above conditions are true.
|
|
43
78
|
*/
|
|
44
79
|
var isSupportedImport = function isSupportedImport(nodeToCheck, referencesInScope, importSources) {
|
|
45
80
|
return nodeToCheck.type === 'Identifier' && referencesInScope.some(function (reference) {
|
|
@@ -57,5 +92,6 @@ var isSupportedImportWrapper = function isSupportedImportWrapper(functionName) {
|
|
|
57
92
|
//
|
|
58
93
|
var isCss = exports.isCss = isSupportedImportWrapper('css');
|
|
59
94
|
var isCxFunction = exports.isCxFunction = isSupportedImportWrapper('cx');
|
|
60
|
-
|
|
61
|
-
|
|
95
|
+
var isCssMap = exports.isCssMap = isSupportedImportWrapper('cssMap');
|
|
96
|
+
var isKeyframes = exports.isKeyframes = isSupportedImportWrapper('keyframes');
|
|
97
|
+
var isStyled = exports.isStyled = isSupportedImportWrapper('styled');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* THIS FILE WAS CREATED VIA CODEGEN DO NOT MODIFY {@see http://go/af-codegen}
|
|
3
|
-
* @codegen <<SignedSource::
|
|
3
|
+
* @codegen <<SignedSource::914085544778f4543f43e3e30d0982e0>>
|
|
4
4
|
* @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
|
|
5
5
|
*/
|
|
6
6
|
export default {
|
|
@@ -16,6 +16,10 @@ export default {
|
|
|
16
16
|
'@atlaskit/design-system/no-deprecated-apis': 'error',
|
|
17
17
|
'@atlaskit/design-system/no-deprecated-design-token-usage': 'warn',
|
|
18
18
|
'@atlaskit/design-system/no-deprecated-imports': 'error',
|
|
19
|
+
'@atlaskit/design-system/no-empty-styled-expression': 'warn',
|
|
20
|
+
'@atlaskit/design-system/no-exported-css': 'warn',
|
|
21
|
+
'@atlaskit/design-system/no-exported-keyframes': 'warn',
|
|
22
|
+
'@atlaskit/design-system/no-invalid-css-map': 'error',
|
|
19
23
|
'@atlaskit/design-system/no-margin': 'warn',
|
|
20
24
|
'@atlaskit/design-system/no-nested-styles': 'error',
|
|
21
25
|
'@atlaskit/design-system/no-physical-properties': 'error',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* THIS FILE WAS CREATED VIA CODEGEN DO NOT MODIFY {@see http://go/af-codegen}
|
|
3
|
-
* @codegen <<SignedSource::
|
|
3
|
+
* @codegen <<SignedSource::577269c832952ce359cde6a50f26f4e0>>
|
|
4
4
|
* @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
|
|
5
5
|
*/
|
|
6
6
|
export default {
|
|
@@ -14,6 +14,10 @@ export default {
|
|
|
14
14
|
'@atlaskit/design-system/no-deprecated-apis': 'error',
|
|
15
15
|
'@atlaskit/design-system/no-deprecated-design-token-usage': 'warn',
|
|
16
16
|
'@atlaskit/design-system/no-deprecated-imports': 'error',
|
|
17
|
+
'@atlaskit/design-system/no-empty-styled-expression': 'warn',
|
|
18
|
+
'@atlaskit/design-system/no-exported-css': 'warn',
|
|
19
|
+
'@atlaskit/design-system/no-exported-keyframes': 'warn',
|
|
20
|
+
'@atlaskit/design-system/no-invalid-css-map': 'error',
|
|
17
21
|
'@atlaskit/design-system/no-nested-styles': 'error',
|
|
18
22
|
'@atlaskit/design-system/no-unsafe-design-token-usage': 'error',
|
|
19
23
|
'@atlaskit/design-system/no-unsafe-style-overrides': 'warn',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* THIS FILE WAS CREATED VIA CODEGEN DO NOT MODIFY {@see http://go/af-codegen}
|
|
3
|
-
* @codegen <<SignedSource::
|
|
3
|
+
* @codegen <<SignedSource::0a2d88c9772eb438048415f13550f592>>
|
|
4
4
|
* @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
|
|
5
5
|
*/
|
|
6
6
|
import consistentCssPropUsage from './consistent-css-prop-usage';
|
|
@@ -13,6 +13,10 @@ import noCssTaggedTemplateExpression from './no-css-tagged-template-expression';
|
|
|
13
13
|
import noDeprecatedApis from './no-deprecated-apis';
|
|
14
14
|
import noDeprecatedDesignTokenUsage from './no-deprecated-design-token-usage';
|
|
15
15
|
import noDeprecatedImports from './no-deprecated-imports';
|
|
16
|
+
import noEmptyStyledExpression from './no-empty-styled-expression';
|
|
17
|
+
import noExportedCss from './no-exported-css';
|
|
18
|
+
import noExportedKeyframes from './no-exported-keyframes';
|
|
19
|
+
import noInvalidCssMap from './no-invalid-css-map';
|
|
16
20
|
import noMargin from './no-margin';
|
|
17
21
|
import noNestedStyles from './no-nested-styles';
|
|
18
22
|
import noPhysicalProperties from './no-physical-properties';
|
|
@@ -37,6 +41,10 @@ export default {
|
|
|
37
41
|
'no-deprecated-apis': noDeprecatedApis,
|
|
38
42
|
'no-deprecated-design-token-usage': noDeprecatedDesignTokenUsage,
|
|
39
43
|
'no-deprecated-imports': noDeprecatedImports,
|
|
44
|
+
'no-empty-styled-expression': noEmptyStyledExpression,
|
|
45
|
+
'no-exported-css': noExportedCss,
|
|
46
|
+
'no-exported-keyframes': noExportedKeyframes,
|
|
47
|
+
'no-invalid-css-map': noInvalidCssMap,
|
|
40
48
|
'no-margin': noMargin,
|
|
41
49
|
'no-nested-styles': noNestedStyles,
|
|
42
50
|
'no-physical-properties': noPhysicalProperties,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createLintRule } from '../utils/create-rule';
|
|
2
|
+
import { getImportSources, isStyled } from '../utils/is-supported-import';
|
|
3
|
+
const isEmptyStyledExpression = node => {
|
|
4
|
+
const [firstArg] = node.arguments;
|
|
5
|
+
if (node.arguments.length === 0) {
|
|
6
|
+
return true;
|
|
7
|
+
} else if (node.arguments.length === 1 && (firstArg === null || firstArg === void 0 ? void 0 : firstArg.type) === 'ArrayExpression') {
|
|
8
|
+
return firstArg.elements.length === 0;
|
|
9
|
+
} else if (node.arguments.length === 1 && (firstArg === null || firstArg === void 0 ? void 0 : firstArg.type) === 'ObjectExpression') {
|
|
10
|
+
return firstArg.properties.length === 0;
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
};
|
|
14
|
+
const createNoEmptyStyledExpressionRule = (isEmptyStyledExpression, messageId) => context => {
|
|
15
|
+
const importSources = getImportSources(context);
|
|
16
|
+
return {
|
|
17
|
+
'CallExpression[callee.type="MemberExpression"]': node => {
|
|
18
|
+
const {
|
|
19
|
+
references
|
|
20
|
+
} = context.getScope();
|
|
21
|
+
|
|
22
|
+
// If we have styled.div(...), make sure `callee` only refers to the
|
|
23
|
+
// `styled` part instead of the whole `styled.div` expression.
|
|
24
|
+
const callee = node.callee.type === 'MemberExpression' ? node.callee.object : node.callee;
|
|
25
|
+
if (!isStyled(callee, references, importSources)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!isEmptyStyledExpression(node)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
context.report({
|
|
32
|
+
messageId,
|
|
33
|
+
node
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
const noEmptyStyledExpressionRule = createLintRule({
|
|
39
|
+
meta: {
|
|
40
|
+
name: 'no-empty-styled-expression',
|
|
41
|
+
docs: {
|
|
42
|
+
description: 'Forbids any styled expression to be used when passing empty arguments to styled.div() (or other JSX elements).',
|
|
43
|
+
recommended: true,
|
|
44
|
+
severity: 'warn'
|
|
45
|
+
},
|
|
46
|
+
messages: {
|
|
47
|
+
unexpected: 'Found an empty expression, or empty object argument passed to `styled` function call. This unnecessarily causes a major performance penalty - please use a plain JSX element or a React fragment instead (e.g. `<div>Hello</div>` or `<>Hello</>`).'
|
|
48
|
+
},
|
|
49
|
+
type: 'problem',
|
|
50
|
+
schema: [{
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
importSources: {
|
|
54
|
+
type: 'array',
|
|
55
|
+
items: [{
|
|
56
|
+
type: 'string'
|
|
57
|
+
}]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
additionalProperties: false
|
|
61
|
+
}]
|
|
62
|
+
},
|
|
63
|
+
create: createNoEmptyStyledExpressionRule(isEmptyStyledExpression, 'unexpected')
|
|
64
|
+
});
|
|
65
|
+
export default noEmptyStyledExpressionRule;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createNoExportedRule } from '../utils/create-no-exported-rule/main';
|
|
2
|
+
import { createLintRule } from '../utils/create-rule';
|
|
3
|
+
import { isCss } from '../utils/is-supported-import';
|
|
4
|
+
const noExportedCssRule = createLintRule({
|
|
5
|
+
meta: {
|
|
6
|
+
name: 'no-exported-css',
|
|
7
|
+
type: 'problem',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Forbid exporting `css` function calls. Exporting `css` function calls can result in unexpected behaviour at runtime, and is not statically analysable.',
|
|
10
|
+
recommended: true,
|
|
11
|
+
severity: 'warn'
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
unexpected: "`css` can't be exported - this will cause unexpected behaviour at runtime. Instead, please move your `css(...)` code to the same file where these styles are being used."
|
|
15
|
+
},
|
|
16
|
+
schema: [{
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
importSources: {
|
|
20
|
+
type: 'array',
|
|
21
|
+
items: [{
|
|
22
|
+
type: 'string'
|
|
23
|
+
}]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
additionalProperties: false
|
|
27
|
+
}]
|
|
28
|
+
},
|
|
29
|
+
create: createNoExportedRule(isCss, 'unexpected')
|
|
30
|
+
});
|
|
31
|
+
export default noExportedCssRule;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createNoExportedRule } from '../utils/create-no-exported-rule/main';
|
|
2
|
+
import { createLintRule } from '../utils/create-rule';
|
|
3
|
+
import { isKeyframes } from '../utils/is-supported-import';
|
|
4
|
+
const noExportedKeyframesRule = createLintRule({
|
|
5
|
+
meta: {
|
|
6
|
+
name: 'no-exported-keyframes',
|
|
7
|
+
type: 'problem',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Forbid exporting `keyframes` function calls. Exporting `css` function calls can result in unexpected behaviour at runtime, and is not statically analysable.',
|
|
10
|
+
recommended: true,
|
|
11
|
+
severity: 'warn'
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
unexpected: "`keyframes` can't be exported - this will cause unexpected behaviour at runtime. Instead, please move your `keyframes(...)` code to the same file where these styles are being used."
|
|
15
|
+
},
|
|
16
|
+
schema: [{
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
importSources: {
|
|
20
|
+
type: 'array',
|
|
21
|
+
items: [{
|
|
22
|
+
type: 'string'
|
|
23
|
+
}]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
additionalProperties: false
|
|
27
|
+
}]
|
|
28
|
+
},
|
|
29
|
+
create: createNoExportedRule(isKeyframes, 'unexpected')
|
|
30
|
+
});
|
|
31
|
+
export default noExportedKeyframesRule;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { checkIfSupportedExport } from '../utils/create-no-exported-rule/check-if-supported-export';
|
|
2
|
+
import { createLintRule } from '../utils/create-rule';
|
|
3
|
+
import { CSS_IN_JS_IMPORTS, isCssMap } from '../utils/is-supported-import';
|
|
4
|
+
import { CssMapObjectChecker, getCssMapObject } from './utils';
|
|
5
|
+
const IMPORT_SOURCES = [CSS_IN_JS_IMPORTS.compiled, CSS_IN_JS_IMPORTS.atlaskitCss];
|
|
6
|
+
const reportIfExported = (node, context) => {
|
|
7
|
+
const state = checkIfSupportedExport(context, node, IMPORT_SOURCES);
|
|
8
|
+
if (!state.isExport) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
context.report({
|
|
12
|
+
messageId: 'noExportedCssMap',
|
|
13
|
+
node: state.node
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
const reportIfNotTopLevelScope = (node, context) => {
|
|
17
|
+
// Treat `export` keyword as valid because the reportIfExported function already handles those
|
|
18
|
+
const validTypes = ['ExportDefaultDeclaration', 'ExportNamedDeclaration', 'Program', 'VariableDeclaration', 'VariableDeclarator'];
|
|
19
|
+
let parentNode = node.parent;
|
|
20
|
+
while (parentNode) {
|
|
21
|
+
if (!validTypes.includes(parentNode.type)) {
|
|
22
|
+
context.report({
|
|
23
|
+
node: node,
|
|
24
|
+
messageId: 'mustBeTopLevelScope'
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
parentNode = parentNode.parent;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const createCssMapRule = context => {
|
|
32
|
+
const {
|
|
33
|
+
text
|
|
34
|
+
} = context.getSourceCode();
|
|
35
|
+
if (IMPORT_SOURCES.every(importSource => !text.includes(importSource))) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
CallExpression(node) {
|
|
40
|
+
const references = context.getScope().references;
|
|
41
|
+
if (!isCssMap(node.callee, references, IMPORT_SOURCES)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
reportIfExported(node, context);
|
|
45
|
+
reportIfNotTopLevelScope(node, context);
|
|
46
|
+
const cssMapObject = getCssMapObject(node);
|
|
47
|
+
if (!cssMapObject) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const cssMapObjectChecker = new CssMapObjectChecker(cssMapObject, context);
|
|
51
|
+
cssMapObjectChecker.run();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const noInvalidCssMapRule = createLintRule({
|
|
56
|
+
meta: {
|
|
57
|
+
name: 'no-invalid-css-map',
|
|
58
|
+
docs: {
|
|
59
|
+
description: "Checks the validity of a CSS map created through cssMap. This is intended to be used alongside TypeScript's type-checking.",
|
|
60
|
+
recommended: true,
|
|
61
|
+
severity: 'error'
|
|
62
|
+
},
|
|
63
|
+
messages: {
|
|
64
|
+
mustBeTopLevelScope: 'cssMap must only be used in the top-most scope of the module.',
|
|
65
|
+
noNonStaticallyEvaluable: 'Cannot statically evaluate the value of this variable. Values used in the cssMap function call should have a value evaluable at build time.',
|
|
66
|
+
noExportedCssMap: 'cssMap usages cannot be exported.',
|
|
67
|
+
noInlineFunctions: 'Cannot use functions as values in cssMap - values must only be statically evaluable values (e.g. strings, numbers).',
|
|
68
|
+
noFunctionCalls: 'Cannot call external functions in cssMap - values must only be statically evaluable values (e.g. strings, numbers).',
|
|
69
|
+
noSpreadElement: 'Cannot use the spread operator in cssMap.'
|
|
70
|
+
},
|
|
71
|
+
schema: [{
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
allowedFunctionCalls: {
|
|
75
|
+
type: 'array',
|
|
76
|
+
items: {
|
|
77
|
+
type: 'array',
|
|
78
|
+
minItems: 2,
|
|
79
|
+
maxItems: 2,
|
|
80
|
+
items: [{
|
|
81
|
+
type: 'string'
|
|
82
|
+
}, {
|
|
83
|
+
type: 'string'
|
|
84
|
+
}]
|
|
85
|
+
},
|
|
86
|
+
uniqueItems: true
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
additionalProperties: false
|
|
90
|
+
}],
|
|
91
|
+
type: 'problem'
|
|
92
|
+
},
|
|
93
|
+
create: createCssMapRule
|
|
94
|
+
});
|
|
95
|
+
export default noInvalidCssMapRule;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export const getCssMapObject = node => {
|
|
2
|
+
// We assume the argument `node` is already a cssMap() call.
|
|
3
|
+
|
|
4
|
+
// Things like the number of arguments to cssMap and the type of
|
|
5
|
+
// cssMap's argument are handled by the TypeScript compiler, so
|
|
6
|
+
// we don't bother with creating eslint errors for these here
|
|
7
|
+
|
|
8
|
+
if (node.arguments.length !== 1 || node.arguments[0].type !== 'ObjectExpression') {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
return node.arguments[0];
|
|
12
|
+
};
|
|
13
|
+
const findNodeReference = (references, node) => {
|
|
14
|
+
return references.find(reference => reference.identifier === node);
|
|
15
|
+
};
|
|
16
|
+
const getAllowedFunctionCalls = options => {
|
|
17
|
+
var _options$;
|
|
18
|
+
if (options.length === 0 || ((_options$ = options[0]) === null || _options$ === void 0 ? void 0 : _options$.allowedFunctionCalls) === undefined) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Beyond the basic check of "does allowedFunctionCalls exist?",
|
|
23
|
+
// we assume ESLint's rule checker type checks the contents of allowedFunctionCalls
|
|
24
|
+
// as it should
|
|
25
|
+
return options[0].allowedFunctionCalls;
|
|
26
|
+
};
|
|
27
|
+
export class CssMapObjectChecker {
|
|
28
|
+
constructor(cssMapObject, context) {
|
|
29
|
+
this.allowedFunctionCalls = getAllowedFunctionCalls(context.options);
|
|
30
|
+
this.cssMapObject = cssMapObject;
|
|
31
|
+
this.report = context.report;
|
|
32
|
+
this.references = context.getScope().references;
|
|
33
|
+
}
|
|
34
|
+
isNotWhitelistedFunction(callee) {
|
|
35
|
+
var _reference$resolved;
|
|
36
|
+
if (callee.type !== 'Identifier' || this.allowedFunctionCalls.length === 0) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
const reference = findNodeReference(this.references, callee);
|
|
40
|
+
const definitions = reference === null || reference === void 0 ? void 0 : (_reference$resolved = reference.resolved) === null || _reference$resolved === void 0 ? void 0 : _reference$resolved.defs;
|
|
41
|
+
if (!definitions) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return definitions.some(definition => {
|
|
45
|
+
// We add some restrictions to keep this simple...
|
|
46
|
+
// Forbid non-imported functions
|
|
47
|
+
if (definition.type !== 'ImportBinding') {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
// Forbid default imports (e.g. `import React from 'react'`)
|
|
51
|
+
if (definition.node.type !== 'ImportSpecifier') {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
const packageName = definition.parent.source.value;
|
|
55
|
+
const importedFunctionName = definition.node.imported.name;
|
|
56
|
+
return !this.allowedFunctionCalls.some(([allowedPackageName, allowedFunctionName]) => allowedPackageName === packageName && allowedFunctionName === importedFunctionName);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
checkCssMapObjectValue(value) {
|
|
60
|
+
if (value.type === 'CallExpression' && this.isNotWhitelistedFunction(value.callee)) {
|
|
61
|
+
// object value is a function call in the style
|
|
62
|
+
// {
|
|
63
|
+
// key: functionCall(), ...
|
|
64
|
+
// }
|
|
65
|
+
this.report({
|
|
66
|
+
node: value,
|
|
67
|
+
messageId: 'noFunctionCalls'
|
|
68
|
+
});
|
|
69
|
+
} else if (value.type === 'ArrowFunctionExpression' || value.type === 'FunctionExpression') {
|
|
70
|
+
// object value is a function call in the style
|
|
71
|
+
// {
|
|
72
|
+
// key: (prop) => prop.color, // ArrowFunctionExpression
|
|
73
|
+
// get danger() { return { ... } }, // FunctionExpression
|
|
74
|
+
// }
|
|
75
|
+
this.report({
|
|
76
|
+
node: value,
|
|
77
|
+
messageId: 'noInlineFunctions'
|
|
78
|
+
});
|
|
79
|
+
} else if (value.type === 'BinaryExpression' || value.type === 'LogicalExpression') {
|
|
80
|
+
this.checkCssMapObjectValue(value.left);
|
|
81
|
+
this.checkCssMapObjectValue(value.right);
|
|
82
|
+
} else if (value.type === 'Identifier') {
|
|
83
|
+
var _reference$resolved2;
|
|
84
|
+
const reference = findNodeReference(this.references, value);
|
|
85
|
+
|
|
86
|
+
// Get the variable's definition when initialised. Assume that the last definition
|
|
87
|
+
// is the most recent one.
|
|
88
|
+
//
|
|
89
|
+
// Ideally we would try to get the variable's value at the point at which
|
|
90
|
+
// cssMap() is run, but ESLint doesn't seem to give us an easy way to
|
|
91
|
+
// do that...
|
|
92
|
+
const definitions = reference === null || reference === void 0 ? void 0 : (_reference$resolved2 = reference.resolved) === null || _reference$resolved2 === void 0 ? void 0 : _reference$resolved2.defs;
|
|
93
|
+
if (!definitions || definitions.length === 0) {
|
|
94
|
+
// Variable is not defined :thinking:
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
for (const definition of definitions) {
|
|
98
|
+
if (definition.type === 'Variable' && definition.node.init) {
|
|
99
|
+
return this.checkCssMapObjectValue(definition.node.init);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} else if (value.type === 'ObjectExpression') {
|
|
103
|
+
// Object inside another object
|
|
104
|
+
this.checkCssMapObject(value);
|
|
105
|
+
} else if (value.type === 'TemplateLiteral') {
|
|
106
|
+
// object value is a template literal, something like
|
|
107
|
+
// `hello world`
|
|
108
|
+
// `hello ${functionCall()} world`
|
|
109
|
+
// `hello ${someVariable} world`
|
|
110
|
+
// etc.
|
|
111
|
+
//
|
|
112
|
+
// where the expressions are the parts enclosed within the
|
|
113
|
+
// ${ ... }
|
|
114
|
+
for (const expression of value.expressions) {
|
|
115
|
+
this.checkCssMapObjectValue(expression);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
checkCssMapObject(cssMapObject) {
|
|
120
|
+
for (const property of cssMapObject.properties) {
|
|
121
|
+
if (property.type === 'SpreadElement') {
|
|
122
|
+
this.report({
|
|
123
|
+
node: property,
|
|
124
|
+
messageId: 'noSpreadElement'
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
this.checkCssMapObjectValue(property.value);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
run() {
|
|
132
|
+
this.checkCssMapObject(this.cssMapObject);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { isStyledComponent } from './is-styled-component';
|
|
2
|
+
const getStack = (context, node) => {
|
|
3
|
+
var _scope;
|
|
4
|
+
const {
|
|
5
|
+
scopeManager
|
|
6
|
+
} = context.getSourceCode();
|
|
7
|
+
const stack = {
|
|
8
|
+
nodes: [],
|
|
9
|
+
root: node
|
|
10
|
+
};
|
|
11
|
+
let scope;
|
|
12
|
+
for (let current = node; current.type !== 'Program'; current = current.parent) {
|
|
13
|
+
if (!scope) {
|
|
14
|
+
const currentScope = scopeManager.acquire(current);
|
|
15
|
+
if (currentScope) {
|
|
16
|
+
scope = currentScope;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
switch (current.type) {
|
|
20
|
+
case 'ExportDefaultDeclaration':
|
|
21
|
+
case 'ExportNamedDeclaration':
|
|
22
|
+
stack.root = current;
|
|
23
|
+
break;
|
|
24
|
+
case 'VariableDeclarator':
|
|
25
|
+
stack.root = current;
|
|
26
|
+
break;
|
|
27
|
+
case 'ExportSpecifier':
|
|
28
|
+
case 'ObjectExpression':
|
|
29
|
+
case 'VariableDeclaration':
|
|
30
|
+
break;
|
|
31
|
+
default:
|
|
32
|
+
stack.nodes.unshift(current);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
...stack,
|
|
37
|
+
scope: (_scope = scope) !== null && _scope !== void 0 ? _scope : context.getScope()
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const matches = (defs, refs) => {
|
|
41
|
+
// When there are no defs, the definition is inlined. This must be a match as we know the refs contain the initial
|
|
42
|
+
// definition.
|
|
43
|
+
if (!defs.length) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// When there are no refs, the reference refers to the entire definition and therefore must be a match.
|
|
48
|
+
if (!refs.length) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// When both the references and definitions exist, they should match in length
|
|
53
|
+
if (defs.length !== refs.length) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return defs.every((def, i) => {
|
|
57
|
+
const ref = refs[i];
|
|
58
|
+
if (def.type === 'Property') {
|
|
59
|
+
// There is a match between the def and the ref when both names match:
|
|
60
|
+
//
|
|
61
|
+
// const fooDef = { bar: '' };
|
|
62
|
+
// const barRef = fooDef.bar
|
|
63
|
+
//
|
|
64
|
+
// There is no match when the ref property does not match the definition key name:
|
|
65
|
+
//
|
|
66
|
+
// const barRef = fooDef.notFound
|
|
67
|
+
return def.key.type === 'Identifier' && ref.type === 'MemberExpression' && ref.property.type === 'Identifier' && ref.property.name === def.key.name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Anything here is either unsupported or should not match...
|
|
71
|
+
return false;
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
export const checkIfSupportedExport = (context, node, importSources, scope = context.getScope()) => {
|
|
75
|
+
// Ignore any expression defined outside of the global or module scope as we have no way of statically analysing them
|
|
76
|
+
if (scope.type !== 'global' && scope.type !== 'module') {
|
|
77
|
+
return {
|
|
78
|
+
isExport: false
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const {
|
|
82
|
+
root,
|
|
83
|
+
nodes
|
|
84
|
+
} = getStack(context, node.parent);
|
|
85
|
+
// Exporting a component with a css reference should be allowed
|
|
86
|
+
if (isStyledComponent(nodes, context, importSources)) {
|
|
87
|
+
return {
|
|
88
|
+
isExport: false
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (root.type === 'ExportDefaultDeclaration' || root.type === 'ExportNamedDeclaration') {
|
|
92
|
+
return {
|
|
93
|
+
isExport: true,
|
|
94
|
+
node: root
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (root.type !== 'VariableDeclarator') {
|
|
98
|
+
return {
|
|
99
|
+
isExport: false
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Find the reference to the variable declarator
|
|
104
|
+
const reference = scope.references.find(({
|
|
105
|
+
identifier
|
|
106
|
+
}) => identifier === root.id);
|
|
107
|
+
if (!reference) {
|
|
108
|
+
return {
|
|
109
|
+
isExport: false
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Iterate through all of the references to the resolved variable declarator node
|
|
114
|
+
const {
|
|
115
|
+
resolved
|
|
116
|
+
} = reference;
|
|
117
|
+
for (const {
|
|
118
|
+
identifier
|
|
119
|
+
} of (_resolved$references = resolved === null || resolved === void 0 ? void 0 : resolved.references) !== null && _resolved$references !== void 0 ? _resolved$references : []) {
|
|
120
|
+
var _resolved$references;
|
|
121
|
+
// Skip references to the root, since it has already been processed above
|
|
122
|
+
if (identifier === root.id) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const {
|
|
126
|
+
nodes: refs,
|
|
127
|
+
scope: nextScope
|
|
128
|
+
} = getStack(context, identifier.parent);
|
|
129
|
+
|
|
130
|
+
// Only validate the resolved reference if it accesses the definition node
|
|
131
|
+
if (matches(nodes, refs.reverse())) {
|
|
132
|
+
// Now validate the identifier reference as a definition
|
|
133
|
+
const validity = checkIfSupportedExport(context, identifier, importSources, nextScope);
|
|
134
|
+
if (validity.isExport) {
|
|
135
|
+
return validity;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
isExport: false
|
|
141
|
+
};
|
|
142
|
+
};
|