@agilebot/eslint-plugin 0.1.1
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/LICENSE +9 -0
- package/README.md +11 -0
- package/lib/index.js +52 -0
- package/lib/rules/import/enforce-icon-alias.js +43 -0
- package/lib/rules/import/monorepo.js +44 -0
- package/lib/rules/intl/id-missing.js +105 -0
- package/lib/rules/intl/id-prefix.js +97 -0
- package/lib/rules/intl/id-unused.js +117 -0
- package/lib/rules/intl/no-default.js +57 -0
- package/lib/rules/others/no-unnecessary-template-literals.js +38 -0
- package/lib/rules/react/better-exhaustive-deps.js +1935 -0
- package/lib/rules/react/hook-use-ref.js +37 -0
- package/lib/rules/react/no-inline-styles.js +87 -0
- package/lib/rules/react/prefer-named-property-access.js +105 -0
- package/lib/rules/tss/class-naming.js +43 -0
- package/lib/rules/tss/no-color-value.js +59 -0
- package/lib/rules/tss/unused-classes.js +108 -0
- package/lib/util/import.js +71 -0
- package/lib/util/intl.js +127 -0
- package/lib/util/settings.js +14 -0
- package/lib/util/translations.js +67 -0
- package/lib/util/tss.js +104 -0
- package/package.json +29 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
const Components = require('eslint-plugin-react/lib/util/Components');
|
2
|
+
|
3
|
+
module.exports = {
|
4
|
+
meta: {
|
5
|
+
docs: {
|
6
|
+
description: 'Ensure naming of useRef hook value.',
|
7
|
+
recommended: false
|
8
|
+
},
|
9
|
+
schema: [],
|
10
|
+
type: 'suggestion',
|
11
|
+
hasSuggestions: true
|
12
|
+
},
|
13
|
+
|
14
|
+
create: Components.detect((context, component, util) => {
|
15
|
+
return {
|
16
|
+
CallExpression(node) {
|
17
|
+
const isImmediateReturn =
|
18
|
+
node.parent && node.parent.type === 'ReturnStatement';
|
19
|
+
|
20
|
+
if (isImmediateReturn || !util.isReactHookCall(node, ['useRef'])) {
|
21
|
+
return;
|
22
|
+
}
|
23
|
+
if (node.parent.id.type !== 'Identifier') {
|
24
|
+
return;
|
25
|
+
}
|
26
|
+
const variable = node.parent.id.name;
|
27
|
+
|
28
|
+
if (!variable.endsWith('Ref')) {
|
29
|
+
context.report({
|
30
|
+
node: node,
|
31
|
+
message: 'useRef call is not end with "Ref"'
|
32
|
+
});
|
33
|
+
}
|
34
|
+
}
|
35
|
+
};
|
36
|
+
})
|
37
|
+
};
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module.exports = {
|
2
|
+
meta: {
|
3
|
+
docs: {
|
4
|
+
description: 'Disallow style props on components and DOM Nodes',
|
5
|
+
category: 'Best Practices',
|
6
|
+
recommended: false
|
7
|
+
},
|
8
|
+
messages: {
|
9
|
+
disallowInlineStyles:
|
10
|
+
'Avoid using inline styles, use sx prop or tss-react or styled-component instead'
|
11
|
+
},
|
12
|
+
schema: [
|
13
|
+
{
|
14
|
+
type: 'object',
|
15
|
+
properties: {
|
16
|
+
allowedFor: {
|
17
|
+
type: 'array',
|
18
|
+
uniqueItems: true,
|
19
|
+
items: { type: 'string' }
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
]
|
24
|
+
},
|
25
|
+
|
26
|
+
create(context) {
|
27
|
+
const configuration = context.options[0] || {};
|
28
|
+
const allowedFor = configuration.allowedFor || [];
|
29
|
+
|
30
|
+
function checkComponent(node) {
|
31
|
+
const parentName = node.parent.name;
|
32
|
+
// Extract a component name when using a "namespace", e.g. `<AntdLayout.Content />`.
|
33
|
+
const tag =
|
34
|
+
parentName.name ||
|
35
|
+
`${parentName.object.name}.${parentName.property.name}`;
|
36
|
+
const componentName = parentName.name || parentName.property.name;
|
37
|
+
if (
|
38
|
+
componentName &&
|
39
|
+
typeof componentName[0] === 'string' &&
|
40
|
+
componentName[0] !== componentName[0].toUpperCase()
|
41
|
+
) {
|
42
|
+
// This is a DOM node, not a Component, so exit.
|
43
|
+
return;
|
44
|
+
}
|
45
|
+
|
46
|
+
if (allowedFor.includes(tag)) {
|
47
|
+
return;
|
48
|
+
}
|
49
|
+
const prop = node.name.name;
|
50
|
+
|
51
|
+
if (prop === 'style') {
|
52
|
+
context.report({
|
53
|
+
node,
|
54
|
+
messageId: 'disallowInlineStyles'
|
55
|
+
});
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
function checkDOMNodes(node) {
|
60
|
+
const tag = node.parent.name.name;
|
61
|
+
if (
|
62
|
+
!(tag && typeof tag === 'string' && tag[0] !== tag[0].toUpperCase())
|
63
|
+
) {
|
64
|
+
// This is a Component, not a DOM node, so exit.
|
65
|
+
return;
|
66
|
+
}
|
67
|
+
if (allowedFor.includes(tag)) {
|
68
|
+
return;
|
69
|
+
}
|
70
|
+
const prop = node.name.name;
|
71
|
+
|
72
|
+
if (prop === 'style') {
|
73
|
+
context.report({
|
74
|
+
node,
|
75
|
+
messageId: 'disallowInlineStyles'
|
76
|
+
});
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
return {
|
81
|
+
JSXAttribute(node) {
|
82
|
+
checkComponent(node);
|
83
|
+
checkDOMNodes(node);
|
84
|
+
}
|
85
|
+
};
|
86
|
+
}
|
87
|
+
};
|
@@ -0,0 +1,105 @@
|
|
1
|
+
const { ESLintUtils } = require('@typescript-eslint/utils');
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Auto-fix util.
|
5
|
+
* Ensures that passed key is imported from 'react' package.
|
6
|
+
*/
|
7
|
+
function* updateImportStatement(context, fixer, key) {
|
8
|
+
const sourceCode = context.getSourceCode();
|
9
|
+
const importNode = sourceCode.ast.body.find(
|
10
|
+
node => node.type === 'ImportDeclaration' && node.source.value === 'react'
|
11
|
+
);
|
12
|
+
|
13
|
+
// No import from 'react' - create import statement
|
14
|
+
if (!importNode) {
|
15
|
+
yield fixer.insertTextBefore(
|
16
|
+
sourceCode.ast.body[0],
|
17
|
+
`import { ${key} } from 'react';\n`
|
18
|
+
);
|
19
|
+
|
20
|
+
return;
|
21
|
+
}
|
22
|
+
|
23
|
+
// Only default import from 'react' - add named imports section
|
24
|
+
if (
|
25
|
+
importNode.specifiers.length === 1 &&
|
26
|
+
importNode.specifiers[0].type === 'ImportDefaultSpecifier'
|
27
|
+
) {
|
28
|
+
yield fixer.insertTextAfter(importNode.specifiers[0], `, { ${key} }`);
|
29
|
+
|
30
|
+
return;
|
31
|
+
}
|
32
|
+
|
33
|
+
const alreadyImportedKeys = importNode.specifiers
|
34
|
+
.filter(specifier => specifier.type === 'ImportSpecifier')
|
35
|
+
.map(specifier => specifier.imported.name);
|
36
|
+
|
37
|
+
// Named imports section is present and current key is already imported - do nothing
|
38
|
+
if (alreadyImportedKeys.includes(key)) {
|
39
|
+
return;
|
40
|
+
}
|
41
|
+
|
42
|
+
// Named imports section is present and current key is not imported yet - add it to named imports section
|
43
|
+
yield fixer.insertTextAfter(importNode.specifiers.slice().pop(), `, ${key}`);
|
44
|
+
}
|
45
|
+
|
46
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
47
|
+
module.exports = ESLintUtils.RuleCreator.withoutDocs({
|
48
|
+
defaultOptions: [],
|
49
|
+
meta: {
|
50
|
+
type: 'layout',
|
51
|
+
fixable: 'code',
|
52
|
+
docs: {
|
53
|
+
description:
|
54
|
+
'Enforce importing each member of React namespace separately instead of accessing them through React namespace',
|
55
|
+
category: 'Layout & Formatting'
|
56
|
+
},
|
57
|
+
messages: {
|
58
|
+
illegalReactPropertyAccess:
|
59
|
+
'Illegal React property access: {{name}}. Use named import instead.'
|
60
|
+
}
|
61
|
+
},
|
62
|
+
|
63
|
+
create(context) {
|
64
|
+
return {
|
65
|
+
// Analyze TS types declarations
|
66
|
+
TSQualifiedName(node) {
|
67
|
+
// Do nothing to types that are ending with 'Event' as they will overlap with global event types otherwise
|
68
|
+
if (node.left.name !== 'React' || node.right.name.endsWith('Event')) {
|
69
|
+
return;
|
70
|
+
}
|
71
|
+
|
72
|
+
context.report({
|
73
|
+
node,
|
74
|
+
messageId: 'illegalReactPropertyAccess',
|
75
|
+
data: {
|
76
|
+
name: node.right.name
|
77
|
+
},
|
78
|
+
*fix(fixer) {
|
79
|
+
yield fixer.replaceText(node, node.right.name);
|
80
|
+
yield* updateImportStatement(context, fixer, node.right.name);
|
81
|
+
}
|
82
|
+
});
|
83
|
+
},
|
84
|
+
|
85
|
+
// Analyze expressions for React.* access
|
86
|
+
MemberExpression(node) {
|
87
|
+
if (node.object.name !== 'React') {
|
88
|
+
return;
|
89
|
+
}
|
90
|
+
|
91
|
+
context.report({
|
92
|
+
node,
|
93
|
+
messageId: 'illegalReactPropertyAccess',
|
94
|
+
data: {
|
95
|
+
name: node.property.name
|
96
|
+
},
|
97
|
+
*fix(fixer) {
|
98
|
+
yield fixer.replaceText(node, node.property.name);
|
99
|
+
yield* updateImportStatement(context, fixer, node.property.name);
|
100
|
+
}
|
101
|
+
});
|
102
|
+
}
|
103
|
+
};
|
104
|
+
}
|
105
|
+
});
|
@@ -0,0 +1,43 @@
|
|
1
|
+
const { getStyesObj, isCamelCase } = require('../../util/tss');
|
2
|
+
|
3
|
+
module.exports = {
|
4
|
+
meta: {
|
5
|
+
type: 'problem'
|
6
|
+
},
|
7
|
+
create: function rule(context) {
|
8
|
+
return {
|
9
|
+
CallExpression(node) {
|
10
|
+
const stylesObj = getStyesObj(node);
|
11
|
+
|
12
|
+
if (typeof stylesObj === 'undefined') {
|
13
|
+
return;
|
14
|
+
}
|
15
|
+
|
16
|
+
stylesObj.properties.forEach(property => {
|
17
|
+
if (property.computed) {
|
18
|
+
// Skip over computed properties for now.
|
19
|
+
// e.g. `{ [foo]: { ... } }`
|
20
|
+
return;
|
21
|
+
}
|
22
|
+
|
23
|
+
if (
|
24
|
+
property.type === 'ExperimentalSpreadProperty' ||
|
25
|
+
property.type === 'SpreadElement'
|
26
|
+
) {
|
27
|
+
// Skip over object spread for now.
|
28
|
+
// e.g. `{ ...foo }`
|
29
|
+
return;
|
30
|
+
}
|
31
|
+
|
32
|
+
const className = property.key.value || property.key.name;
|
33
|
+
if (!isCamelCase(className)) {
|
34
|
+
context.report(
|
35
|
+
property,
|
36
|
+
`Class \`${className}\` must be camelCase in makeStyles.`
|
37
|
+
);
|
38
|
+
}
|
39
|
+
});
|
40
|
+
}
|
41
|
+
};
|
42
|
+
}
|
43
|
+
};
|
@@ -0,0 +1,59 @@
|
|
1
|
+
const { getStyesObj } = require('../../util/tss');
|
2
|
+
|
3
|
+
module.exports = {
|
4
|
+
meta: {
|
5
|
+
type: 'problem',
|
6
|
+
docs: {
|
7
|
+
description:
|
8
|
+
'Enforce the use of color variables instead of color codes within makeStyles'
|
9
|
+
}
|
10
|
+
},
|
11
|
+
create: function (context) {
|
12
|
+
const parserOptions = context.parserOptions;
|
13
|
+
if (!parserOptions || !parserOptions.project) {
|
14
|
+
return {};
|
15
|
+
}
|
16
|
+
|
17
|
+
return {
|
18
|
+
CallExpression(node) {
|
19
|
+
const stylesObj = getStyesObj(node);
|
20
|
+
if (!stylesObj) {
|
21
|
+
return;
|
22
|
+
}
|
23
|
+
|
24
|
+
// Check for color codes inside the stylesObj
|
25
|
+
function checkColorLiteral(value) {
|
26
|
+
if (value.type === 'Literal' && typeof value.value === 'string') {
|
27
|
+
const colorCodePattern =
|
28
|
+
// eslint-disable-next-line max-len
|
29
|
+
/#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\?\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}(?:\s*,\s*\d*(?:\.\d+)?)?\s*\)/g;
|
30
|
+
const isColorCode = colorCodePattern.test(value.value);
|
31
|
+
if (isColorCode) {
|
32
|
+
context.report({
|
33
|
+
node: value,
|
34
|
+
message:
|
35
|
+
'Use color variables instead of color codes in makeStyles.'
|
36
|
+
});
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
function loopStylesObj(obj) {
|
42
|
+
if (obj && obj.type === 'ObjectExpression') {
|
43
|
+
obj.properties.forEach(property => {
|
44
|
+
if (property.type === 'Property' && property.value) {
|
45
|
+
if (property.value.type === 'ObjectExpression') {
|
46
|
+
loopStylesObj(property.value);
|
47
|
+
} else {
|
48
|
+
checkColorLiteral(property.value);
|
49
|
+
}
|
50
|
+
}
|
51
|
+
});
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
loopStylesObj(stylesObj);
|
56
|
+
}
|
57
|
+
};
|
58
|
+
}
|
59
|
+
};
|
@@ -0,0 +1,108 @@
|
|
1
|
+
const { getBasicIdentifier, getStyesObj } = require('../../util/tss');
|
2
|
+
|
3
|
+
module.exports = {
|
4
|
+
meta: {
|
5
|
+
type: 'problem'
|
6
|
+
},
|
7
|
+
create: function rule(context) {
|
8
|
+
const usedClasses = {};
|
9
|
+
const definedClasses = {};
|
10
|
+
|
11
|
+
return {
|
12
|
+
CallExpression(node) {
|
13
|
+
const stylesObj = getStyesObj(node);
|
14
|
+
|
15
|
+
if (typeof stylesObj === 'undefined') {
|
16
|
+
return;
|
17
|
+
}
|
18
|
+
|
19
|
+
stylesObj.properties.forEach(property => {
|
20
|
+
if (property.computed) {
|
21
|
+
// Skip over computed properties for now.
|
22
|
+
// e.g. `{ [foo]: { ... } }`
|
23
|
+
return;
|
24
|
+
}
|
25
|
+
|
26
|
+
if (
|
27
|
+
property.type === 'ExperimentalSpreadProperty' ||
|
28
|
+
property.type === 'SpreadElement'
|
29
|
+
) {
|
30
|
+
// Skip over object spread for now.
|
31
|
+
// e.g. `{ ...foo }`
|
32
|
+
return;
|
33
|
+
}
|
34
|
+
definedClasses[property.key.value || property.key.name] = property;
|
35
|
+
});
|
36
|
+
},
|
37
|
+
|
38
|
+
MemberExpression(node) {
|
39
|
+
if (
|
40
|
+
node.object.type === 'Identifier' &&
|
41
|
+
node.object.name === 'classes'
|
42
|
+
) {
|
43
|
+
const whichClass = getBasicIdentifier(node.property);
|
44
|
+
if (whichClass) {
|
45
|
+
usedClasses[whichClass] = true;
|
46
|
+
}
|
47
|
+
return;
|
48
|
+
}
|
49
|
+
|
50
|
+
const classIdentifier = getBasicIdentifier(node.property);
|
51
|
+
if (!classIdentifier) {
|
52
|
+
// props['foo' + bar].baz
|
53
|
+
return;
|
54
|
+
}
|
55
|
+
|
56
|
+
if (classIdentifier !== 'classes') {
|
57
|
+
// props.foo.bar
|
58
|
+
return;
|
59
|
+
}
|
60
|
+
|
61
|
+
const { parent } = node;
|
62
|
+
|
63
|
+
if (parent.type !== 'MemberExpression') {
|
64
|
+
// foo.styles
|
65
|
+
return;
|
66
|
+
}
|
67
|
+
|
68
|
+
if (
|
69
|
+
node.object.object &&
|
70
|
+
node.object.object.type !== 'ThisExpression'
|
71
|
+
) {
|
72
|
+
// foo.foo.styles
|
73
|
+
return;
|
74
|
+
}
|
75
|
+
|
76
|
+
const propsIdentifier = getBasicIdentifier(parent.object);
|
77
|
+
if (propsIdentifier && propsIdentifier !== 'props') {
|
78
|
+
return;
|
79
|
+
}
|
80
|
+
if (!propsIdentifier && parent.object.type !== 'MemberExpression') {
|
81
|
+
return;
|
82
|
+
}
|
83
|
+
|
84
|
+
if (parent.parent.type === 'MemberExpression') {
|
85
|
+
// this.props.props.styles
|
86
|
+
return;
|
87
|
+
}
|
88
|
+
|
89
|
+
const parentClassIdentifier = getBasicIdentifier(parent.property);
|
90
|
+
if (parentClassIdentifier) {
|
91
|
+
usedClasses[parentClassIdentifier] = true;
|
92
|
+
}
|
93
|
+
},
|
94
|
+
'Program:exit': () => {
|
95
|
+
// Now we know all of the defined classes and used classes, so we can
|
96
|
+
// see if there are any defined classes that are not used.
|
97
|
+
Object.keys(definedClasses).forEach(definedClassKey => {
|
98
|
+
if (!usedClasses[definedClassKey]) {
|
99
|
+
context.report(
|
100
|
+
definedClasses[definedClassKey],
|
101
|
+
`Class \`${definedClassKey}\` is unused`
|
102
|
+
);
|
103
|
+
}
|
104
|
+
});
|
105
|
+
}
|
106
|
+
};
|
107
|
+
}
|
108
|
+
};
|
@@ -0,0 +1,71 @@
|
|
1
|
+
const path = require('path');
|
2
|
+
const jiti = require('jiti');
|
3
|
+
const { transform } = require('sucrase');
|
4
|
+
const { getTsconfig } = require('get-tsconfig');
|
5
|
+
|
6
|
+
/**
|
7
|
+
* import ts module, like require, but support ts
|
8
|
+
* @param {*} modulePath - module path
|
9
|
+
*/
|
10
|
+
function tsImport(modulePath) {
|
11
|
+
if (!modulePath) {
|
12
|
+
return;
|
13
|
+
}
|
14
|
+
/** try to delete cache first */
|
15
|
+
try {
|
16
|
+
if (require.cache[modulePath]) {
|
17
|
+
delete require.cache[modulePath];
|
18
|
+
}
|
19
|
+
} catch (err) {
|
20
|
+
/* empty */
|
21
|
+
}
|
22
|
+
|
23
|
+
try {
|
24
|
+
return require(modulePath);
|
25
|
+
} catch (err) {
|
26
|
+
const tsconfig = getTsconfig(modulePath);
|
27
|
+
const { paths, baseUrl } = tsconfig.config.compilerOptions;
|
28
|
+
let basePath = path.dirname(tsconfig.path);
|
29
|
+
basePath = path.resolve(basePath, baseUrl);
|
30
|
+
|
31
|
+
const alias = resolveTsconfigPathsToAlias(paths, basePath);
|
32
|
+
|
33
|
+
return jiti(__filename, {
|
34
|
+
interopDefault: true,
|
35
|
+
cache: false,
|
36
|
+
debug: !!process.env.DEBUG,
|
37
|
+
transform: options => {
|
38
|
+
return transform(options.source, {
|
39
|
+
transforms: ['imports', 'typescript']
|
40
|
+
});
|
41
|
+
},
|
42
|
+
alias
|
43
|
+
})(modulePath);
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
/**
|
48
|
+
* Resolve tsconfig.json paths to Webpack aliases
|
49
|
+
* @param {string} paths - tsconfig.json paths
|
50
|
+
* @param {string} basePath - Path from tsconfig to Webpack config to create absolute aliases
|
51
|
+
* @return {object} - Webpack alias config
|
52
|
+
*/
|
53
|
+
function resolveTsconfigPathsToAlias(paths, basePath = __dirname) {
|
54
|
+
const aliases = {};
|
55
|
+
|
56
|
+
Object.keys(paths).forEach(item => {
|
57
|
+
const key = item.replace('/*', '');
|
58
|
+
const value = path.resolve(
|
59
|
+
basePath,
|
60
|
+
paths[item][0].replace('/*', '').replace('*', '')
|
61
|
+
);
|
62
|
+
|
63
|
+
aliases[key] = value;
|
64
|
+
});
|
65
|
+
|
66
|
+
return aliases;
|
67
|
+
}
|
68
|
+
|
69
|
+
module.exports = {
|
70
|
+
tsImport
|
71
|
+
};
|
package/lib/util/intl.js
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
/**
|
2
|
+
* Finds an attribute in formatMessage using attribute name.
|
3
|
+
*
|
4
|
+
* @param {Object} node - parent formatMessage node
|
5
|
+
* @param {string} attrName - attribute name.
|
6
|
+
* @returns {Object} node - returns node if it finds the attribute.
|
7
|
+
*/
|
8
|
+
function findFormatMessageAttrNode(node, attrName) {
|
9
|
+
// Find formatMessage usages
|
10
|
+
if (
|
11
|
+
node.type === 'CallExpression' &&
|
12
|
+
(node.callee.name === 'formatMessage' || node.callee.name === '$t')
|
13
|
+
) {
|
14
|
+
if (node.arguments.length && node.arguments[0].properties) {
|
15
|
+
return node.arguments[0].properties.find(
|
16
|
+
a => a.key && a.key.name === attrName
|
17
|
+
);
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
// Find intl.formatMessage usages
|
22
|
+
if (
|
23
|
+
node.type === 'CallExpression' &&
|
24
|
+
node.callee.type === 'MemberExpression' &&
|
25
|
+
(node.callee.object.name === 'intl' ||
|
26
|
+
(node.callee.object.name && node.callee.object.name.endsWith('Intl'))) &&
|
27
|
+
(node.callee.property.name === 'formatMessage' ||
|
28
|
+
node.callee.property.name === '$t')
|
29
|
+
) {
|
30
|
+
return node.arguments[0].properties.find(
|
31
|
+
a => a.key && a.key.name === attrName
|
32
|
+
);
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
/**
|
37
|
+
* Finds an attribute in FormattedMessage using attribute name.
|
38
|
+
*
|
39
|
+
* @param {Object} node - parent FormattedMessage node
|
40
|
+
* @param {string} attrName - attribute name.
|
41
|
+
* @returns {Object} node - returns node if it finds the attribute.
|
42
|
+
*/
|
43
|
+
function findFormattedMessageAttrNode(node, attrName) {
|
44
|
+
if (
|
45
|
+
node.type === 'JSXIdentifier' &&
|
46
|
+
node.name === 'FormattedMessage' &&
|
47
|
+
node.parent &&
|
48
|
+
node.parent.type === 'JSXOpeningElement'
|
49
|
+
) {
|
50
|
+
return node.parent.attributes.find(a => a.name && a.name.name === attrName);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
/**
|
55
|
+
* Finds an attribute in defineMessages using attribute name.
|
56
|
+
*
|
57
|
+
* @param {Object} node - parent defineMessages node
|
58
|
+
* @param {string} attrName - attribute name.
|
59
|
+
* @returns {Object} node - returns node if it finds the attribute.
|
60
|
+
*/
|
61
|
+
function findAttrNodeInDefineMessages(node, attrName) {
|
62
|
+
if (
|
63
|
+
node.type === 'Property' &&
|
64
|
+
node.key.name === attrName &&
|
65
|
+
node.parent &&
|
66
|
+
node.parent.parent &&
|
67
|
+
node.parent.parent.parent &&
|
68
|
+
node.parent.parent.parent.parent &&
|
69
|
+
node.parent.parent.parent.parent.type === 'CallExpression' &&
|
70
|
+
node.parent.parent.parent.parent.callee.name === 'defineMessages'
|
71
|
+
) {
|
72
|
+
return node;
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
/**
|
77
|
+
* Finds an attribute in defineMessages using attribute name.
|
78
|
+
*
|
79
|
+
* @param {Object} node - parent defineMessages node
|
80
|
+
* @param {string} attrName - attribute name.
|
81
|
+
* @returns {Object} node - returns node if it finds the attribute.
|
82
|
+
*/
|
83
|
+
function findAttrNodeInDefineMessage(node, attrName) {
|
84
|
+
if (
|
85
|
+
node.type === 'Property' &&
|
86
|
+
node.key.name === attrName &&
|
87
|
+
node.parent &&
|
88
|
+
node.parent.parent &&
|
89
|
+
node.parent.parent.type === 'CallExpression' &&
|
90
|
+
node.parent.parent.callee.name === 'defineMessage'
|
91
|
+
) {
|
92
|
+
return node;
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
/**
|
97
|
+
* Returns a sorted array of nodes, based on their starting posting in the locale id.
|
98
|
+
*
|
99
|
+
* @param {Object} node - parent node containing the locale id.
|
100
|
+
* @returns {Array} child nodes - sorted list.
|
101
|
+
*/
|
102
|
+
function sortedTemplateElements(node) {
|
103
|
+
return [...node.quasis, ...node.expressions].sort(
|
104
|
+
(a, b) => a.range[0] - b.range[0]
|
105
|
+
);
|
106
|
+
}
|
107
|
+
|
108
|
+
/**
|
109
|
+
* Replaces place holders with asterisk and returns the resulting id.
|
110
|
+
*
|
111
|
+
* @param {Object} node - parent node containing the locale id.
|
112
|
+
* @returns {string} id - fixed id.
|
113
|
+
*/
|
114
|
+
function templateLiteralDisplayStr(node) {
|
115
|
+
return sortedTemplateElements(node)
|
116
|
+
.map(e => (!e.value ? '*' : e.value.raw))
|
117
|
+
.join('');
|
118
|
+
}
|
119
|
+
|
120
|
+
module.exports = {
|
121
|
+
findFormatMessageAttrNode,
|
122
|
+
findFormattedMessageAttrNode,
|
123
|
+
findAttrNodeInDefineMessages,
|
124
|
+
findAttrNodeInDefineMessage,
|
125
|
+
sortedTemplateElements,
|
126
|
+
templateLiteralDisplayStr
|
127
|
+
};
|
@@ -0,0 +1,14 @@
|
|
1
|
+
/**
|
2
|
+
* Get a setting from eslint config
|
3
|
+
*
|
4
|
+
* @param {object} context - Context
|
5
|
+
* @param {string} name - Name
|
6
|
+
* @returns {any} result
|
7
|
+
*/
|
8
|
+
function getSetting(context, name) {
|
9
|
+
return context.settings[`agilebot/${name}`];
|
10
|
+
}
|
11
|
+
|
12
|
+
module.exports = {
|
13
|
+
getSetting
|
14
|
+
};
|