Skip to content

Commit fa36d49

Browse files
Xunnamiusljharb
authored andcommitted
[New] extensions: add `pathGroupOverrides to allow enforcement decision overrides based on specifier
1 parent 74c9763 commit fa36d49

File tree

3 files changed

+169
-3
lines changed

3 files changed

+169
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
99
### Added
1010
- add [`enforce-node-protocol-usage`] rule and `import/node-version` setting ([#3024], thanks [@GoldStrikeArch] and [@sevenc-nanashi])
1111
- add TypeScript types ([#3097], thanks [@G-Rath])
12+
- [`extensions`]: add `pathGroupOverrides to allow enforcement decision overrides based on specifier ([#3105], thanks [@Xunnamius])
1213

1314
### Fixed
1415
- [`no-unused-modules`]: provide more meaningful error message when no .eslintrc is present ([#3116], thanks [@michaelfaith])
@@ -1170,6 +1171,7 @@ for info on changes for earlier releases.
11701171
[#3122]: https://github.com/import-js/eslint-plugin-import/pull/3122
11711172
[#3116]: https://github.com/import-js/eslint-plugin-import/pull/3116
11721173
[#3106]: https://github.com/import-js/eslint-plugin-import/pull/3106
1174+
[#3105]: https://github.com/import-js/eslint-plugin-import/pull/3105
11731175
[#3097]: https://github.com/import-js/eslint-plugin-import/pull/3097
11741176
[#3073]: https://github.com/import-js/eslint-plugin-import/pull/3073
11751177
[#3072]: https://github.com/import-js/eslint-plugin-import/pull/3072

src/rules/extensions.js

Lines changed: 47 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,39 @@ 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') {
193+
return;
194+
}
195+
152196
// don't enforce anything on builtins
153-
if (isBuiltIn(importPathWithQueryString, context.settings)) { return; }
197+
if (!overrideAction && isBuiltIn(importPathWithQueryString, context.settings)) { return; }
154198

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

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

161205
const resolvedPath = resolve(importPath, context);
162206

@@ -174,7 +218,7 @@ module.exports = {
174218
if (!extension || !importPath.endsWith(`.${extension}`)) {
175219
// ignore type-only imports and exports
176220
if (!props.checkTypeImports && (node.importKind === 'type' || node.exportKind === 'type')) { return; }
177-
const extensionRequired = isUseOfExtensionRequired(extension, isPackage);
221+
const extensionRequired = isUseOfExtensionRequired(extension, !overrideAction && isPackage);
178222
const extensionForbidden = isUseOfExtensionForbidden(extension);
179223
if (extensionRequired && !extensionForbidden) {
180224
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