@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
@@ -9,9 +9,17 @@ import noDuplicateDependencies from './rules/no-duplicate-dependencies';
9
9
  import noInvalidFeatureFlagUsage from './rules/no-invalid-feature-flag-usage';
10
10
  import ensureFeatureFlagPrefix from './rules/ensure-feature-flag-prefix';
11
11
  import ensureCriticalDependencyResolutions from './rules/ensure-critical-dependency-resolutions';
12
+ import ensureValidEmotionCssProp from './rules/ensure-valid-emotion-css-prop';
12
13
  import noInvalidStorybookDecoratorUsage from './rules/no-invalid-storybook-decorator-usage';
13
14
  import ensurePublishValid from './rules/ensure-publish-valid';
14
15
  import ensureNativeAndAfExportsSynced from './rules/ensure-native-and-af-exports-synced';
16
+ import noModuleLevelEval from './rules/no-module-level-eval';
17
+ import staticFeatureFlags from './rules/static-feature-flags';
18
+ import noPreconditioning from './rules/no-preconditioning';
19
+ import inlineUsage from './rules/inline-usage';
20
+ import preferFG from './rules/prefer-fg';
21
+ import noAlias from './rules/no-alias';
22
+ import useRecommendedUtils from './rules/use-recommended-utils';
15
23
  export const rules = {
16
24
  'ensure-feature-flag-registration': ensureFeatureFlagRegistration,
17
25
  'ensure-feature-flag-prefix': ensureFeatureFlagPrefix,
@@ -19,12 +27,20 @@ export const rules = {
19
27
  'ensure-test-runner-nested-count': ensureTestRunnerNestedCount,
20
28
  'ensure-atlassian-team': ensureAtlassianTeam,
21
29
  'ensure-critical-dependency-resolutions': ensureCriticalDependencyResolutions,
30
+ 'ensure-valid-emotion-css-prop': ensureValidEmotionCssProp,
22
31
  'no-duplicate-dependencies': noDuplicateDependencies,
23
32
  'no-invalid-feature-flag-usage': noInvalidFeatureFlagUsage,
24
33
  'no-pre-post-install-scripts': noPreAndPostInstallScripts,
25
34
  'no-invalid-storybook-decorator-usage': noInvalidStorybookDecoratorUsage,
26
35
  'ensure-publish-valid': ensurePublishValid,
27
- 'ensure-native-and-af-exports-synced': ensureNativeAndAfExportsSynced
36
+ 'ensure-native-and-af-exports-synced': ensureNativeAndAfExportsSynced,
37
+ 'no-module-level-eval': noModuleLevelEval,
38
+ 'static-feature-flags': staticFeatureFlags,
39
+ 'no-preconditioning': noPreconditioning,
40
+ 'inline-usage': inlineUsage,
41
+ 'prefer-fg': preferFG,
42
+ 'no-alias': noAlias,
43
+ 'use-recommended-utils': useRecommendedUtils
28
44
  };
29
45
  export const configs = {
30
46
  recommended: {
@@ -38,11 +54,18 @@ export const configs = {
38
54
  '@atlaskit/platform/ensure-test-runner-nested-count': 'warn',
39
55
  '@atlaskit/platform/no-invalid-feature-flag-usage': 'error',
40
56
  '@atlaskit/platform/no-invalid-storybook-decorator-usage': 'error',
41
- '@atlaskit/platform/ensure-atlassian-team': 'error'
57
+ '@atlaskit/platform/ensure-atlassian-team': 'error',
58
+ '@atlaskit/platform/no-module-level-eval': 'error',
59
+ '@atlaskit/platform/static-feature-flags': 'error',
60
+ '@atlaskit/platform/no-preconditioning': 'error',
61
+ '@atlaskit/platform/inline-usage': 'error',
62
+ '@atlaskit/platform/prefer-fg': 'error',
63
+ '@atlaskit/platform/no-alias': 'error'
42
64
  }
43
65
  }
44
66
  };
45
67
  const jsonPrefix = '/* eslint-disable quote-props, comma-dangle, quotes, semi, eol-last, @typescript-eslint/semi, no-template-curly-in-string */ module.exports = ';
68
+ const jsonPrefixForFlatConfig = '/* eslint-disable quote-props, comma-dangle, quotes, semi, eol-last, no-template-curly-in-string */ module.exports = ';
46
69
  export const processors = {
47
70
  'package-json-processor': {
48
71
  preprocess: source => {
@@ -68,5 +91,32 @@ export const processors = {
68
91
  });
69
92
  },
70
93
  supportsAutofix: true
94
+ },
95
+ // This processor is used for ESLint FlatConfig,
96
+ // once we roll out FlatConfig, we can remove the above processor
97
+ 'package-json-processor-for-flat-config': {
98
+ preprocess: source => {
99
+ // augment the json into a js file
100
+ return [jsonPrefixForFlatConfig + source.trim()];
101
+ },
102
+ postprocess: messages => {
103
+ return messages[0].map(message => {
104
+ const {
105
+ fix
106
+ } = message;
107
+ if (!fix) {
108
+ return message;
109
+ }
110
+ const offset = jsonPrefixForFlatConfig.length;
111
+ return {
112
+ ...message,
113
+ fix: {
114
+ ...fix,
115
+ range: [fix.range[0] - offset, fix.range[1] - offset]
116
+ }
117
+ };
118
+ });
119
+ },
120
+ supportsAutofix: true
71
121
  }
72
122
  };
