Skip to content

Commit 1c127b7

Browse files
committed
feat(import/extensions): allow enforcement decision overrides based on specifier
1 parent a20d843 commit 1c127b7

File tree

2 files changed

+165
-3
lines changed

2 files changed

+165
-3
lines changed

src/rules/extensions.js

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import path from 'path';
22

3+
import minimatch from 'minimatch';
34
import resolve from 'eslint-module-utils/resolve';
45
import { isBuiltIn, isExternalModule, isScoped } from '../core/importType';
56
import moduleVisitor from 'eslint-module-utils/moduleVisitor';
@@ -16,6 +17,26 @@ const properties = {
1617
pattern: patternProperties,
1718
checkTypeImports: { type: 'boolean' },
1819
ignorePackages: { type: 'boolean' },
20+
pathGroupOverrides: {
21+
type: 'array',
22+
items: {
23+
type: 'object',
24+
properties: {
25+
pattern: {
26+
type: 'string',
27+
},
28+
patternOptions: {
29+
type: 'object',
30+
},
31+
action: {
32+
type: 'string',
33+
enum: ['enforce', 'ignore'],
34+
},
35+
},
36+
additionalProperties: false,
37+
required: ['pattern', 'action'],
38+
},
39+
}
1940
},
2041
};
2142

@@ -54,6 +75,10 @@ function buildProperties(context) {
5475
if (obj.checkTypeImports !== undefined) {
5576
result.checkTypeImports = obj.checkTypeImports;
5677
}
78+
79+
if (obj.pathGroupOverrides !== undefined) {
80+
result.pathGroupOverrides = obj.pathGroupOverrides;
81+
}
5782
});
5883

5984
if (result.defaultConfig === 'ignorePackages') {
@@ -143,20 +168,37 @@ module.exports = {
143168
return false;
144169
}
145170

171+
function computeOverrideAction(pathGroupOverrides = [], path) {
172+
for (let i = 0, l = pathGroupOverrides.length; i < l; i++) {
173+
const { pattern, patternOptions, action } = pathGroupOverrides[i];
174+
if (minimatch(path, pattern, patternOptions || { nocomment: true })) {
175+
return action;
176+
}
177+
}
178+
}
179+
146180
function checkFileExtension(source, node) {
147181
// bail if the declaration doesn't have a source, e.g. "export { foo };", or if it's only partially typed like in an editor
148182
if (!source || !source.value) { return; }
149183

150184
const importPathWithQueryString = source.value;
151185

186+
// If not undefined, the user decided if rules are enforced on this import
187+
const overrideAction = computeOverrideAction(
188+
props.pathGroupOverrides,
189+
importPathWithQueryString
190+
);
191+
192+
if(overrideAction === 'ignore') { return ; }
193+
152194
// don't enforce anything on builtins
153-
if (isBuiltIn(importPathWithQueryString, context.settings)) { return; }
195+
if (!overrideAction && isBuiltIn(importPathWithQueryString, context.settings)) { return; }
154196

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

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

161203
const resolvedPath = resolve(importPath, context);
162204

@@ -174,7 +216,7 @@ module.exports = {
174216
if (!extension || !importPath.endsWith(`.${extension}`)) {
175217
// ignore type-only imports and exports
176218
if (!props.checkTypeImports && (node.importKind === 'type' || node.exportKind === 'type')) { return; }
177-
const extensionRequired = isUseOfExtensionRequired(extension, isPackage);
219+
const extensionRequired = isUseOfExtensionRequired(extension, !overrideAction && isPackage);
178220
const extensionForbidden = isUseOfExtensionForbidden(extension);
179221
if (extensionRequired && !extensionForbidden) {
180222
context.report({

tests/src/rules/extensions.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,86 @@ describe('TypeScript', () => {
736736
],
737737
parser,
738738
}),
739+
740+
// pathGroupOverrides: no patterns match good bespoke specifiers
741+
test({
742+
code: `
743+
import { ErrorMessage as UpstreamErrorMessage } from '@black-flag/core/util';
744+
745+
import { $instances } from 'rootverse+debug:src.ts';
746+
import { $exists } from 'rootverse+bfe:src/symbols.ts';
747+
748+
import type { Entries } from 'type-fest';
749+
`,
750+
parser,
751+
options: [
752+
'always',
753+
{
754+
ignorePackages: true,
755+
checkTypeImports: true,
756+
pathGroupOverrides: [
757+
{
758+
pattern: 'multiverse{*,*/**}',
759+
action: 'enforce'
760+
}
761+
]
762+
}
763+
]
764+
}),
765+
// pathGroupOverrides: an enforce pattern matches good bespoke specifiers
766+
test({
767+
code: `
768+
import { ErrorMessage as UpstreamErrorMessage } from '@black-flag/core/util';
769+
770+
import { $instances } from 'rootverse+debug:src.ts';
771+
import { $exists } from 'rootverse+bfe:src/symbols.ts';
772+
773+
import type { Entries } from 'type-fest';
774+
`,
775+
parser,
776+
options: [
777+
'always',
778+
{
779+
ignorePackages: true,
780+
checkTypeImports: true,
781+
pathGroupOverrides: [
782+
{
783+
pattern: 'rootverse{*,*/**}',
784+
action: 'enforce'
785+
},
786+
]
787+
}
788+
]
789+
}),
790+
// pathGroupOverrides: an ignore pattern matches bad bespoke specifiers
791+
test({
792+
code: `
793+
import { ErrorMessage as UpstreamErrorMessage } from '@black-flag/core/util';
794+
795+
import { $instances } from 'rootverse+debug:src';
796+
import { $exists } from 'rootverse+bfe:src/symbols';
797+
798+
import type { Entries } from 'type-fest';
799+
`,
800+
parser,
801+
options: [
802+
'always',
803+
{
804+
ignorePackages: true,
805+
checkTypeImports: true,
806+
pathGroupOverrides: [
807+
{
808+
pattern: 'multiverse{*,*/**}',
809+
action: 'enforce'
810+
},
811+
{
812+
pattern: 'rootverse{*,*/**}',
813+
action: 'ignore'
814+
},
815+
]
816+
}
817+
]
818+
}),
739819
],
740820
invalid: [
741821
test({
@@ -756,6 +836,46 @@ describe('TypeScript', () => {
756836
],
757837
parser,
758838
}),
839+
840+
// pathGroupOverrides: an enforce pattern matches bad bespoke specifiers
841+
test({
842+
code: `
843+
import { ErrorMessage as UpstreamErrorMessage } from '@black-flag/core/util';
844+
845+
import { $instances } from 'rootverse+debug:src';
846+
import { $exists } from 'rootverse+bfe:src/symbols';
847+
848+
import type { Entries } from 'type-fest';
849+
`,
850+
parser,
851+
options: [
852+
'always',
853+
{
854+
ignorePackages: true,
855+
checkTypeImports: true,
856+
pathGroupOverrides: [
857+
{
858+
pattern: 'rootverse{*,*/**}',
859+
action: 'enforce'
860+
},
861+
{
862+
pattern: 'universe{*,*/**}',
863+
action: 'ignore'
864+
}
865+
]
866+
}
867+
],
868+
errors: [
869+
{
870+
message: 'Missing file extension for "rootverse+debug:src"',
871+
line: 4,
872+
},
873+
{
874+
message: 'Missing file extension for "rootverse+bfe:src/symbols"',
875+
line: 5,
876+
}
877+
],
878+
}),
759879
],
760880
});
761881
});

0 commit comments

Comments
 (0)