@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
@@ -4,132 +4,125 @@ import type { Node, Expression } from 'estree';
4
4
 
5
5
  const FF_GETTER_BOOLEAN_IDENTIFIER = 'getBooleanFF' as const;
6
6
 
7
- const __isOnlyOneFlagCheckInExpression = (
8
- root: Node | Expression,
9
- ignoredNode: Node,
10
- ): boolean => {
11
- switch (root.type) {
12
- case 'IfStatement':
13
- return __isOnlyOneFlagCheckInExpression(root.test, ignoredNode);
14
- case 'UnaryExpression':
15
- return __isOnlyOneFlagCheckInExpression(root.argument, ignoredNode);
7
+ const __isOnlyOneFlagCheckInExpression = (root: Node | Expression, ignoredNode: Node): boolean => {
8
+ switch (root.type) {
9
+ case 'IfStatement':
10
+ return __isOnlyOneFlagCheckInExpression(root.test, ignoredNode);
11
+ case 'UnaryExpression':
12
+ return __isOnlyOneFlagCheckInExpression(root.argument, ignoredNode);
16
13
 
17
- case 'CallExpression':
18
- if (root === ignoredNode) {
19
- return true;
20
- }
21
- return !(
22
- root.callee.type === 'Identifier' &&
23
- root.callee.name === FF_GETTER_BOOLEAN_IDENTIFIER
24
- );
14
+ case 'CallExpression':
15
+ if (root === ignoredNode) {
16
+ return true;
17
+ }
18
+ return !(
19
+ root.callee.type === 'Identifier' && root.callee.name === FF_GETTER_BOOLEAN_IDENTIFIER
20
+ );
25
21
 
26
- // shouldn't ever get here but just in case
27
- case 'Identifier':
28
- return root.name !== FF_GETTER_BOOLEAN_IDENTIFIER;
22
+ // shouldn't ever get here but just in case
23
+ case 'Identifier':
24
+ return root.name !== FF_GETTER_BOOLEAN_IDENTIFIER;
29
25
 
30
- case 'BinaryExpression':
31
- case 'LogicalExpression':
32
- return (
33
- __isOnlyOneFlagCheckInExpression(root.left, ignoredNode) &&
34
- __isOnlyOneFlagCheckInExpression(root.right, ignoredNode)
35
- );
26
+ case 'BinaryExpression':
27
+ case 'LogicalExpression':
28
+ return (
29
+ __isOnlyOneFlagCheckInExpression(root.left, ignoredNode) &&
30
+ __isOnlyOneFlagCheckInExpression(root.right, ignoredNode)
31
+ );
36
32
 
37
- default:
38
- return true;
39
- }
33
+ default:
34
+ return true;
35
+ }
40
36
  };
41
37
 
42
38
  const isOnlyOneFlagCheckInExpression = (node: Rule.Node): boolean => {
43
- let root = node.parent;
44
- // find the root node of the expression
45
- // NOTE: This is not an exhaustive check for all ESTree.Expression types but is good enough
46
- while (root.type.endsWith('Expression')) {
47
- root = root.parent;
48
- }
39
+ let root = node.parent;
40
+ // find the root node of the expression
41
+ // NOTE: This is not an exhaustive check for all ESTree.Expression types but is good enough
42
+ while (root.type.endsWith('Expression')) {
43
+ root = root.parent;
44
+ }
49
45
 
50
- return __isOnlyOneFlagCheckInExpression(root, node);
46
+ return __isOnlyOneFlagCheckInExpression(root, node);
51
47
  };
52
48
 
53
49
  const rule: Rule.RuleModule = {
54
- meta: {
55
- hasSuggestions: false,
56
- docs: {
57
- recommended: false,
58
- },
59
- type: 'problem',
60
- messages: {
61
- onlyInlineIf:
62
- "Only call feature flags as part of an expression, don't assign to a variable! See http://go/pff-eslint for more details",
63
- onlyStringLiteral:
64
- "Only get feature flags by string literal, don't use variables! See http://go/pff-eslint for more details",
65
- multipleFlagCheckInExpression: `Only check one flag per expression! See http://go/pff-eslint for more details`,
66
- noModuleScope: `Don't use platform feature flags in module scope! See http://go/pff-eslint for more details`,
67
- },
68
- },
69
- create(context) {
70
- return {
71
- [`CallExpression[callee.name=/${FF_GETTER_BOOLEAN_IDENTIFIER}/]`]: (
72
- node: Rule.Node,
73
- ) => {
74
- // to make typescript happy
75
- if (node.type === 'CallExpression') {
76
- const args = node.arguments;
50
+ meta: {
51
+ hasSuggestions: false,
52
+ docs: {
53
+ recommended: false,
54
+ },
55
+ type: 'problem',
56
+ messages: {
57
+ onlyInlineIf:
58
+ "Only call feature flags as part of an expression, don't assign to a variable! See http://go/pff-eslint for more details",
59
+ onlyStringLiteral:
60
+ "Only get feature flags by string literal, don't use variables! See http://go/pff-eslint for more details",
61
+ multipleFlagCheckInExpression: `Only check one flag per expression! See http://go/pff-eslint for more details`,
62
+ noModuleScope: `Don't use platform feature flags in module scope! See http://go/pff-eslint for more details`,
63
+ },
64
+ },
65
+ create(context) {
66
+ return {
67
+ [`CallExpression[callee.name=/${FF_GETTER_BOOLEAN_IDENTIFIER}/]`]: (node: Rule.Node) => {
68
+ // to make typescript happy
69
+ if (node.type === 'CallExpression') {
70
+ const args = node.arguments;
77
71
 
78
- if (args.length === 1 && args[0].type !== 'Literal') {
79
- return context.report({
80
- node,
81
- messageId: 'onlyStringLiteral',
82
- });
83
- }
72
+ if (args.length === 1 && args[0].type !== 'Literal') {
73
+ return context.report({
74
+ node,
75
+ messageId: 'onlyStringLiteral',
76
+ });
77
+ }
84
78
 
85
- switch (node.parent?.type) {
86
- case 'IfStatement':
87
- break;
88
- case 'ConditionalExpression':
89
- switch (node.parent?.parent.type) {
90
- case 'ExportDefaultDeclaration':
91
- // handles "export default getBooleanFF('test-flag') ? "this is" : "not good";"
92
- context.report({
93
- node,
94
- messageId: 'noModuleScope',
95
- });
96
- break;
97
- case 'VariableDeclarator':
98
- // handles "export const foo = getBooleanFF('test-flag') ? 'this is' : 'not good';"
99
- if (
100
- node.parent.parent.parent.type === 'VariableDeclaration' &&
101
- node.parent.parent.parent.parent.type ===
102
- 'ExportNamedDeclaration'
103
- ) {
104
- context.report({
105
- node,
106
- messageId: 'noModuleScope',
107
- });
108
- }
109
- break;
110
- }
111
- break;
112
- case 'UnaryExpression':
113
- case 'LogicalExpression':
114
- if (!isOnlyOneFlagCheckInExpression(node)) {
115
- context.report({
116
- node,
117
- messageId: 'multipleFlagCheckInExpression',
118
- });
119
- }
120
- break;
121
- default:
122
- return context.report({
123
- node,
124
- messageId: 'onlyInlineIf',
125
- });
126
- }
127
- }
79
+ switch (node.parent?.type) {
80
+ case 'IfStatement':
81
+ break;
82
+ case 'ConditionalExpression':
83
+ switch (node.parent?.parent.type) {
84
+ case 'ExportDefaultDeclaration':
85
+ // handles "export default getBooleanFF('test-flag') ? "this is" : "not good";"
86
+ context.report({
87
+ node,
88
+ messageId: 'noModuleScope',
89
+ });
90
+ break;
91
+ case 'VariableDeclarator':
92
+ // handles "export const foo = getBooleanFF('test-flag') ? 'this is' : 'not good';"
93
+ if (
94
+ node.parent.parent.parent.type === 'VariableDeclaration' &&
95
+ node.parent.parent.parent.parent.type === 'ExportNamedDeclaration'
96
+ ) {
97
+ context.report({
98
+ node,
99
+ messageId: 'noModuleScope',
100
+ });
101
+ }
102
+ break;
103
+ }
104
+ break;
105
+ case 'UnaryExpression':
106
+ case 'LogicalExpression':
107
+ if (!isOnlyOneFlagCheckInExpression(node)) {
108
+ context.report({
109
+ node,
110
+ messageId: 'multipleFlagCheckInExpression',
111
+ });
112
+ }
113
+ break;
114
+ default:
115
+ return context.report({
116
+ node,
117
+ messageId: 'onlyInlineIf',
118
+ });
119
+ }
120
+ }
128
121
 
129
- return {};
130
- },
131
- };
132
- },
122
+ return {};
123
+ },
124
+ };
125
+ },
133
126
  };
134
127
 
135
128
  export default rule;
@@ -2,17 +2,17 @@ import { tester } from '../../../../__tests__/utils/_tester';
2
2
  import rule from '../../index';
3
3
 
4
4
  describe('no-invalid-storybook-decorator-usage tests', () => {
5
- tester.run('no-invalid-storybook-decorator-usage', rule, {
6
- valid: [
7
- {
8
- code: `withPlatformFeatureFlags({})(<SampleComponent/>)`,
9
- },
10
- ],
11
- invalid: [
12
- {
13
- code: `const flags = {'uip.sample.color': true}; withPlatformFeatureFlags(flags)(<SampleComponent/>)`,
14
- errors: [{ messageId: 'onlyObjectExpression' }],
15
- },
16
- ],
17
- });
5
+ tester.run('no-invalid-storybook-decorator-usage', rule, {
6
+ valid: [
7
+ {
8
+ code: `withPlatformFeatureFlags({})(<SampleComponent/>)`,
9
+ },
10
+ ],
11
+ invalid: [
12
+ {
13
+ code: `const flags = {'uip.sample.color': true}; withPlatformFeatureFlags(flags)(<SampleComponent/>)`,
14
+ errors: [{ messageId: 'onlyObjectExpression' }],
15
+ },
16
+ ],
17
+ });
18
18
  });
@@ -4,38 +4,36 @@ import type { Rule } from 'eslint';
4
4
  const STORYBOOK_DECORATOR_IDENTIFIER = 'withPlatformFeatureFlags' as const;
5
5
 
6
6
  const rule: Rule.RuleModule = {
7
- meta: {
8
- hasSuggestions: false,
9
- docs: {
10
- recommended: false,
11
- },
12
- type: 'problem',
13
- messages: {
14
- onlyObjectExpression:
15
- 'Only object literals allowed in the storybook decorator! See http://go/pff-eslint for more details',
16
- },
17
- },
18
- create(context) {
19
- return {
20
- [`CallExpression[callee.name=/${STORYBOOK_DECORATOR_IDENTIFIER}/]`]: (
21
- node: Rule.Node,
22
- ) => {
23
- // to make typescript happy
24
- if (node.type === 'CallExpression') {
25
- const args = node.arguments;
7
+ meta: {
8
+ hasSuggestions: false,
9
+ docs: {
10
+ recommended: false,
11
+ },
12
+ type: 'problem',
13
+ messages: {
14
+ onlyObjectExpression:
15
+ 'Only object literals allowed in the storybook decorator! See http://go/pff-eslint for more details',
16
+ },
17
+ },
18
+ create(context) {
19
+ return {
20
+ [`CallExpression[callee.name=/${STORYBOOK_DECORATOR_IDENTIFIER}/]`]: (node: Rule.Node) => {
21
+ // to make typescript happy
22
+ if (node.type === 'CallExpression') {
23
+ const args = node.arguments;
26
24
 
27
- if (args.length === 1 && args[0].type !== 'ObjectExpression') {
28
- return context.report({
29
- node,
30
- messageId: 'onlyObjectExpression',
31
- });
32
- }
33
- }
25
+ if (args.length === 1 && args[0].type !== 'ObjectExpression') {
26
+ return context.report({
27
+ node,
28
+ messageId: 'onlyObjectExpression',
29
+ });
30
+ }
31
+ }
34
32
 
35
- return {};
36
- },
37
- };
38
- },
33
+ return {};
34
+ },
35
+ };
36
+ },
39
37
  };
40
38
 
41
39
  export default rule;
@@ -0,0 +1,53 @@
1
+ # Avoid using feature flags at module level (feature-flags/no-module-level-eval)
2
+
3
+ Disallow usage of feature flags in global/module scope. Due to JFE using SSR, feature flags should
4
+ not be called/initialised before components have mounted as it could cause errors.
5
+
6
+ ## Examples
7
+
8
+ 👎 Examples of **incorrect** code for this rule: ff is called in module scope
9
+
10
+ ```tsx
11
+ import { ff } from '@atlassian/jira-feature-flagging';
12
+ import { OldComponent } from './old';
13
+ import { NewComponent } from './new';
14
+
15
+ const isEnabled = ff('is_enabled');
16
+
17
+ export const Component = ff('is_redesign') ? NewComponent : OldComponent;
18
+ ```
19
+
20
+ 👍 Examples of **correct** code to feature flag components: use ComponentWithFF
21
+
22
+ ```tsx
23
+ import { ff } from '@atlassian/jira-feature-flagging';
24
+ import { ComponentWithFF } from '@atlassian/jira-feature-flagging-utils';
25
+
26
+ export const Container = () => {
27
+ const isEnabled = ff('is_enabled');
28
+ };
29
+
30
+ export const Component = ComponentWithFF('is_redesign', NewComponent, OldComponent);
31
+ ```
32
+
33
+ 👍 Examples of **correct** code to feature flag functions: use functionWithCondition
34
+
35
+ ```tsx
36
+ import { ff } from '@atlassian/jira-feature-flagging';
37
+ import { functionWithCondition } from '@atlassian/jira-feature-flagging-utils';
38
+
39
+ const selectorOld = createSelector(
40
+ permissionsSelector,
41
+ (permissions: Permissions): boolean => permissions.canCreateChildren,
42
+ );
43
+
44
+ // Change the signature of the selector by adding new arguments
45
+ const selectorNew = createSelector(
46
+ permissionsSelector,
47
+ supportedChildIssueTypesSelector,
48
+ (permissions: Permissions, supportedChildIssueTypes: IssueTypeForHierarchyLevel[]): boolean =>
49
+ permissions.canCreateChildren && !!supportedChildIssueTypes.length,
50
+ );
51
+
52
+ export const selector = functionWithCondition(() => ff('is_enabled'), selectorNew, selectorOld);
53
+ ```
@@ -0,0 +1,133 @@
1
+ import outdent from 'outdent';
2
+ import { tester } from '../../../__tests__/utils/_tester';
3
+ import rule from '../index';
4
+
5
+ tester.run('feature-flags/no-module-level-eval', rule, {
6
+ valid: [
7
+ {
8
+ name: 'Variable declaration within arrow function',
9
+ code: outdent`
10
+ import { ff } from '@atlassian/jira-feature-flagging';
11
+
12
+ const Component = () => {
13
+ const ff1 = ff('flag');
14
+ }
15
+ `,
16
+ },
17
+ {
18
+ name: 'Boolean within arrow function',
19
+ code: outdent`
20
+ import { ff } from '@atlassian/jira-feature-flagging';
21
+
22
+ const someFunction = () => {
23
+ if(ff('flag')) {
24
+ return true;
25
+ }
26
+ }
27
+ `,
28
+ },
29
+ {
30
+ name: 'Immediate call within a function',
31
+ code: outdent`
32
+ import { ff } from '@atlassian/jira-feature-flagging';
33
+
34
+ function someFunction() {
35
+ return ff('flag');
36
+ }
37
+ `,
38
+ },
39
+
40
+ {
41
+ name: 'Does not affect other function calls in global scope',
42
+ code: outdent`
43
+ import { foo } from 'foo-lib'
44
+ import { ff } from '@atlassian/jira-feature-flagging';
45
+
46
+ foo();
47
+ `,
48
+ },
49
+ {
50
+ name: 'Variable declaration within an arrow function with different import source',
51
+ code: outdent`
52
+ import { ff } from '@atlassian/jira-feature-flagging-using-meta';
53
+
54
+ const Component = () => {
55
+ const ff1 = ff('flag');
56
+ }
57
+ `,
58
+ },
59
+ {
60
+ name: 'Class field',
61
+ code: outdent`
62
+ import { ff } from '@atlassian/jira-feature-flagging';
63
+
64
+ export class Component {
65
+ value = ff('flag') ? 'newValue' : 'oldValue';
66
+ }
67
+ `,
68
+ },
69
+ ],
70
+ invalid: [
71
+ {
72
+ name: 'Immediate call',
73
+ code: outdent`
74
+ import { ff } from '@atlassian/jira-feature-flagging';
75
+
76
+ ff('flag');
77
+ `,
78
+ errors: [{ messageId: 'noModuleLevelEval' }],
79
+ },
80
+ {
81
+ name: 'Module level storage into a variable',
82
+ code: outdent`
83
+ import { ff } from '@atlassian/jira-feature-flagging';
84
+
85
+ const flag = ff('flag');
86
+ `,
87
+ errors: [{ messageId: 'noModuleLevelEval' }],
88
+ },
89
+ {
90
+ name: 'Module level collection',
91
+ code: outdent`
92
+ import { ff } from '@atlassian/jira-feature-flagging';
93
+
94
+ const collection = [ff('flag') ? 0 : 1];
95
+ `,
96
+ errors: [{ messageId: 'noModuleLevelEval' }],
97
+ },
98
+ {
99
+ name: 'Module level object',
100
+ code: outdent`
101
+ import { ff } from '@atlassian/jira-feature-flagging';
102
+
103
+ const object = {
104
+ isEnabled: ff('flag')
105
+ }
106
+ `,
107
+ errors: [{ messageId: 'noModuleLevelEval' }],
108
+ },
109
+ {
110
+ name: 'Module level with boolean',
111
+ code: outdent`
112
+ import { ff } from '@atlassian/jira-feature-flagging';
113
+ import { NewComp } from 'new';
114
+ import { OldComp } from 'old';
115
+
116
+ export const Component = ff('flag') ? NewComp : OldComp;
117
+ `,
118
+ errors: [{ messageId: 'noModuleLevelEval' }],
119
+ },
120
+ // This test fails unsure what the environment difference is between Jira + Platform.
121
+ // {
122
+ // name: 'Static class field',
123
+ // code: outdent`
124
+ // import { ff } from '@atlassian/jira-feature-flagging';
125
+
126
+ // export class Component {
127
+ // static value = ff('flag') ? 'newValue' : 'oldValue';
128
+ // }
129
+ // `,
130
+ // errors: [{ messageId: 'noModuleLevelEval' }],
131
+ // },
132
+ ],
133
+ });
@@ -0,0 +1,52 @@
1
+ import type { Rule } from 'eslint';
2
+ import { isAPIimport, type Node } from '../utils';
3
+
4
+ const isInFunctionLevel = (context: Rule.RuleContext) => {
5
+ let scope: any = context.getScope();
6
+
7
+ while (scope?.type !== 'module' && scope?.type !== 'global') {
8
+ if (scope.type === 'function') {
9
+ return true;
10
+ }
11
+
12
+ if (scope.type === 'class-field-initializer') {
13
+ return !scope.block.parent.static;
14
+ }
15
+
16
+ scope = scope.upper;
17
+ }
18
+
19
+ return false;
20
+ };
21
+
22
+ const rule: Rule.RuleModule = {
23
+ meta: {
24
+ docs: {
25
+ description: 'Disallow feature flag usage at module level',
26
+ url: 'https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/browse/platform/packages/platform/eslint-plugin/src/rules/ff/no-module-level-eval/README.md',
27
+ },
28
+ messages: {
29
+ noModuleLevelEval:
30
+ 'Do not evaluate feature flags at module level, it will always resolve to false when server side rendered.',
31
+ },
32
+ },
33
+ create(context) {
34
+ return {
35
+ 'CallExpression[callee.type="Identifier"]': (node: Node<any>) => {
36
+ if (
37
+ node.type === 'CallExpression' &&
38
+ node.callee.type === 'Identifier' &&
39
+ isAPIimport(node.callee.name, context) &&
40
+ !isInFunctionLevel(context)
41
+ ) {
42
+ context.report({
43
+ messageId: 'noModuleLevelEval',
44
+ node,
45
+ });
46
+ }
47
+ },
48
+ };
49
+ },
50
+ };
51
+
52
+ export default rule;
@@ -2,40 +2,40 @@ import { tester } from '../../../../__tests__/utils/_tester';
2
2
  import rule from '../../index';
3
3
 
4
4
  describe('test no-pre-post-installs rule', () => {
5
- tester.run('no-pre-post-installs', rule, {
6
- valid: [
7
- {
8
- code: `const foo = { "scripts": { "preinstall": 1, "postinstall": 2 }}`,
9
- filename: 'hello/foo.ts',
10
- },
11
- {
12
- code: `const foo = { "scripts": { "preinstall": 1, "postinstall": 2 }}`,
13
- filename: 'foo/dummy.json',
14
- },
15
- {
16
- code: `const foo = { "scripts": { "bar": 1, "dummy": 'echo 1' }}`,
17
- filename: 'foo/package.json',
18
- },
19
- {
20
- code: `module.exports = { "scripts": { "fakePreinstall": 1 }};`,
21
- filename: 'bar/package.json',
22
- },
23
- {
24
- code: `module.exports = { "scripts": { "fakePostinstall": 1 }};`,
25
- filename: 'bar/package.json',
26
- },
27
- ],
28
- invalid: [
29
- {
30
- code: `module.exports = { "scripts": { "preinstall": 1 }};`,
31
- filename: 'bar/package.json',
32
- errors: [{ messageId: 'prePostInstallScriptsNotAllowed' }],
33
- },
34
- {
35
- code: `const foo = { "scripts": { "postinstall": 1 }}`,
36
- filename: 'baz/package.json',
37
- errors: [{ messageId: 'prePostInstallScriptsNotAllowed' }],
38
- },
39
- ],
40
- });
5
+ tester.run('no-pre-post-installs', rule, {
6
+ valid: [
7
+ {
8
+ code: `const foo = { "scripts": { "preinstall": 1, "postinstall": 2 }}`,
9
+ filename: 'hello/foo.ts',
10
+ },
11
+ {
12
+ code: `const foo = { "scripts": { "preinstall": 1, "postinstall": 2 }}`,
13
+ filename: 'foo/dummy.json',
14
+ },
15
+ {
16
+ code: `const foo = { "scripts": { "bar": 1, "dummy": 'echo 1' }}`,
17
+ filename: 'foo/package.json',
18
+ },
19
+ {
20
+ code: `module.exports = { "scripts": { "fakePreinstall": 1 }};`,
21
+ filename: 'bar/package.json',
22
+ },
23
+ {
24
+ code: `module.exports = { "scripts": { "fakePostinstall": 1 }};`,
25
+ filename: 'bar/package.json',
26
+ },
27
+ ],
28
+ invalid: [
29
+ {
30
+ code: `module.exports = { "scripts": { "preinstall": 1 }};`,
31
+ filename: 'bar/package.json',
32
+ errors: [{ messageId: 'prePostInstallScriptsNotAllowed' }],
33
+ },
34
+ {
35
+ code: `const foo = { "scripts": { "postinstall": 1 }}`,
36
+ filename: 'baz/package.json',
37
+ errors: [{ messageId: 'prePostInstallScriptsNotAllowed' }],
38
+ },
39
+ ],
40
+ });
41
41
  });