Skip to content

Commit

Permalink
[New] `extensions: allow enforcement decision overrides based on spec…
Browse files Browse the repository at this point in the history
…ifier
  • Loading branch information
Xunnamius authored and ljharb committed Nov 18, 2024
1 parent f0727a6 commit b067170
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 3 deletions.
48 changes: 45 additions & 3 deletions src/rules/extensions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import path from 'path';

import minimatch from 'minimatch';
import resolve from 'eslint-module-utils/resolve';
import { isBuiltIn, isExternalModule, isScoped } from '../core/importType';
import moduleVisitor from 'eslint-module-utils/moduleVisitor';
Expand All @@ -16,6 +17,26 @@ const properties = {
pattern: patternProperties,
checkTypeImports: { type: 'boolean' },
ignorePackages: { type: 'boolean' },
pathGroupOverrides: {
type: 'array',
items: {
type: 'object',
properties: {
pattern: {
type: 'string',
},
patternOptions: {
type: 'object',
},
action: {
type: 'string',
enum: ['enforce', 'ignore'],
},
},
additionalProperties: false,
required: ['pattern', 'action'],
},
},
},
};

Expand Down Expand Up @@ -54,6 +75,10 @@ function buildProperties(context) {
if (obj.checkTypeImports !== undefined) {
result.checkTypeImports = obj.checkTypeImports;
}

if (obj.pathGroupOverrides !== undefined) {
result.pathGroupOverrides = obj.pathGroupOverrides;

Check warning on line 80 in src/rules/extensions.js

View check run for this annotation

Codecov / codecov/patch

src/rules/extensions.js#L80

Added line #L80 was not covered by tests
}
});

if (result.defaultConfig === 'ignorePackages') {
Expand Down Expand Up @@ -143,20 +168,37 @@ module.exports = {
return false;
}

function computeOverrideAction(path, pathGroupOverrides = []) {
for (let i = 0, l = pathGroupOverrides.length; i < l; i++) {
const { pattern, patternOptions, action } = pathGroupOverrides[i];
if (minimatch(path, pattern, patternOptions || { nocomment: true })) {
return action;

Check warning on line 175 in src/rules/extensions.js

View check run for this annotation

Codecov / codecov/patch

src/rules/extensions.js#L173-L175

Added lines #L173 - L175 were not covered by tests
}
}
}

function checkFileExtension(source, node) {
// bail if the declaration doesn't have a source, e.g. "export { foo };", or if it's only partially typed like in an editor
if (!source || !source.value) { return; }

const importPathWithQueryString = source.value;

// If not undefined, the user decided if rules are enforced on this import
const overrideAction = computeOverrideAction(
importPathWithQueryString,
props.pathGroupOverrides,
);

if (overrideAction === 'ignore') { return; }

// don't enforce anything on builtins
if (isBuiltIn(importPathWithQueryString, context.settings)) { return; }
if (!overrideAction && isBuiltIn(importPathWithQueryString, context.settings)) { return; }

const importPath = importPathWithQueryString.replace(/\?(.*)$/, '');

// don't enforce in root external packages as they may have names with `.js`.
// Like `import Decimal from decimal.js`)
if (isExternalRootModule(importPath)) { return; }
if (!overrideAction && isExternalRootModule(importPath)) { return; }

const resolvedPath = resolve(importPath, context);

Expand All @@ -174,7 +216,7 @@ module.exports = {
if (!extension || !importPath.endsWith(`.${extension}`)) {
// ignore type-only imports and exports
if (!props.checkTypeImports && (node.importKind === 'type' || node.exportKind === 'type')) { return; }
const extensionRequired = isUseOfExtensionRequired(extension, isPackage);
const extensionRequired = isUseOfExtensionRequired(extension, !overrideAction && isPackage);
const extensionForbidden = isUseOfExtensionForbidden(extension);
if (extensionRequired && !extensionForbidden) {
context.report({
Expand Down
120 changes: 120 additions & 0 deletions tests/src/rules/extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,86 @@ describe('TypeScript', () => {
],
parser,
}),

// pathGroupOverrides: no patterns match good bespoke specifiers
test({
code: `
import { ErrorMessage as UpstreamErrorMessage } from '@black-flag/core/util';
import { $instances } from 'rootverse+debug:src.ts';
import { $exists } from 'rootverse+bfe:src/symbols.ts';
import type { Entries } from 'type-fest';
`,
parser,
options: [
'always',
{
ignorePackages: true,
checkTypeImports: true,
pathGroupOverrides: [
{
pattern: 'multiverse{*,*/**}',
action: 'enforce'
}
]
}
]
}),
// pathGroupOverrides: an enforce pattern matches good bespoke specifiers
test({
code: `
import { ErrorMessage as UpstreamErrorMessage } from '@black-flag/core/util';
import { $instances } from 'rootverse+debug:src.ts';
import { $exists } from 'rootverse+bfe:src/symbols.ts';
import type { Entries } from 'type-fest';
`,
parser,
options: [
'always',
{
ignorePackages: true,
checkTypeImports: true,
pathGroupOverrides: [
{
pattern: 'rootverse{*,*/**}',
action: 'enforce'
},
]
}
]
}),
// pathGroupOverrides: an ignore pattern matches bad bespoke specifiers
test({
code: `
import { ErrorMessage as UpstreamErrorMessage } from '@black-flag/core/util';
import { $instances } from 'rootverse+debug:src';
import { $exists } from 'rootverse+bfe:src/symbols';
import type { Entries } from 'type-fest';
`,
parser,
options: [
'always',
{
ignorePackages: true,
checkTypeImports: true,
pathGroupOverrides: [
{
pattern: 'multiverse{*,*/**}',
action: 'enforce'
},
{
pattern: 'rootverse{*,*/**}',
action: 'ignore'
},
]
}
]
}),
],
invalid: [
test({
Expand All @@ -756,6 +836,46 @@ describe('TypeScript', () => {
],
parser,
}),

// pathGroupOverrides: an enforce pattern matches bad bespoke specifiers
test({
code: `
import { ErrorMessage as UpstreamErrorMessage } from '@black-flag/core/util';
import { $instances } from 'rootverse+debug:src';
import { $exists } from 'rootverse+bfe:src/symbols';
import type { Entries } from 'type-fest';
`,
parser,
options: [
'always',
{
ignorePackages: true,
checkTypeImports: true,
pathGroupOverrides: [
{
pattern: 'rootverse{*,*/**}',
action: 'enforce'
},
{
pattern: 'universe{*,*/**}',
action: 'ignore'
}
]
}
],
errors: [
{
message: 'Missing file extension for "rootverse+debug:src"',
line: 4,
},
{
message: 'Missing file extension for "rootverse+bfe:src/symbols"',
line: 5,
}
],
}),
],
});
});
Expand Down

0 comments on commit b067170

Please sign in to comment.