@atlaskit/eslint-plugin-platform 0.1.0 → 0.1.2

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @atlaskit/eslint-plugin-platform
2
2
 
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [`166815fbd8f`](https://bitbucket.org/atlassian/atlassian-frontend/commits/166815fbd8f) - Add recommended set of flags for use in products
8
+
9
+ ## 0.1.1
10
+
11
+ ### Patch Changes
12
+
13
+ - [`7edd9e8b4b1`](https://bitbucket.org/atlassian/atlassian-frontend/commits/7edd9e8b4b1) - Add suggestion to change feature flag to the closest matching feature flag using fuzzy search
14
+
3
15
  ## 0.1.0
4
16
 
5
17
  ### Minor Changes
package/dist/cjs/index.js CHANGED
@@ -19,6 +19,14 @@ var rules = {
19
19
  };
20
20
  exports.rules = rules;
21
21
  var configs = {
22
+ productRecommended: {
23
+ plugins: ['@atlaskit/platform'],
24
+ rules: {
25
+ '@atlaskit/platform/ensure-test-runner-arguments': 'error',
26
+ '@atlaskit/platform/ensure-test-runner-nested-count': 'warn',
27
+ '@atlaskit/platform/no-invalid-feature-flag-usage': 'error'
28
+ }
29
+ },
22
30
  recommended: {
23
31
  plugins: ['@atlaskit/platform'],
24
32
  rules: {
@@ -5,21 +5,29 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.default = void 0;
8
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
8
9
  var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
9
10
  var _readPkgUp = _interopRequireDefault(require("read-pkg-up"));
10
11
  var _path = _interopRequireDefault(require("path"));
12
+ var _fuse = _interopRequireDefault(require("fuse.js"));
13
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
14
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
15
+ // defines a "getter" to "type" map, if more types are required for feature flags (like string) add it here!
16
+ var getterIdentifierToFlagTypeMap = {
17
+ getBooleanFF: 'boolean'
18
+ };
11
19
  // make sure we cache reading the package.json so we don't end up reading it for every instance of this rule.
12
20
  var pkgJsonCache = new Map();
13
21
 
14
22
  // get the ancestor package.json for a given file
15
- var getPackageJsonForFileName = function getPackageJsonForFileName(filename) {
23
+ var getMetadataForFilename = function getMetadataForFilename(filename) {
16
24
  var splitFilename = filename.split(_path.default.sep);
17
25
  for (var i = 0; i < splitFilename.length; i++) {
18
26
  // attempt to search using the filename in the cache to see if we've read the package.json for a sibling file before
19
27
  var searchPath = _path.default.join.apply(_path.default, (0, _toConsumableArray2.default)(splitFilename.splice(0, i)));
20
- var cachedPkgJson = pkgJsonCache.get(searchPath);
21
- if (cachedPkgJson) {
22
- return cachedPkgJson;
28
+ var cachedMetaData = pkgJsonCache.get(searchPath);
29
+ if (cachedMetaData) {
30
+ return cachedMetaData;
23
31
  }
24
32
  }
25
33
  var _ref = _readPkgUp.default.sync({
@@ -28,29 +36,38 @@ var getPackageJsonForFileName = function getPackageJsonForFileName(filename) {
28
36
  }),
29
37
  packageJson = _ref.packageJson,
30
38
  pkgJsonPath = _ref.path;
31
- pkgJsonCache.set(pkgJsonPath, packageJson);
32
- return packageJson;
39
+ var pkgJson = packageJson;
40
+ var fuse = packageJson['platform-feature-flags'] == null ? null : new _fuse.default(Object.keys(pkgJson['platform-feature-flags']));
41
+ var metaData = {
42
+ pkgJson: pkgJson,
43
+ fuse: fuse
44
+ };
45
+ pkgJsonCache.set(pkgJsonPath, metaData);
46
+ return metaData;
33
47
  };
34
48
  var rule = {
35
49
  meta: {
36
- hasSuggestions: false,
37
50
  docs: {
38
51
  recommended: false
39
52
  },
40
53
  type: 'problem',
41
54
  messages: {
42
55
  registrationSectionMissing: 'Please add a "platform-feature-flags" section to your package.json! See http://go/pff-eslint for more details',
43
- featureFlagMissing: "Please add a \"{{ featureFlag }}\" section to the \"platform-feature-flags\" section in your package.json. See http://go/pff-eslint for more details"
44
- }
56
+ featureFlagMissing: "Please add a \"{{ featureFlag }}\" section to the \"platform-feature-flags\" section in your package.json. See http://go/pff-eslint for more details",
57
+ changeFeatureFlag: "Change flag key to \"{{ closestFlag }}\" already defined in package.json"
58
+ },
59
+ hasSuggestions: true
45
60
  },
46
61
  create: function create(context) {
47
- return {
48
- 'CallExpression[callee.name=/getBooleanFF/]': function CallExpressionCalleeNameGetBooleanFF(node) {
62
+ return Object.fromEntries(Object.keys(getterIdentifierToFlagTypeMap).map(function (getterIdentifier) {
63
+ return ["CallExpression[callee.name=/".concat(getterIdentifier, "/]"), function (node) {
49
64
  // to make typescript happy
50
65
  if (node.type === 'CallExpression') {
51
66
  var args = node.arguments;
52
67
  var filename = context.getFilename();
53
- var packageJson = getPackageJsonForFileName(filename);
68
+ var _getMetadataForFilena = getMetadataForFilename(filename),
69
+ packageJson = _getMetadataForFilena.pkgJson,
70
+ fuse = _getMetadataForFilena.fuse;
54
71
  var platformFeatureFlags = packageJson['platform-feature-flags'];
55
72
  if (!platformFeatureFlags) {
56
73
  return context.report({
@@ -62,19 +79,38 @@ var rule = {
62
79
  var featureFlag = args[0].value;
63
80
  var featureFlagRegistration = platformFeatureFlags[featureFlag];
64
81
  if (!featureFlagRegistration) {
65
- return context.report({
66
- node: node,
82
+ // find the closest match in existing section for suggestion text
83
+ var closestMatchFix = null;
84
+ if (fuse) {
85
+ var closestFlagMatches = fuse.search(featureFlag);
86
+ if (closestFlagMatches.length > 0) {
87
+ var closestFlag = closestFlagMatches[0].item;
88
+ closestMatchFix = {
89
+ messageId: 'changeFeatureFlag',
90
+ data: {
91
+ closestFlag: closestFlag
92
+ },
93
+ fix: function fix(fixer) {
94
+ return fixer.replaceText(node.arguments[0], "'".concat(closestFlag, "'"));
95
+ }
96
+ };
97
+ }
98
+ }
99
+ return context.report(_objectSpread({
100
+ node: args[0],
67
101
  messageId: 'featureFlagMissing',
68
102
  data: {
69
103
  featureFlag: featureFlag
70
104
  }
71
- });
105
+ }, closestMatchFix != null ? {
106
+ suggest: [closestMatchFix]
107
+ } : {}));
72
108
  }
73
109
  }
74
110
  }
75
111
  return {};
76
- }
77
- };
112
+ }];
113
+ }));
78
114
  }
79
115
  };
80
116
  var _default = rule;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/eslint-plugin-platform",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "sideEffects": false
5
5
  }
@@ -11,6 +11,14 @@ export const rules = {
11
11
  'no-pre-post-install-scripts': noPreAndPostInstallScripts
12
12
  };
13
13
  export const configs = {
14
+ productRecommended: {
15
+ plugins: ['@atlaskit/platform'],
16
+ rules: {
17
+ '@atlaskit/platform/ensure-test-runner-arguments': 'error',
18
+ '@atlaskit/platform/ensure-test-runner-nested-count': 'warn',
19
+ '@atlaskit/platform/no-invalid-feature-flag-usage': 'error'
20
+ }
21
+ },
14
22
  recommended: {
15
23
  plugins: ['@atlaskit/platform'],
16
24
  rules: {
@@ -1,17 +1,23 @@
1
1
  import readPkgUp from 'read-pkg-up';
2
2
  import path from 'path';
3
+ import Fuse from 'fuse.js';
4
+
5
+ // defines a "getter" to "type" map, if more types are required for feature flags (like string) add it here!
6
+ const getterIdentifierToFlagTypeMap = {
7
+ getBooleanFF: 'boolean'
8
+ };
3
9
  // make sure we cache reading the package.json so we don't end up reading it for every instance of this rule.
4
10
  const pkgJsonCache = new Map();
5
11
 
6
12
  // get the ancestor package.json for a given file
7
- const getPackageJsonForFileName = filename => {
13
+ const getMetadataForFilename = filename => {
8
14
  const splitFilename = filename.split(path.sep);
9
15
  for (let i = 0; i < splitFilename.length; i++) {
10
16
  // attempt to search using the filename in the cache to see if we've read the package.json for a sibling file before
11
17
  const searchPath = path.join(...splitFilename.splice(0, i));
12
- const cachedPkgJson = pkgJsonCache.get(searchPath);
13
- if (cachedPkgJson) {
14
- return cachedPkgJson;
18
+ const cachedMetaData = pkgJsonCache.get(searchPath);
19
+ if (cachedMetaData) {
20
+ return cachedMetaData;
15
21
  }
16
22
  }
17
23
  const {
@@ -21,53 +27,82 @@ const getPackageJsonForFileName = filename => {
21
27
  cwd: filename,
22
28
  normalize: false
23
29
  });
24
- pkgJsonCache.set(pkgJsonPath, packageJson);
25
- return packageJson;
30
+ const pkgJson = packageJson;
31
+ const fuse = packageJson['platform-feature-flags'] == null ? null : new Fuse(Object.keys(pkgJson['platform-feature-flags']));
32
+ const metaData = {
33
+ pkgJson,
34
+ fuse
35
+ };
36
+ pkgJsonCache.set(pkgJsonPath, metaData);
37
+ return metaData;
26
38
  };
27
39
  const rule = {
28
40
  meta: {
29
- hasSuggestions: false,
30
41
  docs: {
31
42
  recommended: false
32
43
  },
33
44
  type: 'problem',
34
45
  messages: {
35
46
  registrationSectionMissing: 'Please add a "platform-feature-flags" section to your package.json! See http://go/pff-eslint for more details',
36
- featureFlagMissing: `Please add a "{{ featureFlag }}" section to the "platform-feature-flags" section in your package.json. See http://go/pff-eslint for more details`
37
- }
47
+ featureFlagMissing: `Please add a "{{ featureFlag }}" section to the "platform-feature-flags" section in your package.json. See http://go/pff-eslint for more details`,
48
+ changeFeatureFlag: `Change flag key to "{{ closestFlag }}" already defined in package.json`
49
+ },
50
+ hasSuggestions: true
38
51
  },
39
52
  create(context) {
40
- return {
41
- 'CallExpression[callee.name=/getBooleanFF/]': node => {
42
- // to make typescript happy
43
- if (node.type === 'CallExpression') {
44
- const args = node.arguments;
45
- const filename = context.getFilename();
46
- const packageJson = getPackageJsonForFileName(filename);
47
- const platformFeatureFlags = packageJson['platform-feature-flags'];
48
- if (!platformFeatureFlags) {
53
+ return Object.fromEntries(Object.keys(getterIdentifierToFlagTypeMap).map(getterIdentifier => [`CallExpression[callee.name=/${getterIdentifier}/]`, node => {
54
+ // to make typescript happy
55
+ if (node.type === 'CallExpression') {
56
+ const args = node.arguments;
57
+ const filename = context.getFilename();
58
+ const {
59
+ pkgJson: packageJson,
60
+ fuse
61
+ } = getMetadataForFilename(filename);
62
+ const platformFeatureFlags = packageJson['platform-feature-flags'];
63
+ if (!platformFeatureFlags) {
64
+ return context.report({
65
+ node: node,
66
+ messageId: 'registrationSectionMissing'
67
+ });
68
+ }
69
+ if (args.length === 1 && args[0].type === 'Literal' && args[0].raw) {
70
+ const featureFlag = args[0].value;
71
+ const featureFlagRegistration = platformFeatureFlags[featureFlag];
72
+ if (!featureFlagRegistration) {
73
+ // find the closest match in existing section for suggestion text
74
+ let closestMatchFix = null;
75
+ if (fuse) {
76
+ const closestFlagMatches = fuse.search(featureFlag);
77
+ if (closestFlagMatches.length > 0) {
78
+ const closestFlag = closestFlagMatches[0].item;
79
+ closestMatchFix = {
80
+ messageId: 'changeFeatureFlag',
81
+ data: {
82
+ closestFlag
83
+ },
84
+ fix: fixer => {
85
+ return fixer.replaceText(node.arguments[0], `'${closestFlag}'`);
86
+ }
87
+ };
88
+ }
89
+ }
49
90
  return context.report({
50
- node,
51
- messageId: 'registrationSectionMissing'
91
+ node: args[0],
92
+ messageId: 'featureFlagMissing',
93
+ data: {
94
+ featureFlag
95
+ },
96
+ // only suggest if we have a close flag to match
97
+ ...(closestMatchFix != null ? {
98
+ suggest: [closestMatchFix]
99
+ } : {})
52
100
  });
53
101
  }
54
- if (args.length === 1 && args[0].type === 'Literal' && args[0].raw) {
55
- const featureFlag = args[0].value;
56
- const featureFlagRegistration = platformFeatureFlags[featureFlag];
57
- if (!featureFlagRegistration) {
58
- return context.report({
59
- node,
60
- messageId: 'featureFlagMissing',
61
- data: {
62
- featureFlag
63
- }
64
- });
65
- }
66
- }
67
102
  }
68
- return {};
69
103
  }
70
- };
104
+ return {};
105
+ }]));
71
106
  }
72
107
  };
73
108
  export default rule;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/eslint-plugin-platform",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "sideEffects": false
5
5
  }
package/dist/esm/index.js CHANGED
@@ -11,6 +11,14 @@ export var rules = {
11
11
  'no-pre-post-install-scripts': noPreAndPostInstallScripts
12
12
  };
13
13
  export var configs = {
14
+ productRecommended: {
15
+ plugins: ['@atlaskit/platform'],
16
+ rules: {
17
+ '@atlaskit/platform/ensure-test-runner-arguments': 'error',
18
+ '@atlaskit/platform/ensure-test-runner-nested-count': 'warn',
19
+ '@atlaskit/platform/no-invalid-feature-flag-usage': 'error'
20
+ }
21
+ },
14
22
  recommended: {
15
23
  plugins: ['@atlaskit/platform'],
16
24
  rules: {
@@ -1,18 +1,27 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
1
2
  import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
3
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
4
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
2
5
  import readPkgUp from 'read-pkg-up';
3
6
  import path from 'path';
7
+ import Fuse from 'fuse.js';
8
+
9
+ // defines a "getter" to "type" map, if more types are required for feature flags (like string) add it here!
10
+ var getterIdentifierToFlagTypeMap = {
11
+ getBooleanFF: 'boolean'
12
+ };
4
13
  // make sure we cache reading the package.json so we don't end up reading it for every instance of this rule.
5
14
  var pkgJsonCache = new Map();
6
15
 
7
16
  // get the ancestor package.json for a given file
8
- var getPackageJsonForFileName = function getPackageJsonForFileName(filename) {
17
+ var getMetadataForFilename = function getMetadataForFilename(filename) {
9
18
  var splitFilename = filename.split(path.sep);
10
19
  for (var i = 0; i < splitFilename.length; i++) {
11
20
  // attempt to search using the filename in the cache to see if we've read the package.json for a sibling file before
12
21
  var searchPath = path.join.apply(path, _toConsumableArray(splitFilename.splice(0, i)));
13
- var cachedPkgJson = pkgJsonCache.get(searchPath);
14
- if (cachedPkgJson) {
15
- return cachedPkgJson;
22
+ var cachedMetaData = pkgJsonCache.get(searchPath);
23
+ if (cachedMetaData) {
24
+ return cachedMetaData;
16
25
  }
17
26
  }
18
27
  var _ref = readPkgUp.sync({
@@ -21,29 +30,38 @@ var getPackageJsonForFileName = function getPackageJsonForFileName(filename) {
21
30
  }),
22
31
  packageJson = _ref.packageJson,
23
32
  pkgJsonPath = _ref.path;
24
- pkgJsonCache.set(pkgJsonPath, packageJson);
25
- return packageJson;
33
+ var pkgJson = packageJson;
34
+ var fuse = packageJson['platform-feature-flags'] == null ? null : new Fuse(Object.keys(pkgJson['platform-feature-flags']));
35
+ var metaData = {
36
+ pkgJson: pkgJson,
37
+ fuse: fuse
38
+ };
39
+ pkgJsonCache.set(pkgJsonPath, metaData);
40
+ return metaData;
26
41
  };
27
42
  var rule = {
28
43
  meta: {
29
- hasSuggestions: false,
30
44
  docs: {
31
45
  recommended: false
32
46
  },
33
47
  type: 'problem',
34
48
  messages: {
35
49
  registrationSectionMissing: 'Please add a "platform-feature-flags" section to your package.json! See http://go/pff-eslint for more details',
36
- featureFlagMissing: "Please add a \"{{ featureFlag }}\" section to the \"platform-feature-flags\" section in your package.json. See http://go/pff-eslint for more details"
37
- }
50
+ featureFlagMissing: "Please add a \"{{ featureFlag }}\" section to the \"platform-feature-flags\" section in your package.json. See http://go/pff-eslint for more details",
51
+ changeFeatureFlag: "Change flag key to \"{{ closestFlag }}\" already defined in package.json"
52
+ },
53
+ hasSuggestions: true
38
54
  },
39
55
  create: function create(context) {
40
- return {
41
- 'CallExpression[callee.name=/getBooleanFF/]': function CallExpressionCalleeNameGetBooleanFF(node) {
56
+ return Object.fromEntries(Object.keys(getterIdentifierToFlagTypeMap).map(function (getterIdentifier) {
57
+ return ["CallExpression[callee.name=/".concat(getterIdentifier, "/]"), function (node) {
42
58
  // to make typescript happy
43
59
  if (node.type === 'CallExpression') {
44
60
  var args = node.arguments;
45
61
  var filename = context.getFilename();
46
- var packageJson = getPackageJsonForFileName(filename);
62
+ var _getMetadataForFilena = getMetadataForFilename(filename),
63
+ packageJson = _getMetadataForFilena.pkgJson,
64
+ fuse = _getMetadataForFilena.fuse;
47
65
  var platformFeatureFlags = packageJson['platform-feature-flags'];
48
66
  if (!platformFeatureFlags) {
49
67
  return context.report({
@@ -55,19 +73,38 @@ var rule = {
55
73
  var featureFlag = args[0].value;
56
74
  var featureFlagRegistration = platformFeatureFlags[featureFlag];
57
75
  if (!featureFlagRegistration) {
58
- return context.report({
59
- node: node,
76
+ // find the closest match in existing section for suggestion text
77
+ var closestMatchFix = null;
78
+ if (fuse) {
79
+ var closestFlagMatches = fuse.search(featureFlag);
80
+ if (closestFlagMatches.length > 0) {
81
+ var closestFlag = closestFlagMatches[0].item;
82
+ closestMatchFix = {
83
+ messageId: 'changeFeatureFlag',
84
+ data: {
85
+ closestFlag: closestFlag
86
+ },
87
+ fix: function fix(fixer) {
88
+ return fixer.replaceText(node.arguments[0], "'".concat(closestFlag, "'"));
89
+ }
90
+ };
91
+ }
92
+ }
93
+ return context.report(_objectSpread({
94
+ node: args[0],
60
95
  messageId: 'featureFlagMissing',
61
96
  data: {
62
97
  featureFlag: featureFlag
63
98
  }
64
- });
99
+ }, closestMatchFix != null ? {
100
+ suggest: [closestMatchFix]
101
+ } : {}));
65
102
  }
66
103
  }
67
104
  }
68
105
  return {};
69
- }
70
- };
106
+ }];
107
+ }));
71
108
  }
72
109
  };
73
110
  export default rule;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/eslint-plugin-platform",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "sideEffects": false
5
5
  }
@@ -7,6 +7,14 @@ export declare const rules: {
7
7
  'no-pre-post-install-scripts': import("eslint").Rule.RuleModule;
8
8
  };
9
9
  export declare const configs: {
10
+ productRecommended: {
11
+ plugins: string[];
12
+ rules: {
13
+ '@atlaskit/platform/ensure-test-runner-arguments': string;
14
+ '@atlaskit/platform/ensure-test-runner-nested-count': string;
15
+ '@atlaskit/platform/no-invalid-feature-flag-usage': string;
16
+ };
17
+ };
10
18
  recommended: {
11
19
  plugins: string[];
12
20
  rules: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@atlaskit/eslint-plugin-platform",
3
3
  "description": "The essential plugin for use with Atlassian frontend platform tools",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "atlassian": {
7
7
  "team": "UIP - Platform Integration Trust (PITa)",
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@babel/runtime": "^7.0.0",
26
+ "fuse.js": "^6.6.2",
26
27
  "read-pkg-up": "^7.0.1"
27
28
  },
28
29
  "devDependencies": {
package/report.api.md CHANGED
@@ -20,6 +20,14 @@ import { Rule } from 'eslint';
20
20
 
21
21
  // @public (undocumented)
22
22
  export const configs: {
23
+ productRecommended: {
24
+ plugins: string[];
25
+ rules: {
26
+ '@atlaskit/platform/ensure-test-runner-arguments': string;
27
+ '@atlaskit/platform/ensure-test-runner-nested-count': string;
28
+ '@atlaskit/platform/no-invalid-feature-flag-usage': string;
29
+ };
30
+ };
23
31
  recommended: {
24
32
  plugins: string[];
25
33
  rules: {
@@ -16,6 +16,7 @@ import { RuleTester } from 'eslint';
16
16
  method();
17
17
  });
18
18
  };
19
+
19
20
  export const tester = new RuleTester({
20
21
  parser: require.resolve('babel-eslint'),
21
22
  parserOptions: {
package/src/index.tsx CHANGED
@@ -14,6 +14,14 @@ export const rules = {
14
14
  };
15
15
 
16
16
  export const configs = {
17
+ productRecommended: {
18
+ plugins: ['@atlaskit/platform'],
19
+ rules: {
20
+ '@atlaskit/platform/ensure-test-runner-arguments': 'error',
21
+ '@atlaskit/platform/ensure-test-runner-nested-count': 'warn',
22
+ '@atlaskit/platform/no-invalid-feature-flag-usage': 'error',
23
+ },
24
+ },
17
25
  recommended: {
18
26
  plugins: ['@atlaskit/platform'],
19
27
  rules: {
@@ -31,6 +31,7 @@ describe('with existing platform-feature-flags section', () => {
31
31
  };
32
32
  });
33
33
 
34
+ // this isolates the invalid case so we can test the suggestion properly
34
35
  tester.run('ensure-feature-flag-registration', rule, {
35
36
  valid: [
36
37
  {
@@ -40,7 +41,20 @@ describe('with existing platform-feature-flags section', () => {
40
41
  invalid: [
41
42
  {
42
43
  code: `getBooleanFF('invalid-flag')`,
43
- errors: [{ messageId: 'featureFlagMissing' }],
44
+ errors: [
45
+ {
46
+ messageId: 'featureFlagMissing',
47
+ suggestions: [
48
+ {
49
+ messageId: 'changeFeatureFlag',
50
+ data: {
51
+ closestFlag: 'test-flag',
52
+ },
53
+ output: `getBooleanFF('test-flag')`,
54
+ },
55
+ ],
56
+ },
57
+ ],
44
58
  },
45
59
  ],
46
60
  });
@@ -1,26 +1,42 @@
1
1
  import type { Rule } from 'eslint';
2
2
  import readPkgUp from 'read-pkg-up';
3
3
  import path from 'path';
4
+ import Fuse from 'fuse.js';
5
+
6
+ // defines a "getter" to "type" map, if more types are required for feature flags (like string) add it here!
7
+ const getterIdentifierToFlagTypeMap = {
8
+ getBooleanFF: 'boolean' as const,
9
+ } as const;
4
10
 
5
11
  type PlatformFeatureFlagRegistrationSection = {
6
12
  [key: string]: {
7
- type: 'boolean';
13
+ // get the values of the object above
14
+ type: typeof getterIdentifierToFlagTypeMap[keyof typeof getterIdentifierToFlagTypeMap];
8
15
  };
9
16
  };
10
17
 
18
+ type EnhancedPackageJson = readPkgUp.PackageJson & {
19
+ 'platform-feature-flags'?: PlatformFeatureFlagRegistrationSection;
20
+ };
21
+
22
+ type PkgJsonMetaData = {
23
+ pkgJson: EnhancedPackageJson;
24
+ fuse: Fuse<string> | null;
25
+ };
26
+
11
27
  // make sure we cache reading the package.json so we don't end up reading it for every instance of this rule.
12
- const pkgJsonCache = new Map<string, readPkgUp.PackageJson>();
28
+ const pkgJsonCache = new Map<string, PkgJsonMetaData>();
13
29
 
14
30
  // get the ancestor package.json for a given file
15
- const getPackageJsonForFileName = (filename: string): readPkgUp.PackageJson => {
31
+ const getMetadataForFilename = (filename: string): PkgJsonMetaData => {
16
32
  const splitFilename = filename.split(path.sep);
17
33
  for (let i = 0; i < splitFilename.length; i++) {
18
34
  // attempt to search using the filename in the cache to see if we've read the package.json for a sibling file before
19
35
  const searchPath = path.join(...splitFilename.splice(0, i));
20
- const cachedPkgJson = pkgJsonCache.get(searchPath);
36
+ const cachedMetaData = pkgJsonCache.get(searchPath);
21
37
 
22
- if (cachedPkgJson) {
23
- return cachedPkgJson;
38
+ if (cachedMetaData) {
39
+ return cachedMetaData;
24
40
  }
25
41
  }
26
42
 
@@ -29,13 +45,21 @@ const getPackageJsonForFileName = (filename: string): readPkgUp.PackageJson => {
29
45
  normalize: false,
30
46
  })!;
31
47
 
32
- pkgJsonCache.set(pkgJsonPath, packageJson);
33
- return packageJson;
48
+ const pkgJson = packageJson as EnhancedPackageJson;
49
+
50
+ const fuse =
51
+ packageJson['platform-feature-flags'] == null
52
+ ? null
53
+ : new Fuse(Object.keys(pkgJson['platform-feature-flags']!));
54
+
55
+ const metaData = { pkgJson, fuse };
56
+
57
+ pkgJsonCache.set(pkgJsonPath, metaData);
58
+ return metaData;
34
59
  };
35
60
 
36
61
  const rule: Rule.RuleModule = {
37
62
  meta: {
38
- hasSuggestions: false,
39
63
  docs: {
40
64
  recommended: false,
41
65
  },
@@ -44,47 +68,88 @@ const rule: Rule.RuleModule = {
44
68
  registrationSectionMissing:
45
69
  'Please add a "platform-feature-flags" section to your package.json! See http://go/pff-eslint for more details',
46
70
  featureFlagMissing: `Please add a "{{ featureFlag }}" section to the "platform-feature-flags" section in your package.json. See http://go/pff-eslint for more details`,
71
+ changeFeatureFlag: `Change flag key to "{{ closestFlag }}" already defined in package.json`,
47
72
  },
73
+
74
+ hasSuggestions: true,
48
75
  },
49
76
  create(context) {
50
- return {
51
- 'CallExpression[callee.name=/getBooleanFF/]': (node: Rule.Node) => {
52
- // to make typescript happy
53
- if (node.type === 'CallExpression') {
54
- const args = node.arguments;
55
-
56
- const filename = context.getFilename();
57
- const packageJson = getPackageJsonForFileName(filename);
58
- const platformFeatureFlags = packageJson[
59
- 'platform-feature-flags'
60
- ] as PlatformFeatureFlagRegistrationSection;
61
-
62
- if (!platformFeatureFlags) {
63
- return context.report({
64
- node,
65
- messageId: 'registrationSectionMissing',
66
- });
67
- }
77
+ return Object.fromEntries(
78
+ (
79
+ Object.keys(
80
+ getterIdentifierToFlagTypeMap,
81
+ ) as (keyof typeof getterIdentifierToFlagTypeMap)[]
82
+ ).map((getterIdentifier) => [
83
+ `CallExpression[callee.name=/${getterIdentifier}/]`,
84
+ (node: Rule.Node) => {
85
+ // to make typescript happy
86
+ if (node.type === 'CallExpression') {
87
+ const args = node.arguments;
68
88
 
69
- if (args.length === 1 && args[0].type === 'Literal' && args[0].raw) {
70
- const featureFlag = args[0].value as string;
71
- const featureFlagRegistration = platformFeatureFlags[featureFlag];
89
+ const filename = context.getFilename();
90
+ const { pkgJson: packageJson, fuse } =
91
+ getMetadataForFilename(filename);
92
+ const platformFeatureFlags = packageJson['platform-feature-flags'];
72
93
 
73
- if (!featureFlagRegistration) {
94
+ if (!platformFeatureFlags) {
74
95
  return context.report({
75
- node,
76
- messageId: 'featureFlagMissing',
77
- data: {
78
- featureFlag,
79
- },
96
+ node: node,
97
+ messageId: 'registrationSectionMissing',
80
98
  });
81
99
  }
100
+
101
+ if (
102
+ args.length === 1 &&
103
+ args[0].type === 'Literal' &&
104
+ args[0].raw
105
+ ) {
106
+ const featureFlag = args[0].value as string;
107
+ const featureFlagRegistration = platformFeatureFlags[featureFlag];
108
+
109
+ if (!featureFlagRegistration) {
110
+ // find the closest match in existing section for suggestion text
111
+ let closestMatchFix: Rule.SuggestionReportDescriptor | null =
112
+ null;
113
+
114
+ if (fuse) {
115
+ const closestFlagMatches = fuse.search(featureFlag);
116
+ if (closestFlagMatches.length > 0) {
117
+ const closestFlag = closestFlagMatches[0].item;
118
+
119
+ closestMatchFix = {
120
+ messageId: 'changeFeatureFlag',
121
+ data: {
122
+ closestFlag,
123
+ },
124
+ fix: (fixer) => {
125
+ return fixer.replaceText(
126
+ node.arguments[0],
127
+ `'${closestFlag}'`,
128
+ );
129
+ },
130
+ };
131
+ }
132
+ }
133
+
134
+ return context.report({
135
+ node: args[0],
136
+ messageId: 'featureFlagMissing',
137
+ data: {
138
+ featureFlag,
139
+ },
140
+ // only suggest if we have a close flag to match
141
+ ...(closestMatchFix != null
142
+ ? { suggest: [closestMatchFix] }
143
+ : {}),
144
+ });
145
+ }
146
+ }
82
147
  }
83
- }
84
148
 
85
- return {};
86
- },
87
- };
149
+ return {};
150
+ },
151
+ ]),
152
+ );
88
153
  },
89
154
  };
90
155
 
@@ -9,6 +9,14 @@ import { Rule } from 'eslint';
9
9
 
10
10
  // @public (undocumented)
11
11
  export const configs: {
12
+ productRecommended: {
13
+ plugins: string[];
14
+ rules: {
15
+ '@atlaskit/platform/ensure-test-runner-arguments': string;
16
+ '@atlaskit/platform/ensure-test-runner-nested-count': string;
17
+ '@atlaskit/platform/no-invalid-feature-flag-usage': string;
18
+ };
19
+ };
12
20
  recommended: {
13
21
  plugins: string[];
14
22
  rules: {