@@ -0,0 +1,5 @@
1
+ // List of libraries that we maintain or have worked on
2
+ // - eg `@atlaskit/feature-gate-js-client` shouldn't be included in here
3
+ export const FEATURE_API_IMPORT_SOURCES = new Set(['@atlassian/jira-feature-flagging', '@atlassian/jira-feature-flagging-using-meta', '@atlassian/jira-feature-gating', '@atlassian/jira-feature-experiments', '@atlaskit/platform-feature-flags']);
4
+ export const FEATURE_MOCKS_IMPORT_SOURCES = new Set(['@atlassian/jira-feature-flagging-mocks', '@atlassian/jira-feature-gates-test-mocks', '@atlassian/jira-feature-gates-storybook-mocks']);
5
+ export const FEATURE_UTILS_IMPORT_SOURCES = new Set(['@atlassian/jira-feature-flagging-utils', '@atlassian/jira-feature-gate-component']);
@@ -8,26 +8,63 @@ import { getObjectPropertyAsObject } from '../util/handle-ast-object';
8
8
  //
9
9
  const DESIRED_PKG_VERSIONS = {
10
10
  typescript: ['5.4'],
11
- '@types/react': ['16.14', '18.2']
11
+ tslib: ['2.6'],
12
+ '@types/react': ['16.14', '18.2'],
13
+ 'react-relay': ['npm:atl-react-relay@0.0.0-main-2ccd6998'],
14
+ 'relay-compiler': ['npm:atl-relay-compiler@0.0.0-main-2ccd6998'],
15
+ 'relay-runtime': ['npm:atl-relay-runtime@0.0.0-main-2ccd6998'],
16
+ 'relay-test-utils': ['npm:atl-relay-test-utils@0.0.0-main-2ccd6998']
12
17
  };
