Skip to content

Add prefer-import-meta-properties rule #2607

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
c2a6c45
`prefer-module`: Support `import.meta.{dirname,filename}`
ota-meshi Mar 26, 2025
0bc4a01
chore: fix linting errors
ota-meshi Mar 26, 2025
7107b20
chore: revert `test/unit/get-documentation-url.js`
ota-meshi Mar 26, 2025
9e1bff8
feat: check for filename vars
ota-meshi Mar 26, 2025
fd842aa
test: fix comment
ota-meshi Mar 26, 2025
93c7dac
docs: add `import.meta.{dirname,filename}`
ota-meshi Mar 26, 2025
e098150
fix: allow `new URL()` operations
ota-meshi Mar 26, 2025
d54ee2a
fix: support for `new URL('./', ...)` and add tests
ota-meshi Mar 26, 2025
e82b019
fix: update error messages
ota-meshi Mar 27, 2025
948e222
change to always auto-fix
ota-meshi Mar 27, 2025
c510e0c
split test cases
ota-meshi Mar 27, 2025
d7d508c
support `process.getBuiltinModule()`
ota-meshi Mar 27, 2025
859a275
use ast utils and some refactor
ota-meshi Mar 29, 2025
c600810
Fix style
fisker Mar 29, 2025
bfdbd2d
Ignore computed and optional
fisker Mar 29, 2025
c4dba43
Strict `Property` check
fisker Mar 29, 2025
d21168a
Code style
fisker Mar 29, 2025
f611d8a
Test other `MetaProperty`
fisker Mar 29, 2025
5df79f1
Stricter property access
fisker Mar 29, 2025
5852350
Minor tweak
fisker Mar 29, 2025
a7293cd
`CALC` -> `CALCULATE`
fisker Mar 29, 2025
e9f4a57
Stricter
fisker Mar 29, 2025
5847068
Stricter `new URL()` check
fisker Mar 29, 2025
d490f06
Stricter check `url.fileURLToPath` and `path.dirname`
fisker Mar 29, 2025
2908767
Function renaming
fisker Mar 29, 2025
0bb4574
Rename and comments
fisker Mar 29, 2025
b6a0b54
Revert rename, misunderstanding
fisker Mar 29, 2025
3ccba73
Remove misleading `targetNode`
fisker Mar 29, 2025
f97138d
Comment tweak
fisker Mar 29, 2025
7816928
Broken case
fisker Mar 29, 2025
c3bcf1c
False alarm
fisker Mar 29, 2025
e953258
One more test
fisker Mar 29, 2025
a2f4442
ignore URL.pathname
ota-meshi Mar 30, 2025
259c6f8
add `prefer-import-meta-properties`, and revert `prefer-module`
ota-meshi Mar 31, 2025
f9b359f
docs: fix docs
ota-meshi Mar 31, 2025
f790666
update eslint.dogfooding.config.js
ota-meshi Mar 31, 2025
50f05e8
Minor tweak
fisker Mar 31, 2025
701d27d
Update prefer-import-meta-properties.md
sindresorhus Mar 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/rules/prefer-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ Prefer using the [JavaScript module](https://developer.mozilla.org/en-US/docs/We

`export …` should be used in JavaScript modules.

1. Disallows `fileURLToPath(import.meta.url)` and similar operations.

Starting with Node.js 20.11, [`import.meta.dirname`](https://nodejs.org/api/esm.html#importmetadirname) and [`import.meta.filename`](https://nodejs.org/api/esm.html#importmetafilename) have been introduced in ES modules.

`fileURLToPath(import.meta.url)` can be replaced by `import.meta.filename`.

`path.dirname(import.meta.filename)` can be replaced by `import.meta.dirname`.

*`.cjs` files are ignored.*

## Fail
Expand Down
273 changes: 272 additions & 1 deletion rules/prefer-module.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import {findVariable, getPropertyName} from '@eslint-community/eslint-utils';
import isShadowed from './utils/is-shadowed.js';
import assertToken from './utils/assert-token.js';
import {getCallExpressionTokens} from './utils/index.js';
import {isStaticRequire, isReferenceIdentifier, isFunction} from './ast/index.js';
import {
isStaticRequire, isReferenceIdentifier, isFunction, isMemberExpression,
} from './ast/index.js';
import {removeParentheses, replaceReferenceIdentifier, removeSpacesAfter} from './fix/index.js';

const ERROR_USE_STRICT_DIRECTIVE = 'error/use-strict-directive';
const ERROR_GLOBAL_RETURN = 'error/global-return';
const ERROR_IDENTIFIER = 'error/identifier';
const ERROR_CALC_DIRNAME = 'error/calc-dirname';
const ERROR_CALC_FILENAME = 'error/calc-filename';
const SUGGESTION_USE_STRICT_DIRECTIVE = 'suggestion/use-strict-directive';
const SUGGESTION_IMPORT_META_DIRNAME = 'suggestion/import-meta-dirname';
const SUGGESTION_IMPORT_META_URL_TO_DIRNAME = 'suggestion/import-meta-url-to-dirname';
Expand All @@ -18,6 +23,8 @@ const messages = {
[ERROR_USE_STRICT_DIRECTIVE]: 'Do not use "use strict" directive.',
[ERROR_GLOBAL_RETURN]: '"return" should be used inside a function.',
[ERROR_IDENTIFIER]: 'Do not use "{{name}}".',
[ERROR_CALC_DIRNAME]: 'Do not construct dirname.',
[ERROR_CALC_FILENAME]: 'Do not construct filename using `fileURLToPath()`.',
[SUGGESTION_USE_STRICT_DIRECTIVE]: 'Remove "use strict" directive.',
[SUGGESTION_IMPORT_META_DIRNAME]: 'Replace `__dirname` with `import.meta.dirname`.',
[SUGGESTION_IMPORT_META_URL_TO_DIRNAME]: 'Replace `__dirname` with `…(import.meta.url)`.',
Expand Down Expand Up @@ -198,6 +205,138 @@ const isTopLevelReturnStatement = node => {
return true;
};

const isParentLiteral = node => {
if (node?.type !== 'Literal') {
return false;
}

return node.value === '.' || node.value === './';
};

const isImportMeta = node =>
node.type === 'MetaProperty'
&& node.meta.name === 'import'
&& node.property.name === 'meta';

/** @returns {node is import('estree').NewExpression} */
const isNewURL = node =>
node.type === 'NewExpression'
&& node.callee.type === 'Identifier'
&& node.callee.name === 'URL';

/** @returns {node is import('estree').MemberExpression} */
const isAccessPathname = node =>
node.type === 'MemberExpression'
&& getPropertyName(node) === 'pathname';

function isCallNodeBuiltinModule(node, propertyName, nodeModuleNames, sourceCode) {
if (node.type !== 'CallExpression') {
return false;
}

const visited = new Set();

return checkExpression(node.callee, 'property');

/** @param {import('estree').Expression} node */
function checkExpression(node, checkKind) {
if (node.type === 'MemberExpression') {
if (checkKind !== 'property' || getPropertyName(node) !== propertyName) {
return false;
}

return checkExpression(node.object, 'module');
}

if (node.type === 'CallExpression') {
if (checkKind !== 'module') {
return false;
}

// Check process.getBuiltinModule('x')
return (
isMemberExpression(node.callee, {property: 'getBuiltinModule', object: 'process'})
&& isModuleLiteral(node.arguments[0])
);
}

if (node.type !== 'Identifier') {
return false;
}

if (visited.has(node)) {
return false;
}

visited.add(node);

const variable = findVariable(sourceCode.getScope(node), node);
if (!variable || variable.defs.length !== 1) {
return;
}

return checkDefinition(variable.defs[0], checkKind);
}

/** @param {import('eslint').Scope.Definition} define */
function checkDefinition(define, checkKind) {
if (define.type === 'ImportBinding') {
if (!isModuleLiteral(define.parent.source)) {
return false;
}

const specifier = define.node;
return checkKind === 'module'
? (specifier?.type === 'ImportDefaultSpecifier' || specifier?.type === 'ImportNamespaceSpecifier')
: (specifier?.type === 'ImportSpecifier' && specifier.imported.name === propertyName);
}

return define.type === 'Variable' && checkPattern(define.name, checkKind);
}

/** @param {import('estree').Identifier | import('estree').ObjectPattern} node */
function checkPattern(node, checkKind) {
/** @type {{parent?: import('estree').Node}} */
const {parent} = node;
if (parent.type === 'VariableDeclarator') {
if (!parent.init || parent.id !== node) {
return false;
}

return checkExpression(parent.init, checkKind);
}

if (parent.type === 'Property') {
if (checkKind !== 'property' || parent.value !== node || getPropertyName(parent) !== propertyName) {
return false;
}

// Check for ObjectPattern
return checkPattern(parent.parent, 'module');
}

return false;
}

function isModuleLiteral(node) {
return node?.type === 'Literal' && nodeModuleNames.includes(node.value);
}
}

/**
@returns {node is import('estree').SimpleCallExpression}
*/
function isCallFileURLToPath(node, sourceCode) {
return isCallNodeBuiltinModule(node, 'fileURLToPath', ['url', 'node:url'], sourceCode);
}

/**
@returns {node is import('estree').SimpleCallExpression}
*/
function isCallPathDirname(node, sourceCode) {
return isCallNodeBuiltinModule(node, 'dirname', ['path', 'node:path'], sourceCode);
}

function fixDefaultExport(node, sourceCode) {
return function * (fixer) {
yield fixer.replaceText(node, 'export default ');
Expand Down Expand Up @@ -358,6 +497,138 @@ function create(context) {

return problem;
});

context.on('MetaProperty', function * (node) {
if (!isImportMeta(node)) {
return;
}

/** @type {{parent?: import('estree').Node}} */
const {parent} = node;
if (
parent.type !== 'MemberExpression'
|| parent.object !== node
) {
return;
}

/** @type {import('estree').Node} */
const targetNode = parent.parent;

const propertyName = getPropertyName(parent);
if (propertyName === 'url') {
if (
isCallFileURLToPath(targetNode, sourceCode)
&& targetNode.arguments[0] === parent
) {
// Report `fileURLToPath(import.meta.url)`
yield * iterateProblemsFromFilename(targetNode, {
reportFilenameNode: true,
});
return;
}

if (isNewURL(targetNode, sourceCode)) {
const urlParent = targetNode.parent;

if (targetNode.arguments[0] === parent) {
if (
isCallFileURLToPath(urlParent, sourceCode)
&& urlParent.arguments[0] === targetNode
) {
// Report `fileURLToPath(new URL(import.meta.url))`
yield * iterateProblemsFromFilename(urlParent, {
reportFilenameNode: true,
});
} else if (isAccessPathname(urlParent)) {
// Process for `new URL(import.meta.url).pathname`
yield * iterateProblemsFromFilename(urlParent);
}

return;
}

if (
isParentLiteral(targetNode.arguments[0])
&& targetNode.arguments[1] === parent
&& isCallFileURLToPath(urlParent, sourceCode)
&& urlParent.arguments[0] === targetNode
) {
// Report `fileURLToPath(new URL(".", import.meta.url))`
yield buildProblem(urlParent, 'dirname');
}
}

return;
}

if (propertyName === 'filename') {
yield * iterateProblemsFromFilename(parent);
}

/**
Iterates over reports where a given filename expression node
would be used to convert it to a dirname.
@param { import('estree').Expression} node
*/
function * iterateProblemsFromFilename(node, {reportFilenameNode = false} = {}) {
/** @type {{parent: import('estree').Node}} */
const {parent} = node;

if (
isCallPathDirname(parent, sourceCode)
&& parent.arguments[0] === node
) {
// Report `path.dirname(filename)`
yield buildProblem(parent, 'dirname');
return;
}

if (reportFilenameNode) {
yield buildProblem(node, 'filename');
}

if (parent.type !== 'VariableDeclarator' || parent.init !== node || parent.id.type !== 'Identifier') {
return;
}

/** @type {import('eslint').Scope.Variable|null} */
const variable = findVariable(sourceCode.getScope(parent.id), parent.id);
if (!variable) {
return;
}

for (const reference of variable.references) {
if (!reference.isReadOnly()) {
continue;
}

/** @type {{parent: import('estree').Node}} */
const {parent} = reference.identifier;
if (
isCallPathDirname(parent, sourceCode)
&& parent.arguments[0] === reference.identifier
) {
// Report `path.dirname(identifier)`
yield buildProblem(parent, 'dirname');
}
}
}

/**
@param { import('estree').Node} node
@param {'dirname' | 'filename'} name
*/
function buildProblem(node, name) {
return {
node,
messageId: name === 'dirname' ? ERROR_CALC_DIRNAME : ERROR_CALC_FILENAME,
data: {name},
fix: fixer =>
fixer.replaceText(node, `import.meta.${name}`),
};
}
});
}

/** @type {import('eslint').Rule.RuleModule} */
Expand Down
3 changes: 1 addition & 2 deletions scripts/create-rule.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import enquirer from 'enquirer';
import {template} from 'lodash-es';
import openEditor from 'open-editor';
import spawn from 'nano-spawn';

const dirname = path.dirname(fileURLToPath(import.meta.url));
const {dirname} = import.meta;
const ROOT = path.join(dirname, '..');

function checkFiles(ruleId) {
Expand Down
3 changes: 1 addition & 2 deletions scripts/internal-rules/no-restricted-property-access.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {isMemberExpression} from '../../rules/ast/index.js';
import {removeMemberExpressionProperty} from '../../rules/fix/index.js';

const messageId = path.basename(fileURLToPath(import.meta.url), '.js');
const messageId = path.basename(import.meta.filename, '.js');

const properties = new Map([
['range', 'sourceCode.getRange'],
Expand Down
3 changes: 1 addition & 2 deletions scripts/internal-rules/no-test-only.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import path from 'node:path';
import {fileURLToPath} from 'node:url';

const messageId = path.basename(fileURLToPath(import.meta.url), '.js');
const messageId = path.basename(import.meta.filename, '.js');

const config = {
create(context) {
Expand Down
3 changes: 1 addition & 2 deletions scripts/internal-rules/prefer-fixer-remove-range.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {isMethodCall, isLiteral} from '../../rules/ast/index.js';
import {removeArgument} from '../../rules/fix/index.js';

const messageId = path.basename(fileURLToPath(import.meta.url), '.js');
const messageId = path.basename(import.meta.filename, '.js');

const config = {
create(context) {
Expand Down
3 changes: 1 addition & 2 deletions scripts/internal-rules/prefer-negative-boolean-attribute.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import path from 'node:path';
import {fileURLToPath} from 'node:url';

const messageId = path.basename(fileURLToPath(import.meta.url), '.js');
const messageId = path.basename(import.meta.filename, '.js');

const shouldReport = (string, value) => {
const index = string.indexOf(`=${value}]`);
Expand Down
3 changes: 1 addition & 2 deletions test/integration/projects.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import path from 'node:path';
import {fileURLToPath} from 'node:url';

const dirname = path.dirname(fileURLToPath(import.meta.url));
const {dirname} = import.meta;

function normalizeProject(project) {
if (typeof project === 'string') {
Expand Down
Loading
Loading