@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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
|
|
2
|
+
import { FEATURE_API_IMPORT_SOURCES, FEATURE_MOCKS_IMPORT_SOURCES, FEATURE_UTILS_IMPORT_SOURCES } from '../constants';
|
|
3
|
+
import { isIdentifierImportedFrom } from '../utils';
|
|
4
|
+
var IMPORT_SOURCES = new Set([].concat(_toConsumableArray(FEATURE_API_IMPORT_SOURCES), _toConsumableArray(FEATURE_MOCKS_IMPORT_SOURCES), _toConsumableArray(FEATURE_UTILS_IMPORT_SOURCES)));
|
|
5
|
+
var rule = {
|
|
6
|
+
meta: {
|
|
7
|
+
docs: {
|
|
8
|
+
url: 'https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/browse/platform/packages/platform/eslint-plugin/src/rules/ff/no-alias/README.md',
|
|
9
|
+
description: 'Disallow aliasing of feature flag utils to ensure feature flag usage is statically analyzable'
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
noSpecifierAlias: 'Do not alias feature flag utils. Feature flag usage should be statically analyzable',
|
|
13
|
+
noNamespaceSpecifier: 'Destructure feature flag utils from import. Feature flag usage should be statically analyzable',
|
|
14
|
+
noReassignment: 'Do not reassign feature flag utils. Feature flag usage should be statically analyzable'
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
create: function create(context) {
|
|
18
|
+
return {
|
|
19
|
+
ImportDeclaration: function ImportDeclaration(node) {
|
|
20
|
+
var _node$specifiers;
|
|
21
|
+
if (typeof node.source.value === 'string' && !IMPORT_SOURCES.has(node.source.value)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
(_node$specifiers = node.specifiers) === null || _node$specifiers === void 0 || _node$specifiers.forEach(function (specifier) {
|
|
25
|
+
if (specifier.type === 'ImportSpecifier') {
|
|
26
|
+
var imported = specifier.imported,
|
|
27
|
+
local = specifier.local;
|
|
28
|
+
if (imported.name !== local.name) {
|
|
29
|
+
context.report({
|
|
30
|
+
messageId: 'noSpecifierAlias',
|
|
31
|
+
node: specifier
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
} else if (specifier.type === 'ImportNamespaceSpecifier') {
|
|
35
|
+
context.report({
|
|
36
|
+
messageId: 'noNamespaceSpecifier',
|
|
37
|
+
node: specifier
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
'VariableDeclaration[kind="const"] > VariableDeclarator[id.type="Identifier"][init.type="Identifier"]': function VariableDeclarationKindConstVariableDeclaratorIdTypeIdentifierInitTypeIdentifier(node) {
|
|
43
|
+
if (!node.init || node.init.type !== 'Identifier') {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
var isReassignment = isIdentifierImportedFrom(node.init.name, IMPORT_SOURCES, context);
|
|
47
|
+
if (isReassignment) {
|
|
48
|
+
context.report({
|
|
49
|
+
messageId: 'noReassignment',
|
|
50
|
+
node: node
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
export default rule;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { isAPIimport } from '../utils';
|
|
2
|
+
var isInFunctionLevel = function isInFunctionLevel(context) {
|
|
3
|
+
var 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
|
+
var 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: function create(context) {
|
|
27
|
+
return {
|
|
28
|
+
'CallExpression[callee.type="Identifier"]': function CallExpressionCalleeTypeIdentifier(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: node
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
export default rule;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { isAPIimport } from '../utils';
|
|
2
|
+
var isAndExpression = function isAndExpression(node) {
|
|
3
|
+
return node.type === 'LogicalExpression' && node.operator === '&&';
|
|
4
|
+
};
|
|
5
|
+
var isExpUsage = function isExpUsage(calleeName) {
|
|
6
|
+
return calleeName === 'expVal' || calleeName === 'expValEquals';
|
|
7
|
+
};
|
|
8
|
+
var getGateType = function getGateType(node, context) {
|
|
9
|
+
var type = node.type;
|
|
10
|
+
if (type === 'BinaryExpression') {
|
|
11
|
+
return getGateType(node.left, context) || getGateType(node.right, context);
|
|
12
|
+
}
|
|
13
|
+
if (node.type === 'CallExpression') {
|
|
14
|
+
var callee = node.callee;
|
|
15
|
+
var 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
|
+
var getPreconditionStatus = function getPreconditionStatus(logicalExpression, context) {
|
|
23
|
+
var _ref = logicalExpression,
|
|
24
|
+
left = _ref.left;
|
|
25
|
+
// If left side is a nested AND expression then the left side node is on the nested's right
|
|
26
|
+
var leftGateType = getGateType(isAndExpression(left) ? left.right : left, context);
|
|
27
|
+
if (leftGateType) {
|
|
28
|
+
var rightGateType = getGateType(logicalExpression.right, context);
|
|
29
|
+
// Check this scenario: fg('gate') && isAdmin
|
|
30
|
+
if (!rightGateType) {
|
|
31
|
+
return 'early-exposure';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Using experiment values in logical expressions in valid
|
|
35
|
+
// i.e. expVal() && expVal()
|
|
36
|
+
if (isExpUsage(leftGateType) && isExpUsage(rightGateType)) {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Then is scenario: fg('gate1') && fg('gate2')
|
|
41
|
+
return 'unnecessary-gate';
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
};
|
|
45
|
+
var rule = {
|
|
46
|
+
meta: {
|
|
47
|
+
docs: {
|
|
48
|
+
description: 'Inform on how to use gates and experiments in logical expressions',
|
|
49
|
+
url: 'https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/browse/platform/packages/platform/eslint-plugin/src/rules/ff/no-preconditioning/README.md'
|
|
50
|
+
},
|
|
51
|
+
messages: {
|
|
52
|
+
useConfig: 'Do not precondition gates or experiments with another gate. Configure this in Statsig instead to reduce unnecessary code and simplify cleanup.',
|
|
53
|
+
incorrectExposure: 'Evaluate gates or experiments at the end of your logical expression to ensure exposure is tracked correctly.'
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
create: function create(context) {
|
|
57
|
+
return {
|
|
58
|
+
'LogicalExpression[operator="&&"]': function LogicalExpressionOperator(node) {
|
|
59
|
+
var parent = node.parent;
|
|
60
|
+
// Don't analyze nested AND logical expressions
|
|
61
|
+
if (isAndExpression(parent)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
var isAssignmentStatement = parent.type !== 'IfStatement' && parent.type !== 'ConditionalExpression' &&
|
|
65
|
+
// @ts-expect-error — this isn't a valid statement but does fail tests when removed.
|
|
66
|
+
// When updating this rule please resolve this supression.
|
|
67
|
+
!(parent.type === 'LogicalExpression' && parent.operator === '||');
|
|
68
|
+
var nextLogicalExpression = node;
|
|
69
|
+
var exposureReported = false;
|
|
70
|
+
var configReported = false;
|
|
71
|
+
while (nextLogicalExpression) {
|
|
72
|
+
var preconditionStatus = getPreconditionStatus(nextLogicalExpression, context);
|
|
73
|
+
// Allow us to check for: fg('') && <Component />
|
|
74
|
+
var isReturningValue =
|
|
75
|
+
// Check if we are on the root logical expression
|
|
76
|
+
// as this is where the returning value is
|
|
77
|
+
// `node` is root logical expression
|
|
78
|
+
isAssignmentStatement && nextLogicalExpression === node;
|
|
79
|
+
if (!exposureReported && !isReturningValue && preconditionStatus === 'early-exposure') {
|
|
80
|
+
context.report({
|
|
81
|
+
messageId: 'incorrectExposure',
|
|
82
|
+
node: node
|
|
83
|
+
});
|
|
84
|
+
exposureReported = true;
|
|
85
|
+
}
|
|
86
|
+
if (!configReported && preconditionStatus === 'unnecessary-gate') {
|
|
87
|
+
context.report({
|
|
88
|
+
messageId: 'useConfig',
|
|
89
|
+
node: node
|
|
90
|
+
});
|
|
91
|
+
configReported = true;
|
|
92
|
+
}
|
|
93
|
+
if (exposureReported && configReported) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
nextLogicalExpression = isAndExpression(nextLogicalExpression.left) ? nextLogicalExpression.left : undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
export default rule;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
3
|
+
import _regeneratorRuntime from "@babel/runtime/regenerator";
|
|
4
|
+
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
5
|
+
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
6
|
+
import { FEATURE_API_IMPORT_SOURCES } from '../constants';
|
|
7
|
+
var validateUsage = function validateUsage(node, utilName, context, changeMap) {
|
|
8
|
+
var _context$getScope$ref;
|
|
9
|
+
var resolved = (_context$getScope$ref = context.getScope().references.find(function (ref) {
|
|
10
|
+
return ref.identifier.name === utilName;
|
|
11
|
+
})) === null || _context$getScope$ref === void 0 ? void 0 : _context$getScope$ref.resolved;
|
|
12
|
+
var importSpecifierDefinition = resolved === null || resolved === void 0 ? void 0 : resolved.defs.find(function (def) {
|
|
13
|
+
var _def$node, _def$parent;
|
|
14
|
+
return ((_def$node = def.node) === null || _def$node === void 0 ? void 0 : _def$node.type) === 'ImportSpecifier' && FEATURE_API_IMPORT_SOURCES.has((_def$parent = def.parent) === null || _def$parent === void 0 ? void 0 : _def$parent.source.value);
|
|
15
|
+
});
|
|
16
|
+
if (importSpecifierDefinition) {
|
|
17
|
+
var _node$arguments = _slicedToArray(node.arguments, 1),
|
|
18
|
+
flagNameArg = _node$arguments[0];
|
|
19
|
+
context.report({
|
|
20
|
+
messageId: 'preferFG',
|
|
21
|
+
node: node,
|
|
22
|
+
fix: /*#__PURE__*/_regeneratorRuntime.mark(function fix(fixer) {
|
|
23
|
+
var importDeclaration, changeCounts;
|
|
24
|
+
return _regeneratorRuntime.wrap(function fix$(_context) {
|
|
25
|
+
while (1) switch (_context.prev = _context.next) {
|
|
26
|
+
case 0:
|
|
27
|
+
_context.next = 2;
|
|
28
|
+
return fixer.replaceText(node, "fg(".concat(context.sourceCode.getText(flagNameArg), ")"));
|
|
29
|
+
case 2:
|
|
30
|
+
importDeclaration = importSpecifierDefinition.parent;
|
|
31
|
+
if (changeMap.has(importDeclaration)) {
|
|
32
|
+
changeCounts = changeMap.get(importDeclaration);
|
|
33
|
+
changeMap.set(importDeclaration, _objectSpread(_objectSpread({}, changeCounts), {}, _defineProperty({}, utilName, changeCounts[utilName] + 1 || 1)));
|
|
34
|
+
} else {
|
|
35
|
+
changeMap.set(importDeclaration, _defineProperty({}, utilName, 1));
|
|
36
|
+
}
|
|
37
|
+
case 4:
|
|
38
|
+
case "end":
|
|
39
|
+
return _context.stop();
|
|
40
|
+
}
|
|
41
|
+
}, fix);
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var rule = {
|
|
47
|
+
meta: {
|
|
48
|
+
docs: {
|
|
49
|
+
url: 'https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/browse/platform/packages/platform/eslint-plugin/src/rules/ff/prefer-fg/README.md',
|
|
50
|
+
description: 'Keep usages of boolean feature flags consistent'
|
|
51
|
+
},
|
|
52
|
+
fixable: 'code',
|
|
53
|
+
messages: {
|
|
54
|
+
preferFG: 'Use `fg` instead for boolean feature flags',
|
|
55
|
+
autoFixImports: 'Use `fg` instead for boolean feature flags'
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
create: function create(context) {
|
|
59
|
+
var changeMap;
|
|
60
|
+
return {
|
|
61
|
+
'CallExpression[callee.name="getBooleanFF"]': function CallExpressionCalleeNameGetBooleanFF(node) {
|
|
62
|
+
if (node.type !== 'CallExpression') {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
changeMap = changeMap || new Map();
|
|
66
|
+
validateUsage(node, 'getBooleanFF', context, changeMap);
|
|
67
|
+
},
|
|
68
|
+
'Program:exit': function ProgramExit() {
|
|
69
|
+
var _changeMap;
|
|
70
|
+
if ((_changeMap = changeMap) !== null && _changeMap !== void 0 && _changeMap.size) {
|
|
71
|
+
changeMap.forEach(function (changeCounts, importDeclaration) {
|
|
72
|
+
var _context$getScope$chi = _slicedToArray(context.getScope().childScopes, 1),
|
|
73
|
+
moduleScope = _context$getScope$chi[0];
|
|
74
|
+
var importSpecifiers = new Set(importDeclaration.specifiers.map(function (_ref) {
|
|
75
|
+
var imported = _ref.imported;
|
|
76
|
+
return imported.name;
|
|
77
|
+
}));
|
|
78
|
+
importSpecifiers.add('fg');
|
|
79
|
+
Object.keys(changeCounts).forEach(function (utilName) {
|
|
80
|
+
var _moduleScope$set$get;
|
|
81
|
+
if (changeCounts[utilName] === ((_moduleScope$set$get = moduleScope.set.get(utilName)) === null || _moduleScope$set$get === void 0 ? void 0 : _moduleScope$set$get.references.length)) {
|
|
82
|
+
importSpecifiers.delete(utilName);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
context.report({
|
|
86
|
+
messageId: 'autoFixImports',
|
|
87
|
+
node: importDeclaration,
|
|
88
|
+
fix: function fix(fixer) {
|
|
89
|
+
return fixer.replaceText(importDeclaration, "import { ".concat(Array.from(importSpecifiers).join(', '), " } from '").concat(importDeclaration.source.value, "';"));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
changeMap.clear();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
export default rule;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
|
|
2
|
+
import { FEATURE_API_IMPORT_SOURCES } from '../constants';
|
|
3
|
+
import { getDef, isIdentifierImportedFrom } from '../utils';
|
|
4
|
+
var IMPORT_SOURCES = new Set([].concat(_toConsumableArray(FEATURE_API_IMPORT_SOURCES), ['@atlassian/jira-feature-flagging-utils', '@atlassian/jira-feature-gate-component', '@atlassian/jira-feature-gates-test-mocks', '@atlassian/jira-feature-gates-storybook-mocks']));
|
|
5
|
+
|
|
6
|
+
// Any functions not in this list should be skipped for performance.
|
|
7
|
+
var FUNCTION_NAMES = new Set(['ff', 'fg', 'getFeatureFlagValue', 'getMultivariateFeatureFlag', 'componentWithFF', 'componentWithFG', 'passGate', 'withGate', 'expVal', 'expValEquals', 'UNSAFE_noExposureExp', 'mockExp', 'withExp', 'wasExperimentManuallyExposed']);
|
|
8
|
+
var STATSIG_ONLY_FUNCTION_NAMES = new Set(['fg', 'componentWithFG', 'passGate', 'withGate', 'expVal', 'expValEquals', 'UNSAFE_noExposureExp', 'mockExp', 'withExp', 'wasExperimentManuallyExposed']);
|
|
9
|
+
var rule = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
docs: {
|
|
13
|
+
url: 'https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/browse/platform/packages/platform/eslint-plugin/src/rules/ff/static-feature-flags/README.md',
|
|
14
|
+
description: 'Ensure feature flags or gates are static string literals'
|
|
15
|
+
},
|
|
16
|
+
fixable: 'code',
|
|
17
|
+
messages: {
|
|
18
|
+
FFLiteral: 'Use static string literal for `featureFlagName`. See https://team.atlassian.com/project/ATLAS-46997/about'
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
create: function create(context) {
|
|
22
|
+
var targetedFunctionsSwitch = context.options[0] === 'ssOnly' ? STATSIG_ONLY_FUNCTION_NAMES : FUNCTION_NAMES;
|
|
23
|
+
return {
|
|
24
|
+
// When they're not literals, show a message
|
|
25
|
+
'CallExpression[callee.type="Identifier"][arguments.length>0][arguments.0.type!="Literal"]': function CallExpressionCalleeTypeIdentifierArgumentsLength0Arguments0TypeLiteral(node) {
|
|
26
|
+
if (node.type !== 'CallExpression') {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (node.callee.type === 'Identifier' && (!targetedFunctionsSwitch.has(node.callee.name) || !isIdentifierImportedFrom(node.callee.name, IMPORT_SOURCES, context))) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
var nameArgument = node.arguments[0];
|
|
33
|
+
if (nameArgument.type === 'Identifier') {
|
|
34
|
+
var def = getDef(nameArgument.name, context);
|
|
35
|
+
if (def != null && def.type === 'Variable') {
|
|
36
|
+
var _ref = def.node.init,
|
|
37
|
+
value = _ref.value;
|
|
38
|
+
context.report({
|
|
39
|
+
node: nameArgument,
|
|
40
|
+
messageId: 'FFLiteral',
|
|
41
|
+
fix: function fix(fixer) {
|
|
42
|
+
return fixer.replaceText(nameArgument, "'".concat(value, "'"));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
context.report({
|
|
49
|
+
node: nameArgument,
|
|
50
|
+
messageId: 'FFLiteral'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
export default rule;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isIdentifierImportedFrom } from '../utils';
|
|
2
|
+
var BANNED_IMPORTS_SET = new Set(['@atlaskit/feature-gate-js-client']);
|
|
3
|
+
var rule = {
|
|
4
|
+
meta: {
|
|
5
|
+
docs: {
|
|
6
|
+
url: 'https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/browse/platform/packages/platform/eslint-plugin/src/rules/ff/use-recommended-utils/README.md',
|
|
7
|
+
description: 'Prefer using the feature flag abstraction over direct statsig library.'
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
notSupported: 'Experimentation is not suported in platform feature flags, reach out to #help-statsig-switcheroo.',
|
|
11
|
+
useRecommended: 'Please do not use FeatureGates.{{util}}, use {{recommended}} from {{lib}} instead.'
|
|
12
|
+
},
|
|
13
|
+
type: 'problem'
|
|
14
|
+
},
|
|
15
|
+
create: function create(context) {
|
|
16
|
+
return {
|
|
17
|
+
'CallExpression > MemberExpression:matches([property.name="checkGate"])': function CallExpressionMemberExpressionMatchesPropertyNameCheckGate(node) {
|
|
18
|
+
if (node.object.type === 'Identifier' && isIdentifierImportedFrom(node.object.name, BANNED_IMPORTS_SET, context)) {
|
|
19
|
+
context.report({
|
|
20
|
+
messageId: 'useRecommended',
|
|
21
|
+
node: node,
|
|
22
|
+
data: {
|
|
23
|
+
lib: '`@atlaskit/platform-feature-flags`',
|
|
24
|
+
util: node.property.name,
|
|
25
|
+
recommended: '`fg`'
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
'CallExpression > MemberExpression:matches([property.name="getExperimentValue"])': function CallExpressionMemberExpressionMatchesPropertyNameGetExperimentValue(node) {
|
|
31
|
+
if (node.object.type === 'Identifier' && isIdentifierImportedFrom(node.object.name, BANNED_IMPORTS_SET, context)) {
|
|
32
|
+
context.report({
|
|
33
|
+
messageId: 'notSupported',
|
|
34
|
+
node: node
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
export default rule;
|
|
@@ -7,7 +7,8 @@ import Fuse from 'fuse.js';
|
|
|
7
7
|
// if you don't want to verify the type use `null` as the value
|
|
8
8
|
export var getterIdentifierToFlagTypeMap = {
|
|
9
9
|
getBooleanFF: 'boolean',
|
|
10
|
-
ffTest: 'boolean'
|
|
10
|
+
ffTest: 'boolean',
|
|
11
|
+
fg: 'boolean'
|
|
11
12
|
};
|
|
12
13
|
// make sure we cache reading the package.json so we don't end up reading it for every instance of this rule.
|
|
13
14
|
var pkgJsonCache = new Map();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
|
|
2
|
+
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
3
|
+
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
|
|
4
|
+
import { FEATURE_API_IMPORT_SOURCES } from './constants';
|
|
5
|
+
export function isIdentifierImportedFrom(identifierName, sources, context) {
|
|
6
|
+
if (sources.size > 0) {
|
|
7
|
+
var _context$getScope$ref, _context$getScope$ref2;
|
|
8
|
+
return (_context$getScope$ref = (_context$getScope$ref2 = context.getScope().references.find(function (ref) {
|
|
9
|
+
return ref.identifier.name === identifierName;
|
|
10
|
+
})) === null || _context$getScope$ref2 === void 0 || (_context$getScope$ref2 = _context$getScope$ref2.resolved) === null || _context$getScope$ref2 === void 0 ? void 0 : _context$getScope$ref2.defs.some(function (def) {
|
|
11
|
+
var _def$parent;
|
|
12
|
+
return ((_def$parent = def.parent) === null || _def$parent === void 0 ? void 0 : _def$parent.type) === 'ImportDeclaration' && sources.has(def.parent.source.value + '');
|
|
13
|
+
})) !== null && _context$getScope$ref !== void 0 ? _context$getScope$ref : false;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
export function isAPIimport(functionName, context) {
|
|
18
|
+
return isIdentifierImportedFrom(functionName, FEATURE_API_IMPORT_SOURCES, context);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// returns the definition node of a variable if it's declared within the scope of the file
|
|
22
|
+
export function getDef(name, context) {
|
|
23
|
+
var scope = context.getScope();
|
|
24
|
+
while (scope && scope.type !== 'global') {
|
|
25
|
+
var _iterator = _createForOfIteratorHelper(scope.variables),
|
|
26
|
+
_step;
|
|
27
|
+
try {
|
|
28
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
29
|
+
var variable = _step.value;
|
|
30
|
+
if (variable.name === name) {
|
|
31
|
+
var definition = variable.defs.find(function (def) {
|
|
32
|
+
return def.node && def.node.type === 'VariableDeclarator';
|
|
33
|
+
});
|
|
34
|
+
return definition;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
_iterator.e(err);
|
|
39
|
+
} finally {
|
|
40
|
+
_iterator.f();
|
|
41
|
+
}
|
|
42
|
+
scope = scope.upper;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -6,12 +6,20 @@ export declare const rules: {
|
|
|
6
6
|
'ensure-test-runner-nested-count': import("eslint").Rule.RuleModule;
|
|
7
7
|
'ensure-atlassian-team': import("eslint").Rule.RuleModule;
|
|
8
8
|
'ensure-critical-dependency-resolutions': import("eslint").Rule.RuleModule;
|
|
9
|
+
'ensure-valid-emotion-css-prop': import("eslint").Rule.RuleModule;
|
|
9
10
|
'no-duplicate-dependencies': import("eslint").Rule.RuleModule;
|
|
10
11
|
'no-invalid-feature-flag-usage': import("eslint").Rule.RuleModule;
|
|
11
12
|
'no-pre-post-install-scripts': import("eslint").Rule.RuleModule;
|
|
12
13
|
'no-invalid-storybook-decorator-usage': import("eslint").Rule.RuleModule;
|
|
13
14
|
'ensure-publish-valid': import("eslint").Rule.RuleModule;
|
|
14
15
|
'ensure-native-and-af-exports-synced': import("eslint").Rule.RuleModule;
|
|
16
|
+
'no-module-level-eval': import("eslint").Rule.RuleModule;
|
|
17
|
+
'static-feature-flags': import("eslint").Rule.RuleModule;
|
|
18
|
+
'no-preconditioning': import("eslint").Rule.RuleModule;
|
|
19
|
+
'inline-usage': import("eslint").Rule.RuleModule;
|
|
20
|
+
'prefer-fg': import("eslint").Rule.RuleModule;
|
|
21
|
+
'no-alias': import("eslint").Rule.RuleModule;
|
|
22
|
+
'use-recommended-utils': import("eslint").Rule.RuleModule;
|
|
15
23
|
};
|
|
16
24
|
export declare const configs: {
|
|
17
25
|
recommended: {
|
|
@@ -26,9 +34,16 @@ export declare const configs: {
|
|
|
26
34
|
'@atlaskit/platform/no-invalid-feature-flag-usage': string;
|
|
27
35
|
'@atlaskit/platform/no-invalid-storybook-decorator-usage': string;
|
|
28
36
|
'@atlaskit/platform/ensure-atlassian-team': string;
|
|
37
|
+
'@atlaskit/platform/no-module-level-eval': string;
|
|
38
|
+
'@atlaskit/platform/static-feature-flags': string;
|
|
39
|
+
'@atlaskit/platform/no-preconditioning': string;
|
|
40
|
+
'@atlaskit/platform/inline-usage': string;
|
|
41
|
+
'@atlaskit/platform/prefer-fg': string;
|
|
42
|
+
'@atlaskit/platform/no-alias': string;
|
|
29
43
|
};
|
|
30
44
|
};
|
|
31
45
|
};
|
|
32
46
|
export declare const processors: {
|
|
33
47
|
'package-json-processor': Linter.Processor<string | Linter.ProcessorFile>;
|
|
48
|
+
'package-json-processor-for-flat-config': Linter.Processor<string | Linter.ProcessorFile>;
|
|
34
49
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Rule, Scope } from 'eslint';
|
|
2
|
+
export declare function isIdentifierImportedFrom(identifierName: string, sources: Set<string>, context: Rule.RuleContext): boolean;
|
|
3
|
+
export declare function isAPIimport(functionName: string, context: Rule.RuleContext): boolean;
|
|
4
|
+
export declare function getDef(name: string, context: Rule.RuleContext): Scope.Definition | null | undefined;
|
|
5
|
+
export type Node<T extends Rule.Node['type']> = Extract<Rule.Node, {
|
|
6
|
+
type: T;
|
|
7
|
+
}>;
|
|
@@ -6,12 +6,20 @@ export declare const rules: {
|
|
|
6
6
|
'ensure-test-runner-nested-count': import("eslint").Rule.RuleModule;
|
|
7
7
|
'ensure-atlassian-team': import("eslint").Rule.RuleModule;
|
|
8
8
|
'ensure-critical-dependency-resolutions': import("eslint").Rule.RuleModule;
|
|
9
|
+
'ensure-valid-emotion-css-prop': import("eslint").Rule.RuleModule;
|
|
9
10
|
'no-duplicate-dependencies': import("eslint").Rule.RuleModule;
|
|
10
11
|
'no-invalid-feature-flag-usage': import("eslint").Rule.RuleModule;
|
|
11
12
|
'no-pre-post-install-scripts': import("eslint").Rule.RuleModule;
|
|
12
13
|
'no-invalid-storybook-decorator-usage': import("eslint").Rule.RuleModule;
|
|
13
14
|
'ensure-publish-valid': import("eslint").Rule.RuleModule;
|
|
14
15
|
'ensure-native-and-af-exports-synced': import("eslint").Rule.RuleModule;
|
|
16
|
+
'no-module-level-eval': import("eslint").Rule.RuleModule;
|
|
17
|
+
'static-feature-flags': import("eslint").Rule.RuleModule;
|
|
18
|
+
'no-preconditioning': import("eslint").Rule.RuleModule;
|
|
19
|
+
'inline-usage': import("eslint").Rule.RuleModule;
|
|
20
|
+
'prefer-fg': import("eslint").Rule.RuleModule;
|
|
21
|
+
'no-alias': import("eslint").Rule.RuleModule;
|
|
22
|
+
'use-recommended-utils': import("eslint").Rule.RuleModule;
|
|
15
23
|
};
|
|
16
24
|
export declare const configs: {
|
|
17
25
|
recommended: {
|
|
@@ -26,9 +34,16 @@ export declare const configs: {
|
|
|
26
34
|
'@atlaskit/platform/no-invalid-feature-flag-usage': string;
|
|
27
35
|
'@atlaskit/platform/no-invalid-storybook-decorator-usage': string;
|
|
28
36
|
'@atlaskit/platform/ensure-atlassian-team': string;
|
|
37
|
+
'@atlaskit/platform/no-module-level-eval': string;
|
|
38
|
+
'@atlaskit/platform/static-feature-flags': string;
|
|
39
|
+
'@atlaskit/platform/no-preconditioning': string;
|
|
40
|
+
'@atlaskit/platform/inline-usage': string;
|
|
41
|
+
'@atlaskit/platform/prefer-fg': string;
|
|
42
|
+
'@atlaskit/platform/no-alias': string;
|
|
29
43
|
};
|
|
30
44
|
};
|
|
31
45
|
};
|
|
32
46
|
export declare const processors: {
|
|
33
47
|
'package-json-processor': Linter.Processor<string | Linter.ProcessorFile>;
|
|
48
|
+
'package-json-processor-for-flat-config': Linter.Processor<string | Linter.ProcessorFile>;
|
|
34
49
|
};
|