13
18
  const matchMinorVersion = (desiredVersion, versionInResolutions) => {
14
19
  const firstChar = versionInResolutions[0];
15
20
  // The version is invalid if it doesn't start with a number or ~
16
- if (!/^\d$/.test(firstChar) && firstChar !== '~') {
21
+ if (!/^\d$/.test(firstChar) && firstChar !== '~' && !versionInResolutions.startsWith('npm:')) {
17
22
  return false;
18
23
  }
19
24
  return versionInResolutions.startsWith(desiredVersion) || versionInResolutions.startsWith('~' + desiredVersion);
20
25
  };
21
- const verifyResolutionFromObject = (node, pkg, version, optional) => {
26
+ const verifyResolutionFromObject = ({
27
+ resolutions,
28
+ dependencies,
29
+ devDependencies,
30
+ pkg,
31
+ version,
32
+ optional
33
+ }) => {
22
34
  // For root package.json, we require the critical packages' resolutions exist and with matching version
23
35
  // For individual package's package.json, it's ok if resolutions don't exist. But if they do, the version should match
24
- const resolutionExist = node.properties.some(p => p.type === 'Property' && p.key.type === 'Literal' && p.key.value === pkg);
36
+ const resolutionExist = resolutions.properties.some(p => p.type === 'Property' && p.key.type === 'Literal' && p.key.value === pkg);
37
+ isDependencyPresent({
38
+ resolutions,
39
+ dependencies,
40
+ devDependencies,
41
+ pkg
42
+ });
25
43
  if (!resolutionExist) {
44
+ // when package is not a part of dependencies/devDependencies
45
+ if (optional === false && !isDependencyPresent({
46
+ resolutions,
47
+ dependencies,
48
+ devDependencies,
49
+ pkg
50
+ })) {
51
+ return true;
52
+ }
26
53
  return optional;
27
54
  }
28
- const resolutionExistAndMatch = node.properties.some(p => p.type === 'Property' && p.key.type === 'Literal' && p.key.value === pkg && p.value.type === 'Literal' && matchMinorVersion(version, p.value.value));
55
+ const resolutionExistAndMatch = resolutions.properties.some(p => p.type === 'Property' && p.key.type === 'Literal' && p.key.value === pkg && p.value.type === 'Literal' && matchMinorVersion(version, p.value.value));
29
56
  return resolutionExistAndMatch;
30
57
  };
58
+ const isDependencyPresent = ({
59
+ resolutions,
60
+ dependencies,
61
+ devDependencies,
62
+ pkg
63
+ }) => {
64
+ const dependencyExist = dependencies !== null && dependencies.properties.some(p => p.type === 'Property' && p.key.type === 'Literal' && p.key.value === pkg);
65
+ const devDependencyExist = devDependencies !== null && devDependencies.properties.some(p => p.type === 'Property' && p.key.type === 'Literal' && p.key.value === pkg);
66
+ return dependencyExist || devDependencyExist;
67
+ };
31
68
  const rule = {
32
69
  meta: {
33
70
  type: 'problem',
@@ -48,12 +85,21 @@ const rule = {
48
85
  return;
49
86
  }
50
87
  const packageResolutions = getObjectPropertyAsObject(node, 'resolutions');
88
+ const packageDependencies = getObjectPropertyAsObject(node, 'dependencies');
89
+ const packageDevDependencies = getObjectPropertyAsObject(node, 'devDependencies');
51
90
  const rootDir = findRootSync(process.cwd());
52
91
  const isRootPackageJson = fileName.endsWith(`${rootDir}/package.json`);
53
92
  if (packageResolutions !== null) {
54
93
  for (const [key, values] of Object.entries(DESIRED_PKG_VERSIONS)) {
55
94
  if (!values.some(value => {
56
- return verifyResolutionFromObject(packageResolutions, key, value, !isRootPackageJson);
95
+ return verifyResolutionFromObject({
96
+ resolutions: packageResolutions,
97
+ dependencies: packageDependencies,
98
+ devDependencies: packageDevDependencies,
99
+ pkg: key,
100
+ version: value,
101
+ optional: !isRootPackageJson
102
+ });
57
103
  })) {
58
104
  return context.report({
59
105
  node,
@@ -65,7 +65,7 @@ const rule = {
65
65
  });
66
66
  continue;
67
67
  }
68
- const exportValueViolations = getExportValueViolation(pkgName, afExportsKey, afExportsValue, nativeExports);
68
+ const exportValueViolations = getExportValueViolation(afExportsKey, afExportsValue, nativeExports);
69
69
  if (exportValueViolations) {
70
70
  context.report({
71
71
  data: {
@@ -83,7 +83,7 @@ const rule = {
83
83
  }
84
84
  };
85
85
  function getExportKeyViolation(afExportsKey, afExportsValue, nativeExports) {
86
- const afExportsValueHasExtension = path.extname(afExportsValue);
86
+ const afExportsValueHasExtension = path.extname(afExportsValue) !== '';
87
87
  if (afExportsValueHasExtension && !nativeExports.hasOwnProperty(afExportsKey)) {
88
88
  return {
89
89
  messageId: 'missingExportsKey',
@@ -106,15 +106,23 @@ function getExportKeyViolation(afExportsKey, afExportsValue, nativeExports) {
106
106
  };
107
107
  }
108
108
  }
109
- function getExportValueViolation(pkgName, afExportsKey, afExportsValue, nativeExports) {
110
- const afExportsValueHasExtension = path.extname(afExportsValue);
109
+ function getNativeExportsValue(afExportsKey, afExportsValueHasExtension, nativeExports) {
110
+ const nativeExportsKey = afExportsValueHasExtension ? afExportsKey : `${afExportsKey}/*`;
111
+ if (typeof nativeExports[nativeExportsKey] === 'object') {
112
+ return nativeExports[nativeExportsKey].default;
113
+ }
114
+ return nativeExports[nativeExportsKey];
115
+ }
116
+ function getExportValueViolation(afExportsKey, afExportsValue, nativeExports) {
117
+ const afExportsValueHasExtension = path.extname(afExportsValue) !== '';
118
+ const nativeExportsValue = getNativeExportsValue(afExportsKey, afExportsValueHasExtension, nativeExports);
111
119
 
112
120
  // Some entrypoints have been updated to an index.js file that registers ts-node
113
121
  // Use path.basename to get the file name to see if it is equal to 'index.js'
114
- if (afExportsValueHasExtension && path.basename(nativeExports[afExportsKey]) === 'index.js') {
122
+ if (afExportsValueHasExtension && path.basename(nativeExportsValue) === 'index.js') {
115
123
  return;
116
124
  }
117
- if (afExportsValueHasExtension && nativeExports[afExportsKey] !== afExportsValue) {
125
+ if (afExportsValueHasExtension && nativeExportsValue !== afExportsValue) {
118
126
  return {
119
127
  key: afExportsKey,
120
128
  expectedValue: afExportsValue
@@ -122,7 +130,7 @@ function getExportValueViolation(pkgName, afExportsKey, afExportsValue, nativeEx
122
130
  }
123
131
 
124
132
  // af:exports entrypoints without a file extension export the whole directory so check to ensure the exports value includes the wildcard
125
- if (!afExportsValueHasExtension && !nativeExports[`${afExportsKey}/*`].startsWith(`${afExportsValue}/*`)) {
133
+ if (!afExportsValueHasExtension && !nativeExportsValue.startsWith(`${afExportsValue}/*`)) {
126
134
  return {
127
135
  key: `${afExportsKey}/*`,
128
136
  expectedValue: `${afExportsValue}/*`
@@ -0,0 +1,87 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+
3
+ const rule = {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Ensure valid use of the `css` prop from `@emotion/react`',
8
+ recommended: true
9
+ },
10
+ messages: {
11
+ noEmotionCssImport: 'Must import `css` from `@emotion/react` when using the `css` prop.',
12
+ noEmotionCssPropFunctionCall: 'No function calls allowed when passing an object directly to the `css` prop with `@emotion/react`.'
13
+ }
14
+ },
15
+ create(context) {
16
+ let emotionJsxImported = false;
17
+ let emotionJsxImportPosition;
18
+ let emotionCssImported = false;
19
+ let cssPropExpressonUsed = false;
20
+
21
+ // Ignore files in these directories
22
+ if (/example|__tests__|__fixtures__/.test(context.filename)) {
23
+ return {};
24
+ }
25
+ return {
26
+ ImportDeclaration(node) {
27
+ if (node.source.value === '@emotion/react') {
28
+ node.specifiers.forEach(specifier => {
29
+ if (specifier.type === 'ImportSpecifier') {
30
+ if (specifier.imported.name === 'jsx') {
31
+ var _specifier$loc;
32
+ emotionJsxImported = true;
33
+ emotionJsxImportPosition = (_specifier$loc = specifier.loc) === null || _specifier$loc === void 0 ? void 0 : _specifier$loc.start;
34
+ }
35
+ if (specifier.imported.name === 'css') {
36
+ emotionCssImported = true;
37
+ }
38
+ }
39
+ });
40
+ }
41
+ },
42
+ JSXAttribute(node) {
43
+ const {
44
+ name,
45
+ value
46
+ } = node;
47
+
48
+ // Only run on emotion css props
49
+ if (!emotionJsxImported) return;
50
+ if (name.name !== 'css') return;
51
+ if (value.type === 'JSXExpressionContainer' && value.expression.type === 'ObjectExpression') {
52
+ cssPropExpressonUsed = true;
53
+ let containsFunctionExpression = false;
54
+
55
+ // Iterate over the properties of the object
56
+ value.expression.properties.forEach(prop => {
57
+ var _prop$value, _prop$value2, _prop$value3;
58
+ // Check for function expressions directly within the object literal
59
+ if (((_prop$value = prop.value) === null || _prop$value === void 0 ? void 0 : _prop$value.type) === 'ArrowFunctionExpression' || ((_prop$value2 = prop.value) === null || _prop$value2 === void 0 ? void 0 : _prop$value2.type) === 'FunctionExpression' || ((_prop$value3 = prop.value) === null || _prop$value3 === void 0 ? void 0 : _prop$value3.type) === 'CallExpression') {
60
+ containsFunctionExpression = true;
61
+ }
62
+ });
63
+
64
+ // If a function expression is found within the direct object literal, report an error
65
+ if (containsFunctionExpression) {
66
+ context.report({
67
+ node,
68
+ messageId: 'noEmotionCssPropFunctionCall'
69
+ });
70
+ }
71
+ }
72
+ },
73
+ 'Program:exit'() {
74
+ if (emotionJsxImported && cssPropExpressonUsed && !emotionCssImported) {
75
+ context.report({
76
+ messageId: 'noEmotionCssImport',
77
+ loc: emotionJsxImportPosition || {
78
+ line: 1,
79
+ column: 0
80
+ }
81
+ });
82
+ }
83
+ }
84
+ };
85
+ }
86
+ };
87
+ export default rule;
@@ -0,0 +1,90 @@
1
+ import { isAPIimport } from '../utils';
2
+ const FUNCTION_NAMES = new Set(['ff', 'fg', 'expVal', 'expValEquals', 'UNSAFE_noExposureExp']);
3
+ const STATSIG_ONLY_FUNCTION_NAMES = new Set(['fg', 'expVal', 'expValEquals', 'UNSAFE_noExposureExp']);
4
+ const findDefinitionDeclaration = node => node.type === 'VariableDeclaration' || node.type === 'FunctionDeclaration' ? node : findDefinitionDeclaration(node.parent);
5
+ const validateCallExpression = (node, context) => {
6
+ const targetedFunctionsSwitch = context.options[0] === 'ssOnly' ? STATSIG_ONLY_FUNCTION_NAMES : FUNCTION_NAMES;
7
+ const {
8
+ callee
9
+ } = node;
10
+ const shouldWarn = callee.type === 'Identifier' && targetedFunctionsSwitch.has(callee.name) && isAPIimport(callee.name, context);
11
+ if (shouldWarn) {
12
+ const defDeclaration = findDefinitionDeclaration(node.parent);
13
+ context.report({
14
+ messageId: 'inlineUsage',
15
+ node: defDeclaration.parent.type === 'ExportNamedDeclaration' ? defDeclaration.parent : defDeclaration
16
+ });
17
+ return true;
18
+ }
19
+ return false;
20
+ };
21
+ const validateBinaryExpression = (node, context) => {
22
+ // Match all comparator operators i.e ===, >=, <
23
+ if (node.operator.match(/^[=|<|>]/)) {
24
+ if (node.left.type === 'CallExpression' && validateCallExpression(node.left, context)) {
25
+ return;
26
+ }
27
+ if (node.right.type === 'CallExpression') {
28
+ validateCallExpression(node.right, context);
29
+ }
30
+ }
31
+ };
32
+ const validateReturnExpression = ({
33
+ body
34
+ }, context) => {
35
+ if (body.length !== 1) {
36
+ return;
37
+ }
38
+ const [statement] = body;
39
+ if (statement.type === 'ReturnStatement') {
40
+ const {
41
+ argument
42
+ } = statement;
43
+ if (argument && argument.type === 'CallExpression') {
44
+ validateCallExpression(argument, context);
45
+ } else if (argument && argument.type === 'BinaryExpression') {
46
+ validateBinaryExpression(argument, context);
47
+ }
48
+ }
49
+ };
50
+ const validateFunctionBody = (body, context) => {
51
+ switch (body.type) {
52
+ case 'CallExpression':
53
+ validateCallExpression(body, context);
54
+ break;
55
+ case 'BinaryExpression':
56
+ validateBinaryExpression(body, context);
57
+ break;
58
+ case 'BlockStatement':
59
+ validateReturnExpression(body, context);
60
+ break;
61
+ default:
62
+ }
63
+ };
64
+ const rule = {
65
+ meta: {
66
+ type: 'problem',
67
+ docs: {
68
+ description: 'Ensure feature flags/gates and experiments are inlined so that they can be statically analyzable.',
69
+ url: 'https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/browse/platform/packages/platform/eslint-plugin/src/rules/ff/inline-usage/README.md'
70
+ },
71
+ messages: {
72
+ inlineUsage: 'Do not export or wrap feature flags/gates and experiments usages. Inline calls at the callsite to ensure it is statically analyzable.'
73
+ }
74
+ },
75
+ create(context) {
76
+ return {
77
+ 'VariableDeclaration[declarations.length=1] > VariableDeclarator[id.type="Identifier"]:matches([init.type="ArrowFunctionExpression"], [init.type="FunctionExpression"]) > *.init': ({
78
+ body
79
+ }) => {
80
+ validateFunctionBody(body, context);
81
+ },
82
+ FunctionDeclaration: ({
83
+ body
84
+ }) => {
85
+ validateFunctionBody(body, context);
86
+ }
87
+ };
88
+ }
89
+ };
90
+ export default rule;
@@ -0,0 +1,58 @@
1
+ import { FEATURE_API_IMPORT_SOURCES, FEATURE_MOCKS_IMPORT_SOURCES, FEATURE_UTILS_IMPORT_SOURCES } from '../constants';
2
+ import { isIdentifierImportedFrom } from '../utils';
3
+ const IMPORT_SOURCES = new Set([...FEATURE_API_IMPORT_SOURCES, ...FEATURE_MOCKS_IMPORT_SOURCES, ...FEATURE_UTILS_IMPORT_SOURCES]);
4
+ const rule = {
5
+ meta: {
6
+ docs: {
7
+ url: 'https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/browse/platform/packages/platform/eslint-plugin/src/rules/ff/no-alias/README.md',
8
+ description: 'Disallow aliasing of feature flag utils to ensure feature flag usage is statically analyzable'
9
+ },
10
+ messages: {
11
+ noSpecifierAlias: 'Do not alias feature flag utils. Feature flag usage should be statically analyzable',
12
+ noNamespaceSpecifier: 'Destructure feature flag utils from import. Feature flag usage should be statically analyzable',
13
+ noReassignment: 'Do not reassign feature flag utils. Feature flag usage should be statically analyzable'
14
+ }
15
+ },
16
+ create(context) {
17
+ return {
18
+ ImportDeclaration: node => {
19
+ var _node$specifiers;
20
+ if (typeof node.source.value === 'string' && !IMPORT_SOURCES.has(node.source.value)) {
21
+ return;
22
+ }
23
+ (_node$specifiers = node.specifiers) === null || _node$specifiers === void 0 ? void 0 : _node$specifiers.forEach(specifier => {
24
+ if (specifier.type === 'ImportSpecifier') {
25
+ const {
26
+ imported,
27
+ local
28
+ } = specifier;
29
+ if (imported.name !== local.name) {
30
+ context.report({
31
+ messageId: 'noSpecifierAlias',
32
+ node: specifier
33
+ });
34
+ }
35
+ } else if (specifier.type === 'ImportNamespaceSpecifier') {
36
+ context.report({
37
+ messageId: 'noNamespaceSpecifier',
38
+ node: specifier
39
+ });
40
+ }
41
+ });
42
+ },
43
+ 'VariableDeclaration[kind="const"] > VariableDeclarator[id.type="Identifier"][init.type="Identifier"]': node => {
44
+ if (!node.init || node.init.type !== 'Identifier') {
45
+ return;
46
+ }
47
+ const isReassignment = isIdentifierImportedFrom(node.init.name, IMPORT_SOURCES, context);
48
+ if (isReassignment) {
49
+ context.report({
50
+ messageId: 'noReassignment',
51
+ node
52
+ });
53
+ }
54
+ }
55
+ };
56
+ }
57
+ };
58
+ export default rule;
@@ -0,0 +1,39 @@
1
+ import { isAPIimport } from '../utils';
2
+ const isInFunctionLevel = context => {
3
+ let scope = context.getScope();
4
+ while (((_scope = scope) === null || _scope === void 0 ? void 0 : _scope.type) !== 'module' && ((_scope2 = scope) === null || _scope2 === void 0 ? void 0 : _scope2.type) !== 'global') {
5
+ var _scope, _scope2;
6
+ if (scope.type === 'function') {
7
+ return true;
8
+ }
9
+ if (scope.type === 'class-field-initializer') {
10
+ return !scope.block.parent.static;
11
+ }
12
+ scope = scope.upper;
13
+ }
14
+ return false;
15
+ };
16
+ const rule = {
17
+ meta: {
18
+ docs: {
19
+ description: 'Disallow feature flag usage at module level',
20
+ 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'
21
+ },
22
+ messages: {
23
+ noModuleLevelEval: 'Do not evaluate feature flags at module level, it will always resolve to false when server side rendered.'
24
+ }
25
+ },
26
+ create(context) {
27
+ return {
28
+ 'CallExpression[callee.type="Identifier"]': node => {
29
+ if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && isAPIimport(node.callee.name, context) && !isInFunctionLevel(context)) {
30
+ context.report({
31
+ messageId: 'noModuleLevelEval',
32
+ node
33
+ });
34
+ }
35
+ }
36
+ };
37
+ }
38
+ };
39
+ export default rule;
@@ -0,0 +1,105 @@
1
+ import { isAPIimport } from '../utils';
2
+ const isAndExpression = node => node.type === 'LogicalExpression' && node.operator === '&&';
3
+ const isExpUsage = calleeName => calleeName === 'expVal' || calleeName === 'expValEquals';
4
+ const getGateType = (node, context) => {
5
+ const {
6
+ type
7
+ } = node;
8
+ if (type === 'BinaryExpression') {
9
+ return getGateType(node.left, context) || getGateType(node.right, context);
10
+ }
11
+ if (node.type === 'CallExpression') {
12
+ const {
13
+ callee
14
+ } = node;
15
+ const isFeatureGate = type === 'CallExpression' && callee.type === 'Identifier' && (
16
+ // Experiments cannot have other experiments as preconditions, only gates
17
+ callee.name === 'fg' || isExpUsage(callee.name)) && isAPIimport(callee.name, context);
18
+ return isFeatureGate ? callee.name : '';
19
+ }
20
+ return '';
21
+ };
22
+ const getPreconditionStatus = (logicalExpression, context) => {
23
+ const {
24
+ left
25
+ } = logicalExpression;
26
+ // If left side is a nested AND expression then the left side node is on the nested's right
27
+ const leftGateType = getGateType(isAndExpression(left) ? left.right : left, context);
28
+ if (leftGateType) {
29
+ const rightGateType = getGateType(logicalExpression.right, context);
30
+ // Check this scenario: fg('gate') && isAdmin
31
+ if (!rightGateType) {
32
+ return 'early-exposure';
33
+ }
34
+
35
+ // Using experiment values in logical expressions in valid
36
+ // i.e. expVal() && expVal()
37
+ if (isExpUsage(leftGateType) && isExpUsage(rightGateType)) {
38
+ return '';
39
+ }
40
+
41
+ // Then is scenario: fg('gate1') && fg('gate2')
42
+ return 'unnecessary-gate';
43
+ }
44
+ return '';
45
+ };
46
+ const rule = {
47
+ meta: {
48
+ docs: {
49
+ description: 'Inform on how to use gates and experiments in logical expressions',
50
+ url: 'https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/browse/platform/packages/platform/eslint-plugin/src/rules/ff/no-preconditioning/README.md'
51
+ },
52
+ messages: {
53
+ useConfig: 'Do not precondition gates or experiments with another gate. Configure this in Statsig instead to reduce unnecessary code and simplify cleanup.',
54
+ incorrectExposure: 'Evaluate gates or experiments at the end of your logical expression to ensure exposure is tracked correctly.'
55
+ }
56
+ },
57
+ create(context) {
58
+ return {
59
+ 'LogicalExpression[operator="&&"]': node => {
60
+ const {
61
+ parent
62
+ } = node;
63
+ // Don't analyze nested AND logical expressions
64
+ if (isAndExpression(parent)) {
65
+ return;
66
+ }
67
+ const isAssignmentStatement = parent.type !== 'IfStatement' && parent.type !== 'ConditionalExpression' &&
68
+ // @ts-expect-error — this isn't a valid statement but does fail tests when removed.
69
+ // When updating this rule please resolve this supression.
70
+ !(parent.type === 'LogicalExpression' && parent.operator === '||');
71
+ let nextLogicalExpression = node;
72
+ let exposureReported = false;
73
+ let configReported = false;
74
+ while (nextLogicalExpression) {
75
+ const preconditionStatus = getPreconditionStatus(nextLogicalExpression, context);
76
+ // Allow us to check for: fg('') && <Component />
77
+ const isReturningValue =
78
+ // Check if we are on the root logical expression
79
+ // as this is where the returning value is
80
+ // `node` is root logical expression
81
+ isAssignmentStatement && nextLogicalExpression === node;
82
+ if (!exposureReported && !isReturningValue && preconditionStatus === 'early-exposure') {
83
+ context.report({
84
+ messageId: 'incorrectExposure',
85
+ node
86
+ });
87
+ exposureReported = true;
88
+ }
89
+ if (!configReported && preconditionStatus === 'unnecessary-gate') {
90
+ context.report({
91
+ messageId: 'useConfig',
92
+ node
93
+ });
94
+ configReported = true;
95
+ }
96
+ if (exposureReported && configReported) {
97
+ return;
98
+ }
99
+ nextLogicalExpression = isAndExpression(nextLogicalExpression.left) ? nextLogicalExpression.left : undefined;
100
+ }
101
+ }
102
+ };
103
+ }
104
+ };
105
+ export default rule;