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 all 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
41 changes: 41 additions & 0 deletions docs/rules/prefer-import-meta-properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Prefer `import.meta.{dirname,filename}` over legacy techniques for getting file paths

🚫 This rule is _disabled_ in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config).

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->

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.

> `import.meta.filename` is the same as the `url.fileURLToPath()` of the `import.meta.url`.\
> `import.meta.dirname` is the same as the `path.dirname()` of the `import.meta.filename`.

This rule replaces legacy patterns with `import.meta.{dirname,filename}`.

## Examples

```js
import path from 'node:path';
import {fileURLToPath} from "node:url";

// ❌
const filename = fileURLToPath(import.meta.url);

// ✅
const filename = import.meta.filename;
```

```js
import path from 'node:path';
import {fileURLToPath} from 'node:url';

// ❌
const dirname = path.dirname(fileURLToPath(import.meta.url));
const dirname = path.dirname(import.meta.filename);
const dirname = fileURLToPath(new URL('.', import.meta.url));

// ✅
const dirname = import.meta.dirname;
```
1 change: 1 addition & 0 deletions eslint.dogfooding.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const config = [
],
rules: {
'unicorn/prefer-module': 'off',
'unicorn/prefer-import-meta-properties': 'off', // We can enable this rule when we drop support for Node.js v18.
},
},
];
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export default [
| [prefer-event-target](docs/rules/prefer-event-target.md) | Prefer `EventTarget` over `EventEmitter`. | ✅ | | |
| [prefer-export-from](docs/rules/prefer-export-from.md) | Prefer `export…from` when re-exporting. | ✅ | 🔧 | 💡 |
| [prefer-global-this](docs/rules/prefer-global-this.md) | Prefer `globalThis` over `window`, `self`, and `global`. | ✅ | 🔧 | |
| [prefer-import-meta-properties](docs/rules/prefer-import-meta-properties.md) | Prefer `import.meta.{dirname,filename}` over legacy techniques for getting file paths. | | 🔧 | |
| [prefer-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()`, `.lastIndexOf()`, and `Array#some()` when checking for existence or non-existence. | ✅ | 🔧 | 💡 |
| [prefer-json-parse-buffer](docs/rules/prefer-json-parse-buffer.md) | Prefer reading a JSON file as a buffer. | | 🔧 | |
| [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. | ✅ | 🔧 | |
Expand Down
2 changes: 2 additions & 0 deletions rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import preferDomNodeTextContent from './prefer-dom-node-text-content.js';
import preferEventTarget from './prefer-event-target.js';
import preferExportFrom from './prefer-export-from.js';
import preferGlobalThis from './prefer-global-this.js';
import preferImportMetaProperties from './prefer-import-meta-properties.js';
import preferIncludes from './prefer-includes.js';
import preferJsonParseBuffer from './prefer-json-parse-buffer.js';
import preferKeyboardEventKey from './prefer-keyboard-event-key.js';
Expand Down Expand Up @@ -215,6 +216,7 @@ const rules = {
'prefer-event-target': createRule(preferEventTarget, 'prefer-event-target'),
'prefer-export-from': createRule(preferExportFrom, 'prefer-export-from'),
'prefer-global-this': createRule(preferGlobalThis, 'prefer-global-this'),
'prefer-import-meta-properties': createRule(preferImportMetaProperties, 'prefer-import-meta-properties'),
'prefer-includes': createRule(preferIncludes, 'prefer-includes'),
'prefer-json-parse-buffer': createRule(preferJsonParseBuffer, 'prefer-json-parse-buffer'),
'prefer-keyboard-event-key': createRule(preferKeyboardEventKey, 'prefer-keyboard-event-key'),
Expand Down
320 changes: 320 additions & 0 deletions rules/prefer-import-meta-properties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
import {findVariable} from '@eslint-community/eslint-utils';
import {
isMemberExpression,
isCallExpression,
isNewExpression,
isMethodCall,
} from './ast/index.js';

const ERROR_DIRNAME = 'error/calculate-dirname';
const ERROR_FILENAME = 'error/calculate-filename';
const messages = {
[ERROR_DIRNAME]: 'Do not construct dirname.',
[ERROR_FILENAME]: 'Do not construct filename using `fileURLToPath()`.',
};

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';

function isNodeBuiltinModuleFunctionCall(node, {modules, functionName, sourceCode}) {
if (!isCallExpression(node, {optional: false, argumentsLength: 1})) {
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'
&& isMemberExpression(node, {property: functionName, computed: false, optional: false})
)) {
return false;
}

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

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

// `process.getBuiltinModule('x')`
return (
isMethodCall(node, {
object: 'process',
method: 'getBuiltinModule',
argumentsLength: 1,
optionalMember: false,
optionalCall: false,
})
&& 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 === functionName);
}

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
|| parent.parent.type !== 'VariableDeclaration'
|| parent.parent.kind !== 'const'
) {
return false;
}

return checkExpression(parent.init, checkKind);
}

if (parent.type === 'Property') {
if (!(
checkKind === 'property'
&& parent.value === node
&& !parent.computed
&& parent.key.type === 'Identifier'
&& parent.key.name === functionName
)) {
return false;
}

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

return false;
}

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

/**
@returns {node is import('estree').SimpleCallExpression}
*/
function isUrlFileURLToPathCall(node, sourceCode) {
return isNodeBuiltinModuleFunctionCall(node, {
modules: new Set(['url', 'node:url']),
functionName: 'fileURLToPath',
sourceCode,
});
}

/**
@returns {node is import('estree').SimpleCallExpression}
*/
function isPathDirnameCall(node, sourceCode) {
return isNodeBuiltinModuleFunctionCall(node, {
modules: new Set(['path', 'node:path']),
functionName: 'dirname',
sourceCode,
});
}

function create(context) {
const {sourceCode} = context;

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

/** @type {import('estree').Node} */
const memberExpression = node.parent;
if (!isMemberExpression(memberExpression, {
properties: ['url', 'filename'],
computed: false,
optional: false,
})) {
return;
}

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

// `new URL(import.meta.url)`
// `new URL('.', import.meta.url)`
// `new URL('./', import.meta.url)`
if (isNewExpression(memberExpression.parent, {name: 'URL', minimumArguments: 1, maximumArguments: 2})) {
const newUrl = memberExpression.parent;
const urlParent = newUrl.parent;

// `new URL(import.meta.url)`
if (
newUrl.arguments.length === 1
&& newUrl.arguments[0] === memberExpression
// `url.fileURLToPath(new URL(import.meta.url))`
&& isUrlFileURLToPathCall(urlParent, sourceCode)
&& urlParent.arguments[0] === newUrl
) {
yield * iterateProblemsFromFilename(urlParent, {
reportFilenameNode: true,
});
return;
}

// `url.fileURLToPath(new URL(".", import.meta.url))`
// `url.fileURLToPath(new URL("./", import.meta.url))`
if (
newUrl.arguments.length === 2
&& isParentLiteral(newUrl.arguments[0])
&& newUrl.arguments[1] === memberExpression
&& isUrlFileURLToPathCall(urlParent, sourceCode)
&& urlParent.arguments[0] === newUrl
) {
yield getProblem(urlParent, 'dirname');
}
}

return;
}

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

/**
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;

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

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

if (
parent.type !== 'VariableDeclarator'
|| parent.init !== node
|| parent.id.type !== 'Identifier'
|| parent.parent.type !== 'VariableDeclaration'
|| parent.parent.kind !== 'const'
) {
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 (
isPathDirnameCall(parent, sourceCode)
&& parent.arguments[0] === reference.identifier
) {
// Report `path.dirname(identifier)`
yield getProblem(parent, 'dirname');
}
}
}

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

/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `import.meta.{dirname,filename}` over legacy techniques for getting file paths.',
recommended: false,
},
fixable: 'code',
messages,
},
};

export default config;
Loading
Loading