@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.
Files changed (126) hide show
  1. package/CHANGELOG.md +103 -28
  2. package/afm-jira/tsconfig.json +20 -0
  3. package/dist/cjs/index.js +48 -2
  4. package/dist/cjs/rules/constants.js +11 -0
  5. package/dist/cjs/rules/ensure-critical-dependency-resolutions/index.js +54 -6
  6. package/dist/cjs/rules/ensure-native-and-af-exports-synced/index.js +16 -7
  7. package/dist/cjs/rules/ensure-valid-emotion-css-prop/index.js +91 -0
  8. package/dist/cjs/rules/inline-usage/index.js +94 -0
  9. package/dist/cjs/rules/no-alias/index.js +64 -0
  10. package/dist/cjs/rules/no-module-level-eval/index.js +45 -0
  11. package/dist/cjs/rules/no-preconditioning/index.js +108 -0
  12. package/dist/cjs/rules/prefer-fg/index.js +106 -0
  13. package/dist/cjs/rules/static-feature-flags/index.js +63 -0
  14. package/dist/cjs/rules/use-recommended-utils/index.js +47 -0
  15. package/dist/cjs/rules/util/registration-utils.js +2 -1
  16. package/dist/cjs/rules/utils.js +53 -0
  17. package/dist/es2019/index.js +52 -2
  18. package/dist/es2019/rules/constants.js +5 -0
  19. package/dist/es2019/rules/ensure-critical-dependency-resolutions/index.js +52 -6
  20. package/dist/es2019/rules/ensure-native-and-af-exports-synced/index.js +15 -7
  21. package/dist/es2019/rules/ensure-valid-emotion-css-prop/index.js +87 -0
  22. package/dist/es2019/rules/inline-usage/index.js +90 -0
  23. package/dist/es2019/rules/no-alias/index.js +58 -0
  24. package/dist/es2019/rules/no-module-level-eval/index.js +39 -0
  25. package/dist/es2019/rules/no-preconditioning/index.js +105 -0
  26. package/dist/es2019/rules/prefer-fg/index.js +81 -0
  27. package/dist/es2019/rules/static-feature-flags/index.js +54 -0
  28. package/dist/es2019/rules/use-recommended-utils/index.js +41 -0
  29. package/dist/es2019/rules/util/registration-utils.js +2 -1
  30. package/dist/es2019/rules/utils.js +29 -0
  31. package/dist/esm/index.js +48 -2
  32. package/dist/esm/rules/constants.js +5 -0
  33. package/dist/esm/rules/ensure-critical-dependency-resolutions/index.js +54 -6
  34. package/dist/esm/rules/ensure-native-and-af-exports-synced/index.js +16 -7
  35. package/dist/esm/rules/ensure-valid-emotion-css-prop/index.js +85 -0
  36. package/dist/esm/rules/inline-usage/index.js +87 -0
  37. package/dist/esm/rules/no-alias/index.js +57 -0
  38. package/dist/esm/rules/no-module-level-eval/index.js +39 -0
  39. package/dist/esm/rules/no-preconditioning/index.js +102 -0
  40. package/dist/esm/rules/prefer-fg/index.js +99 -0
  41. package/dist/esm/rules/static-feature-flags/index.js +56 -0
  42. package/dist/esm/rules/use-recommended-utils/index.js +41 -0
  43. package/dist/esm/rules/util/registration-utils.js +2 -1
  44. package/dist/esm/rules/utils.js +45 -0
  45. package/dist/types/index.d.ts +15 -0
  46. package/dist/types/rules/constants.d.ts +3 -0
  47. package/dist/types/rules/ensure-valid-emotion-css-prop/index.d.ts +3 -0
  48. package/dist/types/rules/inline-usage/index.d.ts +3 -0
  49. package/dist/types/rules/no-alias/index.d.ts +3 -0
  50. package/dist/types/rules/no-module-level-eval/index.d.ts +3 -0
  51. package/dist/types/rules/no-preconditioning/index.d.ts +3 -0
  52. package/dist/types/rules/prefer-fg/index.d.ts +3 -0
  53. package/dist/types/rules/static-feature-flags/index.d.ts +3 -0
  54. package/dist/types/rules/use-recommended-utils/index.d.ts +3 -0
  55. package/dist/types/rules/util/registration-utils.d.ts +1 -0
  56. package/dist/types/rules/utils.d.ts +7 -0
  57. package/dist/types-ts4.5/index.d.ts +15 -0
  58. package/dist/types-ts4.5/rules/constants.d.ts +3 -0
  59. package/dist/types-ts4.5/rules/ensure-valid-emotion-css-prop/index.d.ts +3 -0
  60. package/dist/types-ts4.5/rules/inline-usage/index.d.ts +3 -0
  61. package/dist/types-ts4.5/rules/no-alias/index.d.ts +3 -0
  62. package/dist/types-ts4.5/rules/no-module-level-eval/index.d.ts +3 -0
  63. package/dist/types-ts4.5/rules/no-preconditioning/index.d.ts +3 -0
  64. package/dist/types-ts4.5/rules/prefer-fg/index.d.ts +3 -0
  65. package/dist/types-ts4.5/rules/static-feature-flags/index.d.ts +3 -0
  66. package/dist/types-ts4.5/rules/use-recommended-utils/index.d.ts +3 -0
  67. package/dist/types-ts4.5/rules/util/registration-utils.d.ts +1 -0
  68. package/dist/types-ts4.5/rules/utils.d.ts +7 -0
  69. package/index.js +9 -9
  70. package/package.json +43 -44
  71. package/report.api.md +31 -30
  72. package/src/__tests__/utils/_tester.tsx +16 -16
  73. package/src/index.tsx +102 -51
  74. package/src/rules/constants.tsx +20 -0
  75. package/src/rules/ensure-atlassian-team/__tests__/unit/rule.test.ts +19 -19
  76. package/src/rules/ensure-atlassian-team/index.ts +39 -52
  77. package/src/rules/ensure-critical-dependency-resolutions/__test__/unit/rule.test.tsx +146 -81
  78. package/src/rules/ensure-critical-dependency-resolutions/index.tsx +152 -97
  79. package/src/rules/ensure-feature-flag-prefix/__tests__/unit/rule.test.tsx +51 -51
  80. package/src/rules/ensure-feature-flag-prefix/index.tsx +65 -80
  81. package/src/rules/ensure-feature-flag-registration/__tests__/unit/rule.test.tsx +97 -97
  82. package/src/rules/ensure-feature-flag-registration/index.tsx +88 -105
  83. package/src/rules/ensure-native-and-af-exports-synced/__tests__/unit/rule.test.tsx +180 -180
  84. package/src/rules/ensure-native-and-af-exports-synced/index.tsx +162 -168
  85. package/src/rules/ensure-publish-valid/__tests__/unit/rule.test.ts +34 -36
  86. package/src/rules/ensure-publish-valid/index.ts +66 -81
  87. package/src/rules/ensure-test-runner-arguments/__tests__/unit/rule.test.tsx +93 -93
  88. package/src/rules/ensure-test-runner-arguments/index.tsx +107 -121
  89. package/src/rules/ensure-test-runner-nested-count/__tests__/unit/rule.test.tsx +43 -43
  90. package/src/rules/ensure-test-runner-nested-count/index.tsx +68 -70
  91. package/src/rules/ensure-valid-emotion-css-prop/__tests__/unit/rule.test.ts +142 -0
  92. package/src/rules/ensure-valid-emotion-css-prop/index.ts +96 -0
  93. package/src/rules/inline-usage/README.md +53 -0
  94. package/src/rules/inline-usage/__tests__/rule.test.tsx +106 -0
  95. package/src/rules/inline-usage/index.tsx +130 -0
  96. package/src/rules/no-alias/README.md +29 -0
  97. package/src/rules/no-alias/__tests__/rule.test.tsx +76 -0
  98. package/src/rules/no-alias/index.tsx +75 -0
  99. package/src/rules/no-duplicate-dependencies/__tests__/unit/rule.test.ts +44 -44
  100. package/src/rules/no-duplicate-dependencies/index.ts +68 -73
  101. package/src/rules/no-invalid-feature-flag-usage/__tests__/unit/rule.test.tsx +64 -64
  102. package/src/rules/no-invalid-feature-flag-usage/index.tsx +105 -112
  103. package/src/rules/no-invalid-storybook-decorator-usage/__tests__/unit/rule.test.tsx +13 -13
  104. package/src/rules/no-invalid-storybook-decorator-usage/index.tsx +28 -30
  105. package/src/rules/no-module-level-eval/README.md +53 -0
  106. package/src/rules/no-module-level-eval/__tests__/test.tsx +133 -0
  107. package/src/rules/no-module-level-eval/index.tsx +52 -0
  108. package/src/rules/no-pre-post-installs/__tests__/unit/rule.test.ts +36 -36
  109. package/src/rules/no-pre-post-installs/index.ts +27 -27
  110. package/src/rules/no-preconditioning/README.md +69 -0
  111. package/src/rules/no-preconditioning/__tests__/rule.test.tsx +164 -0
  112. package/src/rules/no-preconditioning/index.tsx +138 -0
  113. package/src/rules/prefer-fg/README.md +3 -0
  114. package/src/rules/prefer-fg/__tests__/rule.test.tsx +83 -0
  115. package/src/rules/prefer-fg/index.tsx +108 -0
  116. package/src/rules/static-feature-flags/README.md +3 -0
  117. package/src/rules/static-feature-flags/__tests__/test.tsx +135 -0
  118. package/src/rules/static-feature-flags/index.tsx +103 -0
  119. package/src/rules/use-recommended-utils/README.md +67 -0
  120. package/src/rules/use-recommended-utils/__tests__/rule.test.tsx +78 -0
  121. package/src/rules/use-recommended-utils/index.tsx +57 -0
  122. package/src/rules/util/handle-ast-object.ts +21 -32
  123. package/src/rules/util/registration-utils.ts +31 -30
  124. package/src/rules/utils.tsx +46 -0
  125. package/tsconfig.app.json +35 -35
  126. 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
- 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:
15
- 'pre/post install scripts not allowed in package.json',
16
- },
17
- },
18
- create(context) {
19
- return {
20
- 'ObjectExpression Property[key.value=scripts] Property[key.value=/^(pre|post)install$/]':
21
- (node: Rule.Node) => {
22
- if (!context.getFilename().endsWith('/package.json')) {
23
- return;
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
- return context.report({
27
- node,
28
- messageId: 'prePostInstallScriptsNotAllowed',
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,3 @@
1
+ # Keep usages of boolean feature flags consistent (feature-flags/prefer-fg)
2
+
3
+ Use `fg` with boolean feature flags
@@ -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;
@@ -0,0 +1,3 @@
1
+ # Ensure `featureFlagName` is a string literal (feature-flags/static-feature-flags) 🍊
2
+
3
+ Ensure feature flags are static