@atlaskit/eslint-plugin-platform 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -28
- package/afm-jira/tsconfig.json +20 -0
- package/dist/cjs/index.js +48 -2
- package/dist/cjs/rules/constants.js +11 -0
- package/dist/cjs/rules/ensure-critical-dependency-resolutions/index.js +54 -6
- package/dist/cjs/rules/ensure-native-and-af-exports-synced/index.js +16 -7
- package/dist/cjs/rules/ensure-valid-emotion-css-prop/index.js +91 -0
- package/dist/cjs/rules/inline-usage/index.js +94 -0
- package/dist/cjs/rules/no-alias/index.js +64 -0
- package/dist/cjs/rules/no-module-level-eval/index.js +45 -0
- package/dist/cjs/rules/no-preconditioning/index.js +108 -0
- package/dist/cjs/rules/prefer-fg/index.js +106 -0
- package/dist/cjs/rules/static-feature-flags/index.js +63 -0
- package/dist/cjs/rules/use-recommended-utils/index.js +47 -0
- package/dist/cjs/rules/util/registration-utils.js +2 -1
- package/dist/cjs/rules/utils.js +53 -0
- package/dist/es2019/index.js +52 -2
- package/dist/es2019/rules/constants.js +5 -0
- package/dist/es2019/rules/ensure-critical-dependency-resolutions/index.js +52 -6
- package/dist/es2019/rules/ensure-native-and-af-exports-synced/index.js +15 -7
- package/dist/es2019/rules/ensure-valid-emotion-css-prop/index.js +87 -0
- package/dist/es2019/rules/inline-usage/index.js +90 -0
- package/dist/es2019/rules/no-alias/index.js +58 -0
- package/dist/es2019/rules/no-module-level-eval/index.js +39 -0
- package/dist/es2019/rules/no-preconditioning/index.js +105 -0
- package/dist/es2019/rules/prefer-fg/index.js +81 -0
- package/dist/es2019/rules/static-feature-flags/index.js +54 -0
- package/dist/es2019/rules/use-recommended-utils/index.js +41 -0
- package/dist/es2019/rules/util/registration-utils.js +2 -1
- package/dist/es2019/rules/utils.js +29 -0
- package/dist/esm/index.js +48 -2
- package/dist/esm/rules/constants.js +5 -0
- package/dist/esm/rules/ensure-critical-dependency-resolutions/index.js +54 -6
- package/dist/esm/rules/ensure-native-and-af-exports-synced/index.js +16 -7
- package/dist/esm/rules/ensure-valid-emotion-css-prop/index.js +85 -0
- package/dist/esm/rules/inline-usage/index.js +87 -0
- package/dist/esm/rules/no-alias/index.js +57 -0
- package/dist/esm/rules/no-module-level-eval/index.js +39 -0
- package/dist/esm/rules/no-preconditioning/index.js +102 -0
- package/dist/esm/rules/prefer-fg/index.js +99 -0
- package/dist/esm/rules/static-feature-flags/index.js +56 -0
- package/dist/esm/rules/use-recommended-utils/index.js +41 -0
- package/dist/esm/rules/util/registration-utils.js +2 -1
- package/dist/esm/rules/utils.js +45 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/rules/constants.d.ts +3 -0
- package/dist/types/rules/ensure-valid-emotion-css-prop/index.d.ts +3 -0
- package/dist/types/rules/inline-usage/index.d.ts +3 -0
- package/dist/types/rules/no-alias/index.d.ts +3 -0
- package/dist/types/rules/no-module-level-eval/index.d.ts +3 -0
- package/dist/types/rules/no-preconditioning/index.d.ts +3 -0
- package/dist/types/rules/prefer-fg/index.d.ts +3 -0
- package/dist/types/rules/static-feature-flags/index.d.ts +3 -0
- package/dist/types/rules/use-recommended-utils/index.d.ts +3 -0
- package/dist/types/rules/util/registration-utils.d.ts +1 -0
- package/dist/types/rules/utils.d.ts +7 -0
- package/dist/types-ts4.5/index.d.ts +15 -0
- package/dist/types-ts4.5/rules/constants.d.ts +3 -0
- package/dist/types-ts4.5/rules/ensure-valid-emotion-css-prop/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/inline-usage/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/no-alias/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/no-module-level-eval/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/no-preconditioning/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/prefer-fg/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/static-feature-flags/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/use-recommended-utils/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/util/registration-utils.d.ts +1 -0
- package/dist/types-ts4.5/rules/utils.d.ts +7 -0
- package/index.js +9 -9
- package/package.json +43 -44
- package/report.api.md +31 -30
- package/src/__tests__/utils/_tester.tsx +16 -16
- package/src/index.tsx +102 -51
- package/src/rules/constants.tsx +20 -0
- package/src/rules/ensure-atlassian-team/__tests__/unit/rule.test.ts +19 -19
- package/src/rules/ensure-atlassian-team/index.ts +39 -52
- package/src/rules/ensure-critical-dependency-resolutions/__test__/unit/rule.test.tsx +146 -81
- package/src/rules/ensure-critical-dependency-resolutions/index.tsx +152 -97
- package/src/rules/ensure-feature-flag-prefix/__tests__/unit/rule.test.tsx +51 -51
- package/src/rules/ensure-feature-flag-prefix/index.tsx +65 -80
- package/src/rules/ensure-feature-flag-registration/__tests__/unit/rule.test.tsx +97 -97
- package/src/rules/ensure-feature-flag-registration/index.tsx +88 -105
- package/src/rules/ensure-native-and-af-exports-synced/__tests__/unit/rule.test.tsx +180 -180
- package/src/rules/ensure-native-and-af-exports-synced/index.tsx +162 -168
- package/src/rules/ensure-publish-valid/__tests__/unit/rule.test.ts +34 -36
- package/src/rules/ensure-publish-valid/index.ts +66 -81
- package/src/rules/ensure-test-runner-arguments/__tests__/unit/rule.test.tsx +93 -93
- package/src/rules/ensure-test-runner-arguments/index.tsx +107 -121
- package/src/rules/ensure-test-runner-nested-count/__tests__/unit/rule.test.tsx +43 -43
- package/src/rules/ensure-test-runner-nested-count/index.tsx +68 -70
- package/src/rules/ensure-valid-emotion-css-prop/__tests__/unit/rule.test.ts +142 -0
- package/src/rules/ensure-valid-emotion-css-prop/index.ts +96 -0
- package/src/rules/inline-usage/README.md +53 -0
- package/src/rules/inline-usage/__tests__/rule.test.tsx +106 -0
- package/src/rules/inline-usage/index.tsx +130 -0
- package/src/rules/no-alias/README.md +29 -0
- package/src/rules/no-alias/__tests__/rule.test.tsx +76 -0
- package/src/rules/no-alias/index.tsx +75 -0
- package/src/rules/no-duplicate-dependencies/__tests__/unit/rule.test.ts +44 -44
- package/src/rules/no-duplicate-dependencies/index.ts +68 -73
- package/src/rules/no-invalid-feature-flag-usage/__tests__/unit/rule.test.tsx +64 -64
- package/src/rules/no-invalid-feature-flag-usage/index.tsx +105 -112
- package/src/rules/no-invalid-storybook-decorator-usage/__tests__/unit/rule.test.tsx +13 -13
- package/src/rules/no-invalid-storybook-decorator-usage/index.tsx +28 -30
- package/src/rules/no-module-level-eval/README.md +53 -0
- package/src/rules/no-module-level-eval/__tests__/test.tsx +133 -0
- package/src/rules/no-module-level-eval/index.tsx +52 -0
- package/src/rules/no-pre-post-installs/__tests__/unit/rule.test.ts +36 -36
- package/src/rules/no-pre-post-installs/index.ts +27 -27
- package/src/rules/no-preconditioning/README.md +69 -0
- package/src/rules/no-preconditioning/__tests__/rule.test.tsx +164 -0
- package/src/rules/no-preconditioning/index.tsx +138 -0
- package/src/rules/prefer-fg/README.md +3 -0
- package/src/rules/prefer-fg/__tests__/rule.test.tsx +83 -0
- package/src/rules/prefer-fg/index.tsx +108 -0
- package/src/rules/static-feature-flags/README.md +3 -0
- package/src/rules/static-feature-flags/__tests__/test.tsx +135 -0
- package/src/rules/static-feature-flags/index.tsx +103 -0
- package/src/rules/use-recommended-utils/README.md +67 -0
- package/src/rules/use-recommended-utils/__tests__/rule.test.tsx +78 -0
- package/src/rules/use-recommended-utils/index.tsx +57 -0
- package/src/rules/util/handle-ast-object.ts +21 -32
- package/src/rules/util/registration-utils.ts +31 -30
- package/src/rules/utils.tsx +46 -0
- package/tsconfig.app.json +35 -35
- package/tsconfig.dev.json +39 -39
package/dist/es2019/index.js
CHANGED
|
@@ -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
|
-
|
|
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 = (
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
110
|
-
const
|
|
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(
|
|
122
|
+
if (afExportsValueHasExtension && path.basename(nativeExportsValue) === 'index.js') {
|
|
115
123
|
return;
|
|
116
124
|
}
|
|
117
|
-
if (afExportsValueHasExtension &&
|
|
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 && !
|
|
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;
|