@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
|
@@ -2,34 +2,34 @@
|
|
|
2
2
|
import type { Rule } from 'eslint';
|
|
3
3
|
|
|
4
4
|
const rule: Rule.RuleModule = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description:
|
|
9
|
+
'This rule disallows public packages to have pre/post install scripts as installations can happen on different environments',
|
|
10
|
+
recommended: false,
|
|
11
|
+
},
|
|
12
|
+
hasSuggestions: false,
|
|
13
|
+
messages: {
|
|
14
|
+
prePostInstallScriptsNotAllowed: 'pre/post install scripts not allowed in package.json',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
'ObjectExpression Property[key.value=scripts] Property[key.value=/^(pre|post)install$/]': (
|
|
20
|
+
node: Rule.Node,
|
|
21
|
+
) => {
|
|
22
|
+
if (!context.getFilename().endsWith('/package.json')) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
return context.report({
|
|
27
|
+
node,
|
|
28
|
+
messageId: 'prePostInstallScriptsNotAllowed',
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
export default rule;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Enforce feature gate not used as a precondition in logical expressions (feature-flags/no-preconditioning)
|
|
2
|
+
|
|
3
|
+
Using a feature gate as a precondition in logical expressions can cause:
|
|
4
|
+
|
|
5
|
+
- Exposure to be tracked incorrectly
|
|
6
|
+
- Unnecessary code
|
|
7
|
+
|
|
8
|
+
## Examples
|
|
9
|
+
|
|
10
|
+
Instead of adding prerequisite gating in code, configure this in Statsig UI by adding rules to gates
|
|
11
|
+
and targeting for experiments. This will reduce unnecessary complexity in code and simplify cleanup.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
15
|
+
import { expVal } from '@atlassian/jira-feature-experiments';
|
|
16
|
+
|
|
17
|
+
// Setup rule in `feature_milestone2` to fail if `feature_milestone1` fails
|
|
18
|
+
if (fg('feature_milestone1') && fg('feature_milestone2')) {
|
|
19
|
+
doSomething();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Setup targeting in `my_exp` to only enroll participants passing `my_gate`
|
|
23
|
+
if (fg('my_gate') && expVal('my_exp')) {
|
|
24
|
+
doSomething();
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Gating experiment values from same or different experiments is valid.
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { expVal } from '@atlassian/jira-feature-experiments';
|
|
32
|
+
|
|
33
|
+
if (expVal('my_exp', 'delayRetries', false) && expVal('my_exp', 'numRetries', 0) > 3) {
|
|
34
|
+
doSomething();
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
👎 Examples of **incorrect** exposure tracking
|
|
39
|
+
|
|
40
|
+
If `isAdmin` is false exposure will be fired even though feature was not intended for audience.
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
44
|
+
|
|
45
|
+
if (fg('my_gate') && isAdmin) {
|
|
46
|
+
doSomething();
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
If `hasSelectedProject` is false and experiment returns true. Expsoure will be tracked as true even
|
|
51
|
+
though feature was not exposed to the user.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
55
|
+
|
|
56
|
+
if (expVal('my_experiment', 'is_enabled', false) && hasSelectedProject) {
|
|
57
|
+
doSomething();
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
👍 Examples of **correct** exposure tracking
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
65
|
+
|
|
66
|
+
if (isAdmin && fg('my_gate')) {
|
|
67
|
+
doSomething();
|
|
68
|
+
}
|
|
69
|
+
```
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import outdent from 'outdent';
|
|
2
|
+
import { tester } from '../../../__tests__/utils/_tester';
|
|
3
|
+
import rule from '../index';
|
|
4
|
+
|
|
5
|
+
tester.run('feature-flags/no-preconditioning', rule, {
|
|
6
|
+
valid: [
|
|
7
|
+
{
|
|
8
|
+
code: outdent`
|
|
9
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
10
|
+
|
|
11
|
+
if (preCheck && fg('gate')) {}
|
|
12
|
+
`,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
code: outdent`
|
|
16
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
17
|
+
|
|
18
|
+
if (preCheck1 && preCheck2 && fg('gate')) {}
|
|
19
|
+
`,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
code: outdent`
|
|
23
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
24
|
+
|
|
25
|
+
const value = fg('gate') && 'value';
|
|
26
|
+
`,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
code: outdent`
|
|
30
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
31
|
+
|
|
32
|
+
const value = preCheck && fg('gate') ? 'value' : '';
|
|
33
|
+
`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
code: outdent`
|
|
37
|
+
import { expVal } from '@atlassian/jira-feature-experiments';
|
|
38
|
+
|
|
39
|
+
if (expVal('one') && expVal('two') ) {}
|
|
40
|
+
`,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
code: outdent`
|
|
44
|
+
import { expVal, expValEquals } from '@atlassian/jira-feature-experiments';
|
|
45
|
+
|
|
46
|
+
if (expVal('one') && expValEquals('two', 'value') ) {}
|
|
47
|
+
`,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
code: outdent`
|
|
51
|
+
import { expVal } from '@atlassian/jira-feature-experiments';
|
|
52
|
+
|
|
53
|
+
if (expVal('one') || expVal('two') ) {}
|
|
54
|
+
`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
code: outdent`
|
|
58
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
59
|
+
|
|
60
|
+
if (preGate && fg('one') || preGate && fg('two') ) {}
|
|
61
|
+
`,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
code: outdent`
|
|
65
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
66
|
+
|
|
67
|
+
if (count > 0 && fg('my_gate') ) {}
|
|
68
|
+
`,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
invalid: [
|
|
72
|
+
{
|
|
73
|
+
code: outdent`
|
|
74
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
75
|
+
|
|
76
|
+
if (fg('one') && fg('two')) {}
|
|
77
|
+
`,
|
|
78
|
+
errors: [{ messageId: 'useConfig' }],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
code: outdent`
|
|
82
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
83
|
+
import { expVal } from '@atlassian/jira-feature-experiments';
|
|
84
|
+
|
|
85
|
+
if (expVal('one') && fg('two')) {}
|
|
86
|
+
`,
|
|
87
|
+
errors: [{ messageId: 'useConfig' }],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
code: outdent`
|
|
91
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
92
|
+
import { expVal } from '@atlassian/jira-feature-experiments';
|
|
93
|
+
|
|
94
|
+
if (fg('one') && fg('two') && expVal('three')) {}
|
|
95
|
+
`,
|
|
96
|
+
errors: [{ messageId: 'useConfig' }],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
code: outdent`
|
|
100
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
101
|
+
|
|
102
|
+
if (preCheck && fg('one') && fg('two')) {}
|
|
103
|
+
`,
|
|
104
|
+
errors: [{ messageId: 'useConfig' }],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
code: outdent`
|
|
108
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
109
|
+
import { expVal } from '@atlassian/jira-feature-experiments';
|
|
110
|
+
|
|
111
|
+
if (fg('my_gate') && expVal('my_exp', 'cohort', 'not-enrolled') === 'variation') {}
|
|
112
|
+
`,
|
|
113
|
+
errors: [{ messageId: 'useConfig' }],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
code: outdent`
|
|
117
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
118
|
+
|
|
119
|
+
const value = fg('one') && preCheck && fg('two') ? 'value' : '';
|
|
120
|
+
`,
|
|
121
|
+
errors: [{ messageId: 'incorrectExposure' }],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
code: outdent`
|
|
125
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
126
|
+
|
|
127
|
+
if (fg('gate') && isAdmin) {}
|
|
128
|
+
`,
|
|
129
|
+
errors: [{ messageId: 'incorrectExposure' }],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
code: outdent`
|
|
133
|
+
import { expVal } from '@atlassian/jira-feature-experiments';
|
|
134
|
+
|
|
135
|
+
if (expVal('exp') && isAdmin) {}
|
|
136
|
+
`,
|
|
137
|
+
errors: [{ messageId: 'incorrectExposure' }],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
code: outdent`
|
|
141
|
+
import { expValEquals } from '@atlassian/jira-feature-experiments';
|
|
142
|
+
|
|
143
|
+
if (expValEquals('exp', 'value') && isAdmin) {}
|
|
144
|
+
`,
|
|
145
|
+
errors: [{ messageId: 'incorrectExposure' }],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
code: outdent`
|
|
149
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
150
|
+
|
|
151
|
+
if (fg('one') && fg('two') && isAdmin) {}
|
|
152
|
+
`,
|
|
153
|
+
errors: [{ messageId: 'incorrectExposure' }, { messageId: 'useConfig' }],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
code: outdent`
|
|
157
|
+
import { fg } from '@atlassian/jira-feature-gating';
|
|
158
|
+
|
|
159
|
+
if (preGate && fg('one') || fg('two') && preGate ) {}
|
|
160
|
+
`,
|
|
161
|
+
errors: [{ messageId: 'incorrectExposure' }],
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
import { isAPIimport, type Node } from '../utils';
|
|
3
|
+
|
|
4
|
+
const isAndExpression = (node: Rule.Node): node is Node<'LogicalExpression'> =>
|
|
5
|
+
node.type === 'LogicalExpression' && node.operator === '&&';
|
|
6
|
+
|
|
7
|
+
const isExpUsage = (calleeName: string) => calleeName === 'expVal' || calleeName === 'expValEquals';
|
|
8
|
+
|
|
9
|
+
const getGateType = (node: Rule.Node, context: Rule.RuleContext): string => {
|
|
10
|
+
const { type } = node;
|
|
11
|
+
|
|
12
|
+
if (type === 'BinaryExpression') {
|
|
13
|
+
return (
|
|
14
|
+
getGateType(node.left as Node<'BinaryExpression'>, context) ||
|
|
15
|
+
getGateType(node.right as Node<'BinaryExpression'>, context)
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (node.type === 'CallExpression') {
|
|
20
|
+
const { callee } = node;
|
|
21
|
+
|
|
22
|
+
const isFeatureGate =
|
|
23
|
+
type === 'CallExpression' &&
|
|
24
|
+
callee.type === 'Identifier' &&
|
|
25
|
+
// Experiments cannot have other experiments as preconditions, only gates
|
|
26
|
+
(callee.name === 'fg' || isExpUsage(callee.name)) &&
|
|
27
|
+
isAPIimport(callee.name, context);
|
|
28
|
+
|
|
29
|
+
return isFeatureGate ? callee.name : '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return '';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getPreconditionStatus = (
|
|
36
|
+
logicalExpression: Node<'LogicalExpression'>,
|
|
37
|
+
context: Rule.RuleContext,
|
|
38
|
+
) => {
|
|
39
|
+
const { left } = logicalExpression as any;
|
|
40
|
+
// If left side is a nested AND expression then the left side node is on the nested's right
|
|
41
|
+
const leftGateType = getGateType(isAndExpression(left) ? left.right : left, context);
|
|
42
|
+
|
|
43
|
+
if (leftGateType) {
|
|
44
|
+
const rightGateType = getGateType(logicalExpression.right as any, context);
|
|
45
|
+
// Check this scenario: fg('gate') && isAdmin
|
|
46
|
+
if (!rightGateType) {
|
|
47
|
+
return 'early-exposure';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Using experiment values in logical expressions in valid
|
|
51
|
+
// i.e. expVal() && expVal()
|
|
52
|
+
if (isExpUsage(leftGateType) && isExpUsage(rightGateType)) {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Then is scenario: fg('gate1') && fg('gate2')
|
|
57
|
+
return 'unnecessary-gate';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return '';
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const rule: Rule.RuleModule = {
|
|
64
|
+
meta: {
|
|
65
|
+
docs: {
|
|
66
|
+
description: 'Inform on how to use gates and experiments in logical expressions',
|
|
67
|
+
url: 'https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/browse/platform/packages/platform/eslint-plugin/src/rules/ff/no-preconditioning/README.md',
|
|
68
|
+
},
|
|
69
|
+
messages: {
|
|
70
|
+
useConfig:
|
|
71
|
+
'Do not precondition gates or experiments with another gate. Configure this in Statsig instead to reduce unnecessary code and simplify cleanup.',
|
|
72
|
+
incorrectExposure:
|
|
73
|
+
'Evaluate gates or experiments at the end of your logical expression to ensure exposure is tracked correctly.',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
create(context) {
|
|
77
|
+
return {
|
|
78
|
+
'LogicalExpression[operator="&&"]': (node: Node<'LogicalExpression'>) => {
|
|
79
|
+
const { parent } = node;
|
|
80
|
+
// Don't analyze nested AND logical expressions
|
|
81
|
+
if (isAndExpression(parent)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isAssignmentStatement =
|
|
86
|
+
parent.type !== 'IfStatement' &&
|
|
87
|
+
parent.type !== 'ConditionalExpression' &&
|
|
88
|
+
// @ts-expect-error — this isn't a valid statement but does fail tests when removed.
|
|
89
|
+
// When updating this rule please resolve this supression.
|
|
90
|
+
!(parent.type === 'LogicalExpression' && parent.operator === '||');
|
|
91
|
+
|
|
92
|
+
let nextLogicalExpression: Node<'LogicalExpression'> | undefined = node;
|
|
93
|
+
let exposureReported = false;
|
|
94
|
+
let configReported = false;
|
|
95
|
+
|
|
96
|
+
while (nextLogicalExpression) {
|
|
97
|
+
const preconditionStatus = getPreconditionStatus(nextLogicalExpression, context);
|
|
98
|
+
// Allow us to check for: fg('') && <Component />
|
|
99
|
+
const isReturningValue =
|
|
100
|
+
// Check if we are on the root logical expression
|
|
101
|
+
// as this is where the returning value is
|
|
102
|
+
// `node` is root logical expression
|
|
103
|
+
isAssignmentStatement && nextLogicalExpression === node;
|
|
104
|
+
|
|
105
|
+
if (!exposureReported && !isReturningValue && preconditionStatus === 'early-exposure') {
|
|
106
|
+
context.report({
|
|
107
|
+
messageId: 'incorrectExposure',
|
|
108
|
+
node,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
exposureReported = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!configReported && preconditionStatus === 'unnecessary-gate') {
|
|
115
|
+
context.report({
|
|
116
|
+
messageId: 'useConfig',
|
|
117
|
+
node,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
configReported = true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (exposureReported && configReported) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
nextLogicalExpression = isAndExpression(
|
|
128
|
+
nextLogicalExpression.left as Node<'LogicalExpression'>,
|
|
129
|
+
)
|
|
130
|
+
? (nextLogicalExpression.left as Node<'LogicalExpression'>)
|
|
131
|
+
: undefined;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export default rule;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import outdent from 'outdent';
|
|
2
|
+
import { tester } from '../../../__tests__/utils/_tester';
|
|
3
|
+
import rule from '../index';
|
|
4
|
+
|
|
5
|
+
const errors = [{ messageId: 'autoFixImports' }, { messageId: 'preferFG' }];
|
|
6
|
+
|
|
7
|
+
tester.run('feature-flags/prefer-fg', rule, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
code: outdent`
|
|
11
|
+
import { getFeatureFlagValue } from '@atlassian/jira-feature-flagging';
|
|
12
|
+
|
|
13
|
+
const ffVal = getFeatureFlagValue('get.value', '');
|
|
14
|
+
`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
code: outdent`
|
|
18
|
+
import { getFeatureFlagValue } from '@atlassian/jira-feature-flagging';
|
|
19
|
+
|
|
20
|
+
const ffVal = getFeatureFlagValue('get.value', {});
|
|
21
|
+
`,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
code: outdent`
|
|
25
|
+
import { getFeatureFlagValue } from '@atlassian/jira-feature-flagging-using-meta';
|
|
26
|
+
|
|
27
|
+
const ffVal = getFeatureFlagValue('get.value', 0);
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
code: outdent`
|
|
32
|
+
import { getFeatureFlagValue } from '@atlassian/jira-feature-flagging-using-meta';
|
|
33
|
+
|
|
34
|
+
const isEnabled = getFeatureFlagValue('is.enabled', true);
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
code: outdent`
|
|
39
|
+
import { getFeatureFlagValue, getFeatureFlagClient } from '@atlassian/jira-feature-flagging-using-meta';
|
|
40
|
+
|
|
41
|
+
const ffVal = getFeatureFlagValue('get.value', {});
|
|
42
|
+
const newVal = getFeatureFlagClient().getFlagEvaluation('get.value', {});
|
|
43
|
+
`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
code: outdent`
|
|
47
|
+
import { getFeatureFlagValue } from 'thirdparty';
|
|
48
|
+
|
|
49
|
+
const isEnabled = getFeatureFlagValue('is.enabled', false);
|
|
50
|
+
`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
invalid: [
|
|
54
|
+
{
|
|
55
|
+
code: outdent`
|
|
56
|
+
import { getBooleanFF, fg } from '@atlaskit/platform-feature-flags';
|
|
57
|
+
|
|
58
|
+
const isCorrect = fg('is.correct');
|
|
59
|
+
const isAlsoEnabled = getBooleanFF('is.also.enabled', false);
|
|
60
|
+
`,
|
|
61
|
+
output: outdent`
|
|
62
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
63
|
+
|
|
64
|
+
const isCorrect = fg('is.correct');
|
|
65
|
+
const isAlsoEnabled = fg('is.also.enabled');
|
|
66
|
+
`,
|
|
67
|
+
errors,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
code: outdent`
|
|
71
|
+
import { getBooleanFF } from '@atlaskit/platform-feature-flags';
|
|
72
|
+
|
|
73
|
+
const isEnabled = getBooleanFF('is.enabled', false);
|
|
74
|
+
`,
|
|
75
|
+
output: outdent`
|
|
76
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
77
|
+
|
|
78
|
+
const isEnabled = fg('is.enabled');
|
|
79
|
+
`,
|
|
80
|
+
errors: [{ messageId: 'autoFixImports' }, { messageId: 'preferFG' }],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
|
|
3
|
+
import { FEATURE_API_IMPORT_SOURCES } from '../constants';
|
|
4
|
+
import { type Node } from '../utils';
|
|
5
|
+
|
|
6
|
+
const validateUsage = (
|
|
7
|
+
node: Node<'CallExpression'>,
|
|
8
|
+
utilName: string,
|
|
9
|
+
context: Rule.RuleContext,
|
|
10
|
+
changeMap: Map<any, any>,
|
|
11
|
+
) => {
|
|
12
|
+
const resolved = context
|
|
13
|
+
.getScope()
|
|
14
|
+
.references.find((ref) => ref.identifier.name === utilName)?.resolved;
|
|
15
|
+
|
|
16
|
+
const importSpecifierDefinition = resolved?.defs.find(
|
|
17
|
+
(def: any) =>
|
|
18
|
+
def.node?.type === 'ImportSpecifier' &&
|
|
19
|
+
FEATURE_API_IMPORT_SOURCES.has(def.parent?.source.value),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (importSpecifierDefinition) {
|
|
23
|
+
const [flagNameArg] = node.arguments;
|
|
24
|
+
|
|
25
|
+
context.report({
|
|
26
|
+
messageId: 'preferFG',
|
|
27
|
+
node,
|
|
28
|
+
*fix(fixer) {
|
|
29
|
+
yield fixer.replaceText(node, `fg(${context.sourceCode.getText(flagNameArg)})`);
|
|
30
|
+
|
|
31
|
+
const importDeclaration = importSpecifierDefinition.parent;
|
|
32
|
+
|
|
33
|
+
if (changeMap.has(importDeclaration)) {
|
|
34
|
+
const changeCounts = changeMap.get(importDeclaration);
|
|
35
|
+
|
|
36
|
+
changeMap.set(importDeclaration, {
|
|
37
|
+
...changeCounts,
|
|
38
|
+
[utilName]: changeCounts[utilName] + 1 || 1,
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
changeMap.set(importDeclaration, { [utilName]: 1 });
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const rule: Rule.RuleModule = {
|
|
49
|
+
meta: {
|
|
50
|
+
docs: {
|
|
51
|
+
url: 'https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/browse/platform/packages/platform/eslint-plugin/src/rules/ff/prefer-fg/README.md',
|
|
52
|
+
description: 'Keep usages of boolean feature flags consistent',
|
|
53
|
+
},
|
|
54
|
+
fixable: 'code',
|
|
55
|
+
messages: {
|
|
56
|
+
preferFG: 'Use `fg` instead for boolean feature flags',
|
|
57
|
+
autoFixImports: 'Use `fg` instead for boolean feature flags',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
create(context) {
|
|
61
|
+
let changeMap: Map<any, any>;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
'CallExpression[callee.name="getBooleanFF"]': (node: Node<any>) => {
|
|
65
|
+
if (node.type !== 'CallExpression') {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
changeMap = changeMap || new Map();
|
|
70
|
+
validateUsage(node, 'getBooleanFF', context, changeMap);
|
|
71
|
+
},
|
|
72
|
+
'Program:exit': () => {
|
|
73
|
+
if (changeMap?.size) {
|
|
74
|
+
changeMap.forEach((changeCounts, importDeclaration) => {
|
|
75
|
+
const [moduleScope] = context.getScope().childScopes;
|
|
76
|
+
const importSpecifiers = new Set(
|
|
77
|
+
importDeclaration.specifiers.map(({ imported }: any) => imported.name),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
importSpecifiers.add('fg');
|
|
81
|
+
|
|
82
|
+
Object.keys(changeCounts).forEach((utilName) => {
|
|
83
|
+
if (changeCounts[utilName] === moduleScope.set.get(utilName)?.references.length) {
|
|
84
|
+
importSpecifiers.delete(utilName);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
context.report({
|
|
89
|
+
messageId: 'autoFixImports',
|
|
90
|
+
node: importDeclaration,
|
|
91
|
+
fix: (fixer) =>
|
|
92
|
+
fixer.replaceText(
|
|
93
|
+
importDeclaration,
|
|
94
|
+
`import { ${Array.from(importSpecifiers).join(', ')} } from '${
|
|
95
|
+
importDeclaration.source.value
|
|
96
|
+
}';`,
|
|
97
|
+
),
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
changeMap.clear();
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default rule;
|