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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy