-
Notifications
You must be signed in to change notification settings - Fork 9.4k
/
eslint-local-rules.cjs
106 lines (91 loc) · 3.36 KB
/
eslint-local-rules.cjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
'use strict';
/**
* @fileoverview A collection of eslint rules written specifically for
* Lighthouse. These are included by the eslint-plugin-local-rules plugin.
*/
const path = require('path');
/** @typedef {import('eslint').Rule.RuleModule} RuleModule */
/**
* Use `require.resolve()` to resolve the location of `path` from a location of
* `baseDir` and return it. Returns null if unable to resolve a path.
* @param {string} path
* @param {string} baseDir
* @return {string|null}
*/
function requireResolveOrNull(path, baseDir) {
try {
return require.resolve(path, {
paths: [baseDir],
});
} catch (err) {
return null;
}
}
/**
* An eslint rule ensuring that any require() of a local path (aka not a core
* module or a module dependency) includes a file extension (.js' or '.json').
* @type {RuleModule}
*/
const requireFileExtension = {
meta: {
docs: {
description: 'disallow require() without a file extension',
category: 'Best Practices',
recommended: false,
},
schema: [],
fixable: 'code',
},
create(context) {
return {
CallExpression(node) {
// Only look at instances of `require(moduleName: string)`.
if (node.type !== 'CallExpression') return;
if (node.callee.type !== 'Identifier' || node.callee.name !== 'require') return;
if (!node.arguments.length) return;
const arg0 = node.arguments[0];
if (arg0.type !== 'Literal' || typeof arg0.value !== 'string') return;
const requiredPath = arg0.value;
// If it's not a local file, we don't care.
if (!requiredPath.startsWith('.')) return;
// Check that `requiredPath` is resolvable from the source file.
const contextDirname = path.dirname(context.getFilename());
const resolvedRequiredPath = requireResolveOrNull(requiredPath, contextDirname);
if (!resolvedRequiredPath) {
return context.report({
node: node,
message: `Cannot resolve module '${requiredPath}'.`,
});
}
// If it has a file extension, it's good to go.
if (requiredPath.endsWith('.js')) return;
if (requiredPath.endsWith('.json')) return;
context.report({
node: node,
message: 'Local require path must have a file extension.',
fix(fixer) {
// Find the correct file extension/filename ending of the requiredPath.
let fixedPath = path.relative(contextDirname, resolvedRequiredPath);
if (!fixedPath.startsWith('.')) fixedPath = `./${fixedPath}`;
// Usually `fixedPath.startsWith(requiredPath)` and this will just add
// a suffix to the existing path, but sometimes humans write confusing
// paths, e.g. './core/lib/../lib/lh-error.js'. To cover both
// cases, double check that the paths resolve to the same file.
const resolvedFixedPath = requireResolveOrNull(fixedPath, contextDirname);
// If somehow they don't point to the same file, don't try to fix.
if (resolvedFixedPath !== resolvedRequiredPath) return null;
return fixer.replaceText(arg0, `'${fixedPath}'`);
},
});
},
};
},
};
module.exports = {
'require-file-extension': requireFileExtension,
};