@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
@@ -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
- node: SimpleCallExpression & Rule.NodeParentExtension,
10
- ): number => {
11
- // Calculate the depth of a binary tree, using a queue to track path
12
- let queue: (typeof node)[] = [];
13
- queue.push(node);
14
- let depth = 0;
15
- while (queue.length > 0) {
16
- let nodeCount = queue.length;
17
- while (nodeCount > 0) {
18
- let currentNode = queue.shift() as typeof node;
19
- if (
20
- currentNode.arguments[1].type === 'ArrowFunctionExpression' &&
21
- currentNode.arguments[1].body.type === 'CallExpression' &&
22
- currentNode.arguments[1].body.callee.type === 'Identifier' &&
23
- currentNode.arguments[1].body.callee.name === TEST_RUNNER_IDENTIFIER
24
- ) {
25
- queue.push({
26
- ...currentNode.arguments[1].body,
27
- parent: currentNode.parent,
28
- });
29
- }
30
- if (
31
- currentNode.arguments[2]?.type === 'ArrowFunctionExpression' &&
32
- currentNode.arguments[2].body.type === 'CallExpression' &&
33
- currentNode.arguments[2].body.callee.type === 'Identifier' &&
34
- currentNode.arguments[2].body.callee.name === TEST_RUNNER_IDENTIFIER
35
- ) {
36
- queue.push({
37
- ...currentNode.arguments[2].body,
38
- parent: currentNode.parent,
39
- });
40
- }
41
- nodeCount--;
42
- }
43
- depth++;
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
- meta: {
50
- docs: {
51
- recommended: false,
52
- },
53
- type: 'problem',
54
- messages: {
55
- tooManyNestedTestRunner:
56
- '{{nestedTestRunner}} test runners are nested. Feature flags may need a clean-up',
57
- },
58
- },
59
- create(context) {
60
- return {
61
- // Find the most outside test runner, could be inside a describe or not
62
- [`Program > * > CallExpression[callee.name=/${TEST_RUNNER_IDENTIFIER}/], CallExpression[callee.name=/describe/] > * > * > * > CallExpression[callee.name=/${TEST_RUNNER_IDENTIFIER}/]`]:
63
- (node: Rule.Node) => {
64
- if (node.type === 'CallExpression') {
65
- // Calculate the depth of nested test runners, counting from the most outside
66
- const depth = getDepthOfNestedRunner(node);
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
- if (depth > NESTED_LIMIT) {
69
- return context.report({
70
- node,
71
- messageId: 'tooManyNestedTestRunner',
72
- data: {
73
- nestedTestRunner: depth.toString(),
74
- },
75
- });
76
- }
77
- }
78
- return {};
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;