@agilebot/eslint-plugin 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ };
@@ -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
+ };