@atlaskit/eslint-plugin-platform 0.6.1 → 0.7.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 +103 -28
- package/afm-jira/tsconfig.json +20 -0
- package/dist/cjs/index.js +48 -2
- package/dist/cjs/rules/constants.js +11 -0
- package/dist/cjs/rules/ensure-critical-dependency-resolutions/index.js +54 -6
- package/dist/cjs/rules/ensure-native-and-af-exports-synced/index.js +16 -7
- package/dist/cjs/rules/ensure-valid-emotion-css-prop/index.js +91 -0
- package/dist/cjs/rules/inline-usage/index.js +94 -0
- package/dist/cjs/rules/no-alias/index.js +64 -0
- package/dist/cjs/rules/no-module-level-eval/index.js +45 -0
- package/dist/cjs/rules/no-preconditioning/index.js +108 -0
- package/dist/cjs/rules/prefer-fg/index.js +106 -0
- package/dist/cjs/rules/static-feature-flags/index.js +63 -0
- package/dist/cjs/rules/use-recommended-utils/index.js +47 -0
- package/dist/cjs/rules/util/registration-utils.js +2 -1
- package/dist/cjs/rules/utils.js +53 -0
- package/dist/es2019/index.js +52 -2
- package/dist/es2019/rules/constants.js +5 -0
- package/dist/es2019/rules/ensure-critical-dependency-resolutions/index.js +52 -6
- package/dist/es2019/rules/ensure-native-and-af-exports-synced/index.js +15 -7
- package/dist/es2019/rules/ensure-valid-emotion-css-prop/index.js +87 -0
- package/dist/es2019/rules/inline-usage/index.js +90 -0
- package/dist/es2019/rules/no-alias/index.js +58 -0
- package/dist/es2019/rules/no-module-level-eval/index.js +39 -0
- package/dist/es2019/rules/no-preconditioning/index.js +105 -0
- package/dist/es2019/rules/prefer-fg/index.js +81 -0
- package/dist/es2019/rules/static-feature-flags/index.js +54 -0
- package/dist/es2019/rules/use-recommended-utils/index.js +41 -0
- package/dist/es2019/rules/util/registration-utils.js +2 -1
- package/dist/es2019/rules/utils.js +29 -0
- package/dist/esm/index.js +48 -2
- package/dist/esm/rules/constants.js +5 -0
- package/dist/esm/rules/ensure-critical-dependency-resolutions/index.js +54 -6
- package/dist/esm/rules/ensure-native-and-af-exports-synced/index.js +16 -7
- package/dist/esm/rules/ensure-valid-emotion-css-prop/index.js +85 -0
- package/dist/esm/rules/inline-usage/index.js +87 -0
- package/dist/esm/rules/no-alias/index.js +57 -0
- package/dist/esm/rules/no-module-level-eval/index.js +39 -0
- package/dist/esm/rules/no-preconditioning/index.js +102 -0
- package/dist/esm/rules/prefer-fg/index.js +99 -0
- package/dist/esm/rules/static-feature-flags/index.js +56 -0
- package/dist/esm/rules/use-recommended-utils/index.js +41 -0
- package/dist/esm/rules/util/registration-utils.js +2 -1
- package/dist/esm/rules/utils.js +45 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/rules/constants.d.ts +3 -0
- package/dist/types/rules/ensure-valid-emotion-css-prop/index.d.ts +3 -0
- package/dist/types/rules/inline-usage/index.d.ts +3 -0
- package/dist/types/rules/no-alias/index.d.ts +3 -0
- package/dist/types/rules/no-module-level-eval/index.d.ts +3 -0
- package/dist/types/rules/no-preconditioning/index.d.ts +3 -0
- package/dist/types/rules/prefer-fg/index.d.ts +3 -0
- package/dist/types/rules/static-feature-flags/index.d.ts +3 -0
- package/dist/types/rules/use-recommended-utils/index.d.ts +3 -0
- package/dist/types/rules/util/registration-utils.d.ts +1 -0
- package/dist/types/rules/utils.d.ts +7 -0
- package/dist/types-ts4.5/index.d.ts +15 -0
- package/dist/types-ts4.5/rules/constants.d.ts +3 -0
- package/dist/types-ts4.5/rules/ensure-valid-emotion-css-prop/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/inline-usage/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/no-alias/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/no-module-level-eval/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/no-preconditioning/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/prefer-fg/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/static-feature-flags/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/use-recommended-utils/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/util/registration-utils.d.ts +1 -0
- package/dist/types-ts4.5/rules/utils.d.ts +7 -0
- package/index.js +9 -9
- package/package.json +43 -44
- package/report.api.md +31 -30
- package/src/__tests__/utils/_tester.tsx +16 -16
- package/src/index.tsx +102 -51
- package/src/rules/constants.tsx +20 -0
- package/src/rules/ensure-atlassian-team/__tests__/unit/rule.test.ts +19 -19
- package/src/rules/ensure-atlassian-team/index.ts +39 -52
- package/src/rules/ensure-critical-dependency-resolutions/__test__/unit/rule.test.tsx +146 -81
- package/src/rules/ensure-critical-dependency-resolutions/index.tsx +152 -97
- package/src/rules/ensure-feature-flag-prefix/__tests__/unit/rule.test.tsx +51 -51
- package/src/rules/ensure-feature-flag-prefix/index.tsx +65 -80
- package/src/rules/ensure-feature-flag-registration/__tests__/unit/rule.test.tsx +97 -97
- package/src/rules/ensure-feature-flag-registration/index.tsx +88 -105
- package/src/rules/ensure-native-and-af-exports-synced/__tests__/unit/rule.test.tsx +180 -180
- package/src/rules/ensure-native-and-af-exports-synced/index.tsx +162 -168
- package/src/rules/ensure-publish-valid/__tests__/unit/rule.test.ts +34 -36
- package/src/rules/ensure-publish-valid/index.ts +66 -81
- package/src/rules/ensure-test-runner-arguments/__tests__/unit/rule.test.tsx +93 -93
- package/src/rules/ensure-test-runner-arguments/index.tsx +107 -121
- package/src/rules/ensure-test-runner-nested-count/__tests__/unit/rule.test.tsx +43 -43
- package/src/rules/ensure-test-runner-nested-count/index.tsx +68 -70
- package/src/rules/ensure-valid-emotion-css-prop/__tests__/unit/rule.test.ts +142 -0
- package/src/rules/ensure-valid-emotion-css-prop/index.ts +96 -0
- package/src/rules/inline-usage/README.md +53 -0
- package/src/rules/inline-usage/__tests__/rule.test.tsx +106 -0
- package/src/rules/inline-usage/index.tsx +130 -0
- package/src/rules/no-alias/README.md +29 -0
- package/src/rules/no-alias/__tests__/rule.test.tsx +76 -0
- package/src/rules/no-alias/index.tsx +75 -0
- package/src/rules/no-duplicate-dependencies/__tests__/unit/rule.test.ts +44 -44
- package/src/rules/no-duplicate-dependencies/index.ts +68 -73
- package/src/rules/no-invalid-feature-flag-usage/__tests__/unit/rule.test.tsx +64 -64
- package/src/rules/no-invalid-feature-flag-usage/index.tsx +105 -112
- package/src/rules/no-invalid-storybook-decorator-usage/__tests__/unit/rule.test.tsx +13 -13
- package/src/rules/no-invalid-storybook-decorator-usage/index.tsx +28 -30
- package/src/rules/no-module-level-eval/README.md +53 -0
- package/src/rules/no-module-level-eval/__tests__/test.tsx +133 -0
- package/src/rules/no-module-level-eval/index.tsx +52 -0
- package/src/rules/no-pre-post-installs/__tests__/unit/rule.test.ts +36 -36
- package/src/rules/no-pre-post-installs/index.ts +27 -27
- package/src/rules/no-preconditioning/README.md +69 -0
- package/src/rules/no-preconditioning/__tests__/rule.test.tsx +164 -0
- package/src/rules/no-preconditioning/index.tsx +138 -0
- package/src/rules/prefer-fg/README.md +3 -0
- package/src/rules/prefer-fg/__tests__/rule.test.tsx +83 -0
- package/src/rules/prefer-fg/index.tsx +108 -0
- package/src/rules/static-feature-flags/README.md +3 -0
- package/src/rules/static-feature-flags/__tests__/test.tsx +135 -0
- package/src/rules/static-feature-flags/index.tsx +103 -0
- package/src/rules/use-recommended-utils/README.md +67 -0
- package/src/rules/use-recommended-utils/__tests__/rule.test.tsx +78 -0
- package/src/rules/use-recommended-utils/index.tsx +57 -0
- package/src/rules/util/handle-ast-object.ts +21 -32
- package/src/rules/util/registration-utils.ts +31 -30
- package/src/rules/utils.tsx +46 -0
- package/tsconfig.app.json +35 -35
- package/tsconfig.dev.json +39 -39
|
@@ -5,80 +5,78 @@ import type { SimpleCallExpression } from 'estree';
|
|
|
5
5
|
const NESTED_LIMIT: number = 4;
|
|
6
6
|
const TEST_RUNNER_IDENTIFIER = 'ffTest' as const;
|
|
7
7
|
|
|
8
|
-
const getDepthOfNestedRunner = (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
return depth;
|
|
8
|
+
const getDepthOfNestedRunner = (node: SimpleCallExpression & Rule.NodeParentExtension): number => {
|
|
9
|
+
// Calculate the depth of a binary tree, using a queue to track path
|
|
10
|
+
let queue: (typeof node)[] = [];
|
|
11
|
+
queue.push(node);
|
|
12
|
+
let depth = 0;
|
|
13
|
+
while (queue.length > 0) {
|
|
14
|
+
let nodeCount = queue.length;
|
|
15
|
+
while (nodeCount > 0) {
|
|
16
|
+
let currentNode = queue.shift() as typeof node;
|
|
17
|
+
if (
|
|
18
|
+
currentNode.arguments[1].type === 'ArrowFunctionExpression' &&
|
|
19
|
+
currentNode.arguments[1].body.type === 'CallExpression' &&
|
|
20
|
+
currentNode.arguments[1].body.callee.type === 'Identifier' &&
|
|
21
|
+
currentNode.arguments[1].body.callee.name === TEST_RUNNER_IDENTIFIER
|
|
22
|
+
) {
|
|
23
|
+
queue.push({
|
|
24
|
+
...currentNode.arguments[1].body,
|
|
25
|
+
parent: currentNode.parent,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (
|
|
29
|
+
currentNode.arguments[2]?.type === 'ArrowFunctionExpression' &&
|
|
30
|
+
currentNode.arguments[2].body.type === 'CallExpression' &&
|
|
31
|
+
currentNode.arguments[2].body.callee.type === 'Identifier' &&
|
|
32
|
+
currentNode.arguments[2].body.callee.name === TEST_RUNNER_IDENTIFIER
|
|
33
|
+
) {
|
|
34
|
+
queue.push({
|
|
35
|
+
...currentNode.arguments[2].body,
|
|
36
|
+
parent: currentNode.parent,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
nodeCount--;
|
|
40
|
+
}
|
|
41
|
+
depth++;
|
|
42
|
+
}
|
|
43
|
+
return depth;
|
|
46
44
|
};
|
|
47
45
|
|
|
48
46
|
const rule: Rule.RuleModule = {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
47
|
+
meta: {
|
|
48
|
+
docs: {
|
|
49
|
+
recommended: false,
|
|
50
|
+
},
|
|
51
|
+
type: 'problem',
|
|
52
|
+
messages: {
|
|
53
|
+
tooManyNestedTestRunner:
|
|
54
|
+
'{{nestedTestRunner}} test runners are nested. Feature flags may need a clean-up',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
create(context) {
|
|
58
|
+
return {
|
|
59
|
+
// Find the most outside test runner, could be inside a describe or not
|
|
60
|
+
[`Program > * > CallExpression[callee.name=/${TEST_RUNNER_IDENTIFIER}/], CallExpression[callee.name=/describe/] > * > * > * > CallExpression[callee.name=/${TEST_RUNNER_IDENTIFIER}/]`]:
|
|
61
|
+
(node: Rule.Node) => {
|
|
62
|
+
if (node.type === 'CallExpression') {
|
|
63
|
+
// Calculate the depth of nested test runners, counting from the most outside
|
|
64
|
+
const depth = getDepthOfNestedRunner(node);
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
66
|
+
if (depth > NESTED_LIMIT) {
|
|
67
|
+
return context.report({
|
|
68
|
+
node,
|
|
69
|
+
messageId: 'tooManyNestedTestRunner',
|
|
70
|
+
data: {
|
|
71
|
+
nestedTestRunner: depth.toString(),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
82
80
|
};
|
|
83
81
|
|
|
84
82
|
export default rule;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { tester } from '../../../../__tests__/utils/_tester';
|
|
2
|
+
import rule from '../../index';
|
|
3
|
+
|
|
4
|
+
describe('test ensure-valid-emotion-css-prop', () => {
|
|
5
|
+
tester.run('ensure-valid-emotion-css-prop', rule, {
|
|
6
|
+
valid: [
|
|
7
|
+
{
|
|
8
|
+
code: `
|
|
9
|
+
/** @jsx jsx */
|
|
10
|
+
import { jsx, css } from '@emotion/react';
|
|
11
|
+
const styles = css({ opacity: 0.5 });
|
|
12
|
+
const Component = () => <div css={styles} />;`,
|
|
13
|
+
filename: 'src/index.tsx',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
code: `
|
|
17
|
+
/** @jsx jsx */
|
|
18
|
+
import { jsx, css } from '@emotion/react';
|
|
19
|
+
const styles = css({ opacity: 0.5 });
|
|
20
|
+
const Component = () => <div css={[styles, { color: 'red' }]} />;`,
|
|
21
|
+
filename: 'src/index.tsx',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
code: `
|
|
25
|
+
/** @jsx jsx */
|
|
26
|
+
import { jsx } from '@emotion/react';
|
|
27
|
+
const Component = () => <div />;`,
|
|
28
|
+
filename: 'src/index.tsx',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
code: `
|
|
32
|
+
/** @jsx jsx */
|
|
33
|
+
import { jsx, css } from '@emotion/react';
|
|
34
|
+
const Component = () => <div css={css({ opacity: 0.5 })} />;`,
|
|
35
|
+
filename: 'src/index.tsx',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
code: `
|
|
39
|
+
/** @jsx jsx */
|
|
40
|
+
import { jsx } from '@emotion/react';
|
|
41
|
+
const Component = () => <div css={{ opacity: 0.5 }} />;`,
|
|
42
|
+
filename: 'src/__tests__/test.tsx',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
code: `
|
|
46
|
+
/** @jsx jsx */
|
|
47
|
+
import { jsx } from '@emotion/react';
|
|
48
|
+
const Component = () => <div css={{ opacity: 0.5 }} />;`,
|
|
49
|
+
filename: 'src/examples/example.tsx',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
code: `
|
|
53
|
+
/** @jsx jsx */
|
|
54
|
+
import { jsx } from '@emotion/react';
|
|
55
|
+
const Component = () => <div css={{ opacity: 0.5 }} />;`,
|
|
56
|
+
filename: 'src/example-helpers/index.tsx',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
code: `
|
|
60
|
+
/** @jsx jsx */
|
|
61
|
+
import { jsx } from '@emotion/react';
|
|
62
|
+
const Component = () => <div css={{ opacity: 0.5 }} />;`,
|
|
63
|
+
filename: 'src/__fixtures__/index.tsx',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
code: `
|
|
67
|
+
/** @jsx jsx */
|
|
68
|
+
import { jsx } from '@compiled/react';
|
|
69
|
+
const Component = () => <div />;`,
|
|
70
|
+
filename: 'src/index.tsx',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
code: `
|
|
74
|
+
/** @jsx jsx */
|
|
75
|
+
import { jsx } from '@emotion/react';
|
|
76
|
+
const styles = { opacity: 0.5 };
|
|
77
|
+
const Component = () => <div css={styles} />;`,
|
|
78
|
+
filename: 'src/index.tsx',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
code: `
|
|
82
|
+
/** @jsx jsx */
|
|
83
|
+
import { jsx, css } from '@emotion/react';
|
|
84
|
+
const getOpacity = (foo) => 0.5;
|
|
85
|
+
const Component = (props) => {
|
|
86
|
+
const styles = { opacity: getOpacity(props.foo) };
|
|
87
|
+
return <div css={styles} />
|
|
88
|
+
};`,
|
|
89
|
+
filename: 'src/index.tsx',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
code: `
|
|
93
|
+
/** @jsx jsx */
|
|
94
|
+
import { jsx, css } from '@emotion/react';
|
|
95
|
+
const getStyles = (opacity) => ({ color: 'red', opacity });
|
|
96
|
+
const Component = (props) => <div css={getStyles(props.opacity)} />`,
|
|
97
|
+
filename: 'src/index.tsx',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
code: `
|
|
101
|
+
/** @jsx jsx */
|
|
102
|
+
import { jsx, css } from '@emotion/react';
|
|
103
|
+
const getOpacity = (foo) => 0.5;
|
|
104
|
+
function getStyles(props) {
|
|
105
|
+
return { color: 'red', opacity: props.opacity };
|
|
106
|
+
}
|
|
107
|
+
const Component = (props) => <div css={getStyles(props)} />;`,
|
|
108
|
+
filename: 'src/index.tsx',
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
invalid: [
|
|
112
|
+
{
|
|
113
|
+
code: `
|
|
114
|
+
/** @jsx jsx */
|
|
115
|
+
import { jsx } from '@emotion/react';
|
|
116
|
+
const Component = () => <div css={{ opacity: 0.5 }} />;`,
|
|
117
|
+
filename: 'src/index.tsx',
|
|
118
|
+
errors: [{ messageId: 'noEmotionCssImport' }],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
code: `
|
|
122
|
+
/** @jsx jsx */
|
|
123
|
+
import { jsx, css } from '@emotion/react';
|
|
124
|
+
const getOpacity = (foo) => 0.5;
|
|
125
|
+
const Component = (props) => <div css={{ opacity: getOpacity(props.foo) }} />;`,
|
|
126
|
+
filename: 'src/index.tsx',
|
|
127
|
+
errors: [{ messageId: 'noEmotionCssPropFunctionCall' }],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
code: `
|
|
131
|
+
/** @jsx jsx */
|
|
132
|
+
import { jsx, css } from '@emotion/react';
|
|
133
|
+
function getOpacity(foo) {
|
|
134
|
+
return 0.5;
|
|
135
|
+
}
|
|
136
|
+
const Component = (props) => <div css={{ opacity: getOpacity(props.foo) }} />;`,
|
|
137
|
+
filename: 'src/index.tsx',
|
|
138
|
+
errors: [{ messageId: 'noEmotionCssPropFunctionCall' }],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
2
|
+
import type { Rule } from 'eslint';
|
|
3
|
+
|
|
4
|
+
type Position = {
|
|
5
|
+
line: number;
|
|
6
|
+
column: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const rule: Rule.RuleModule = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Ensure valid use of the `css` prop from `@emotion/react`',
|
|
14
|
+
recommended: true,
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
noEmotionCssImport: 'Must import `css` from `@emotion/react` when using the `css` prop.',
|
|
18
|
+
noEmotionCssPropFunctionCall:
|
|
19
|
+
'No function calls allowed when passing an object directly to the `css` prop with `@emotion/react`.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
let emotionJsxImported = false;
|
|
24
|
+
let emotionJsxImportPosition: Position | null | undefined;
|
|
25
|
+
let emotionCssImported = false;
|
|
26
|
+
let cssPropExpressonUsed = false;
|
|
27
|
+
|
|
28
|
+
// Ignore files in these directories
|
|
29
|
+
if (/example|__tests__|__fixtures__/.test(context.filename)) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
ImportDeclaration(node) {
|
|
35
|
+
if (node.source.value === '@emotion/react') {
|
|
36
|
+
node.specifiers.forEach((specifier) => {
|
|
37
|
+
if (specifier.type === 'ImportSpecifier') {
|
|
38
|
+
if (specifier.imported.name === 'jsx') {
|
|
39
|
+
emotionJsxImported = true;
|
|
40
|
+
emotionJsxImportPosition = specifier.loc?.start;
|
|
41
|
+
}
|
|
42
|
+
if (specifier.imported.name === 'css') {
|
|
43
|
+
emotionCssImported = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
JSXAttribute(node: any) {
|
|
50
|
+
const { name, value } = node;
|
|
51
|
+
|
|
52
|
+
// Only run on emotion css props
|
|
53
|
+
if (!emotionJsxImported) return;
|
|
54
|
+
if (name.name !== 'css') return;
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
value.type === 'JSXExpressionContainer' &&
|
|
58
|
+
value.expression.type === 'ObjectExpression'
|
|
59
|
+
) {
|
|
60
|
+
cssPropExpressonUsed = true;
|
|
61
|
+
let containsFunctionExpression = false;
|
|
62
|
+
|
|
63
|
+
// Iterate over the properties of the object
|
|
64
|
+
value.expression.properties.forEach((prop: any) => {
|
|
65
|
+
// Check for function expressions directly within the object literal
|
|
66
|
+
if (
|
|
67
|
+
prop.value?.type === 'ArrowFunctionExpression' ||
|
|
68
|
+
prop.value?.type === 'FunctionExpression' ||
|
|
69
|
+
prop.value?.type === 'CallExpression'
|
|
70
|
+
) {
|
|
71
|
+
containsFunctionExpression = true;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// If a function expression is found within the direct object literal, report an error
|
|
76
|
+
if (containsFunctionExpression) {
|
|
77
|
+
context.report({
|
|
78
|
+
node,
|
|
79
|
+
messageId: 'noEmotionCssPropFunctionCall',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
'Program:exit'() {
|
|
85
|
+
if (emotionJsxImported && cssPropExpressonUsed && !emotionCssImported) {
|
|
86
|
+
context.report({
|
|
87
|
+
messageId: 'noEmotionCssImport',
|
|
88
|
+
loc: emotionJsxImportPosition || { line: 1, column: 0 },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default rule;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Inline feature flags/gates and experiments usages (feature-flags/inline-usage)
|
|
2
|
+
|
|
3
|
+
Ensure feature flags/gates and experiments are inlined so that they can be statically analyzable.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
👎 Examples of **incorrect** code for this rule: Feature flag call is assigned to a variable
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { ff } from '@atlassian/jira-feature-flagging';
|
|
11
|
+
|
|
12
|
+
const myFF = () => ff('my_flag');
|
|
13
|
+
|
|
14
|
+
export const doSomething = () => {
|
|
15
|
+
if (myFF()) {
|
|
16
|
+
doSomethingNew();
|
|
17
|
+
} else {
|
|
18
|
+
doSomethingOld();
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
👍 Examples of **correct** code for this rule: Usage is inlined
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { ff } from '@atlassian/jira-feature-flagging';
|
|
27
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
28
|
+
import { expValEquals } from '@atlassian/jira-feature-experiments';
|
|
29
|
+
|
|
30
|
+
export const doSomething = () => {
|
|
31
|
+
if (ff('my_flag')) {
|
|
32
|
+
doSomethingNew();
|
|
33
|
+
} else {
|
|
34
|
+
doSomethingOld();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const doSomething = () => {
|
|
39
|
+
if (fg('my_gate')) {
|
|
40
|
+
doSomethingNew();
|
|
41
|
+
} else {
|
|
42
|
+
doSomethingOld();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const doSomething = () => {
|
|
47
|
+
if (expValEquals('my_exp', 'on', true)) {
|
|
48
|
+
doSomethingNew();
|
|
49
|
+
} else {
|
|
50
|
+
doSomethingOld();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import outdent from 'outdent';
|
|
2
|
+
import { tester } from '../../../__tests__/utils/_tester';
|
|
3
|
+
import rule from '../index';
|
|
4
|
+
|
|
5
|
+
const options = ['ssOnly'];
|
|
6
|
+
|
|
7
|
+
tester.run('feature-flags/inline-usage', rule, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
name: 'Valid API usage',
|
|
11
|
+
code: outdent`
|
|
12
|
+
import { ff } from '@atlassian/jira-feature-flagging';
|
|
13
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
14
|
+
import { expVal, expValEquals } from '@atlassian/jira-feature-experiments';
|
|
15
|
+
|
|
16
|
+
export const FFComponent = () => ff('is.enabled') && <>Valid</>;
|
|
17
|
+
export const FGComponent = () => fg('is.enabled') && <>Valid</>;
|
|
18
|
+
|
|
19
|
+
export const ExpValComponent = function() {
|
|
20
|
+
return expVal('is.enabled', 'on', false) && <>Valid</>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function ExpValEqualsComponent() {
|
|
24
|
+
expValEquals('is.enabled', 'on', true) && <>Valid</>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const valid1 = () => {
|
|
28
|
+
console.log('valid');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const valid2 = () => 'valid';
|
|
32
|
+
`,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'When using ssOnly option, ff() can avoid inline-usage',
|
|
36
|
+
options,
|
|
37
|
+
code: outdent`
|
|
38
|
+
import { ff } from '@atlassian/jira-feature-flagging';
|
|
39
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
40
|
+
|
|
41
|
+
const invalidFF = () => ff('is.enabled');
|
|
42
|
+
export const FGComponent = () => fg('is.enabled') && <>Valid</>;
|
|
43
|
+
`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'Edge cases',
|
|
47
|
+
code: outdent`
|
|
48
|
+
import { ff } from '@atlassian/jira-feature-flagging';
|
|
49
|
+
|
|
50
|
+
function edgeCase1() {
|
|
51
|
+
console.log('workAround');
|
|
52
|
+
return ff('is.enabled');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const edgeCase2 = function() {
|
|
56
|
+
console.log('workAround');
|
|
57
|
+
return ff('is.enabled');
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const edgeCase3 = () => {
|
|
61
|
+
console.log('workAround');
|
|
62
|
+
return ff('is.enabled');
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const edgeCase4 = () => {
|
|
66
|
+
ff('is.enabled');
|
|
67
|
+
};
|
|
68
|
+
`,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
invalid: [
|
|
72
|
+
{
|
|
73
|
+
name: 'Invalid API usage',
|
|
74
|
+
code: outdent`
|
|
75
|
+
import { ff } from '@atlassian/jira-feature-flagging';
|
|
76
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
77
|
+
import { expVal, expValEquals, UNSAFE_noExposureExp } from '@atlassian/jira-feature-experiments';
|
|
78
|
+
|
|
79
|
+
const invalidFF = () => ff('is.enabled');
|
|
80
|
+
const invalidFG = () => fg('is.enabled');
|
|
81
|
+
const invalidExpVal = () => expVal('is.enabled', 'on', false);
|
|
82
|
+
const invalidExpValBinary = () => expVal('is.enabled', 'cohort', null) === 'variation';
|
|
83
|
+
const invalidExpValBinary2 = () => expVal('is.enabled', 'count', 0) >= 0;
|
|
84
|
+
|
|
85
|
+
const invalidExpValEquals = function() {
|
|
86
|
+
return expValEquals('is.enabled', 'on', true);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function invalidUnsafe() {
|
|
90
|
+
return UNSAFE_noExposureExp('is.enabled');
|
|
91
|
+
}
|
|
92
|
+
`,
|
|
93
|
+
errors: Array.from(Array(7), () => ({ messageId: 'inlineUsage' })),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'When using ssOnly option, Statsig functions should be inlined',
|
|
97
|
+
options,
|
|
98
|
+
code: outdent`
|
|
99
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
100
|
+
|
|
101
|
+
const invalidFG = () => fg('is.enabled');
|
|
102
|
+
`,
|
|
103
|
+
errors: [{ messageId: 'inlineUsage' }],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
import { isAPIimport, type Node } from '../utils';
|
|
3
|
+
|
|
4
|
+
const FUNCTION_NAMES = new Set(['ff', 'fg', 'expVal', 'expValEquals', 'UNSAFE_noExposureExp']);
|
|
5
|
+
const STATSIG_ONLY_FUNCTION_NAMES = new Set([
|
|
6
|
+
'fg',
|
|
7
|
+
'expVal',
|
|
8
|
+
'expValEquals',
|
|
9
|
+
'UNSAFE_noExposureExp',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const findDefinitionDeclaration = (
|
|
13
|
+
node: Rule.Node,
|
|
14
|
+
): (Node<'VariableDeclaration'> | Node<'FunctionDeclaration'>) & Rule.NodeParentExtension =>
|
|
15
|
+
node.type === 'VariableDeclaration' || node.type === 'FunctionDeclaration'
|
|
16
|
+
? node
|
|
17
|
+
: findDefinitionDeclaration(node.parent);
|
|
18
|
+
|
|
19
|
+
const validateCallExpression = (
|
|
20
|
+
node: Node<'CallExpression'> & Rule.NodeParentExtension,
|
|
21
|
+
context: Rule.RuleContext,
|
|
22
|
+
) => {
|
|
23
|
+
const targetedFunctionsSwitch =
|
|
24
|
+
context.options[0] === 'ssOnly' ? STATSIG_ONLY_FUNCTION_NAMES : FUNCTION_NAMES;
|
|
25
|
+
|
|
26
|
+
const { callee } = node;
|
|
27
|
+
const shouldWarn =
|
|
28
|
+
callee.type === 'Identifier' &&
|
|
29
|
+
targetedFunctionsSwitch.has(callee.name) &&
|
|
30
|
+
isAPIimport(callee.name, context);
|
|
31
|
+
|
|
32
|
+
if (shouldWarn) {
|
|
33
|
+
const defDeclaration = findDefinitionDeclaration(node.parent);
|
|
34
|
+
|
|
35
|
+
context.report({
|
|
36
|
+
messageId: 'inlineUsage',
|
|
37
|
+
node:
|
|
38
|
+
defDeclaration.parent.type === 'ExportNamedDeclaration'
|
|
39
|
+
? defDeclaration.parent
|
|
40
|
+
: defDeclaration,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return false;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const validateBinaryExpression = (
|
|
50
|
+
node: Node<'BinaryExpression'> & Rule.NodeParentExtension,
|
|
51
|
+
context: Rule.RuleContext,
|
|
52
|
+
) => {
|
|
53
|
+
// Match all comparator operators i.e ===, >=, <
|
|
54
|
+
if (node.operator.match(/^[=|<|>]/)) {
|
|
55
|
+
if (
|
|
56
|
+
node.left.type === 'CallExpression' &&
|
|
57
|
+
validateCallExpression(node.left as Node<'CallExpression'>, context)
|
|
58
|
+
) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (node.right.type === 'CallExpression') {
|
|
63
|
+
validateCallExpression(node.right as Node<'CallExpression'>, context);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const validateReturnExpression = ({ body }: Node<'BlockStatement'>, context: Rule.RuleContext) => {
|
|
69
|
+
if (body.length !== 1) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const [statement] = body;
|
|
74
|
+
|
|
75
|
+
if (statement.type === 'ReturnStatement') {
|
|
76
|
+
const { argument } = statement;
|
|
77
|
+
|
|
78
|
+
if (argument && argument.type === 'CallExpression') {
|
|
79
|
+
validateCallExpression(argument as Node<'CallExpression'>, context);
|
|
80
|
+
} else if (argument && argument.type === 'BinaryExpression') {
|
|
81
|
+
validateBinaryExpression(argument as Node<'BinaryExpression'>, context);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const validateFunctionBody = (
|
|
87
|
+
body: Node<'BinaryExpression'> | Node<'CallExpression'> | Node<'BlockStatement'>,
|
|
88
|
+
context: Rule.RuleContext,
|
|
89
|
+
) => {
|
|
90
|
+
switch (body.type) {
|
|
91
|
+
case 'CallExpression':
|
|
92
|
+
validateCallExpression(body, context);
|
|
93
|
+
break;
|
|
94
|
+
case 'BinaryExpression':
|
|
95
|
+
validateBinaryExpression(body, context);
|
|
96
|
+
break;
|
|
97
|
+
case 'BlockStatement':
|
|
98
|
+
validateReturnExpression(body, context);
|
|
99
|
+
break;
|
|
100
|
+
default:
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const rule: Rule.RuleModule = {
|
|
105
|
+
meta: {
|
|
106
|
+
type: 'problem',
|
|
107
|
+
docs: {
|
|
108
|
+
description:
|
|
109
|
+
'Ensure feature flags/gates and experiments are inlined so that they can be statically analyzable.',
|
|
110
|
+
url: 'https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/browse/platform/packages/platform/eslint-plugin/src/rules/ff/inline-usage/README.md',
|
|
111
|
+
},
|
|
112
|
+
messages: {
|
|
113
|
+
inlineUsage:
|
|
114
|
+
'Do not export or wrap feature flags/gates and experiments usages. Inline calls at the callsite to ensure it is statically analyzable.',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
create(context) {
|
|
118
|
+
return {
|
|
119
|
+
'VariableDeclaration[declarations.length=1] > VariableDeclarator[id.type="Identifier"]:matches([init.type="ArrowFunctionExpression"], [init.type="FunctionExpression"]) > *.init':
|
|
120
|
+
({ body }: Node<'FunctionDeclaration'>) => {
|
|
121
|
+
validateFunctionBody(body as Node<'BlockStatement'> & Rule.NodeParentExtension, context);
|
|
122
|
+
},
|
|
123
|
+
FunctionDeclaration: ({ body }: Node<'FunctionDeclaration'>) => {
|
|
124
|
+
validateFunctionBody(body as Node<'BlockStatement'> & Rule.NodeParentExtension, context);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export default rule;
|