2024-08-09 12:04:48 +00:00
/ * *
* @ fileoverview Restrict usage of specified node imports .
* @ author Guy Ellis
* /
"use strict" ;
2024-08-21 06:34:30 +00:00
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require ( "./utils/ast-utils" ) ;
2024-08-09 12:04:48 +00:00
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const ignore = require ( "ignore" ) ;
const arrayOfStringsOrObjects = {
type : "array" ,
items : {
anyOf : [
{ type : "string" } ,
{
type : "object" ,
properties : {
name : { type : "string" } ,
message : {
type : "string" ,
minLength : 1
} ,
importNames : {
type : "array" ,
items : {
type : "string"
}
}
} ,
additionalProperties : false ,
required : [ "name" ]
}
]
} ,
uniqueItems : true
} ;
const arrayOfStringsOrObjectPatterns = {
anyOf : [
{
type : "array" ,
items : {
type : "string"
} ,
uniqueItems : true
} ,
{
type : "array" ,
items : {
type : "object" ,
properties : {
2024-08-21 06:34:30 +00:00
importNames : {
type : "array" ,
items : {
type : "string"
} ,
minItems : 1 ,
uniqueItems : true
} ,
2024-08-09 12:04:48 +00:00
group : {
type : "array" ,
items : {
type : "string"
} ,
minItems : 1 ,
uniqueItems : true
} ,
2024-08-21 06:34:30 +00:00
importNamePattern : {
type : "string"
} ,
2024-08-09 12:04:48 +00:00
message : {
type : "string" ,
minLength : 1
2024-08-21 06:34:30 +00:00
} ,
caseSensitive : {
type : "boolean"
2024-08-09 12:04:48 +00:00
}
} ,
additionalProperties : false ,
required : [ "group" ]
} ,
uniqueItems : true
}
]
} ;
2024-08-21 06:34:30 +00:00
/** @type {import('../shared/types').Rule} */
2024-08-09 12:04:48 +00:00
module . exports = {
meta : {
type : "suggestion" ,
docs : {
2024-08-21 06:34:30 +00:00
description : "Disallow specified modules when loaded by `import`" ,
2024-08-09 12:04:48 +00:00
recommended : false ,
2024-08-21 06:34:30 +00:00
url : "https://eslint.org/docs/latest/rules/no-restricted-imports"
2024-08-09 12:04:48 +00:00
} ,
messages : {
path : "'{{importSource}}' import is restricted from being used." ,
2024-08-21 06:34:30 +00:00
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
2024-08-09 12:04:48 +00:00
pathWithCustomMessage : "'{{importSource}}' import is restricted from being used. {{customMessage}}" ,
patterns : "'{{importSource}}' import is restricted from being used by a pattern." ,
2024-08-21 06:34:30 +00:00
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
2024-08-09 12:04:48 +00:00
patternWithCustomMessage : "'{{importSource}}' import is restricted from being used by a pattern. {{customMessage}}" ,
2024-08-21 06:34:30 +00:00
patternAndImportName : "'{{importName}}' import from '{{importSource}}' is restricted from being used by a pattern." ,
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
patternAndImportNameWithCustomMessage : "'{{importName}}' import from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}" ,
patternAndEverything : "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern." ,
patternAndEverythingWithRegexImportName : "* import is invalid because import name matching '{{importNames}}' pattern from '{{importSource}}' is restricted from being used." ,
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
patternAndEverythingWithCustomMessage : "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}" ,
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
patternAndEverythingWithRegexImportNameAndCustomMessage : "* import is invalid because import name matching '{{importNames}}' pattern from '{{importSource}}' is restricted from being used. {{customMessage}}" ,
2024-08-09 12:04:48 +00:00
everything : "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted." ,
2024-08-21 06:34:30 +00:00
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
2024-08-09 12:04:48 +00:00
everythingWithCustomMessage : "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted. {{customMessage}}" ,
importName : "'{{importName}}' import from '{{importSource}}' is restricted." ,
2024-08-21 06:34:30 +00:00
// eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
2024-08-09 12:04:48 +00:00
importNameWithCustomMessage : "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}"
} ,
schema : {
anyOf : [
arrayOfStringsOrObjects ,
{
type : "array" ,
items : [ {
type : "object" ,
properties : {
paths : arrayOfStringsOrObjects ,
patterns : arrayOfStringsOrObjectPatterns
} ,
additionalProperties : false
} ] ,
additionalItems : false
}
]
}
} ,
create ( context ) {
2024-08-21 06:34:30 +00:00
const sourceCode = context . sourceCode ;
2024-08-09 12:04:48 +00:00
const options = Array . isArray ( context . options ) ? context . options : [ ] ;
const isPathAndPatternsObject =
typeof options [ 0 ] === "object" &&
( Object . prototype . hasOwnProperty . call ( options [ 0 ] , "paths" ) || Object . prototype . hasOwnProperty . call ( options [ 0 ] , "patterns" ) ) ;
const restrictedPaths = ( isPathAndPatternsObject ? options [ 0 ] . paths : context . options ) || [ ] ;
const restrictedPathMessages = restrictedPaths . reduce ( ( memo , importSource ) => {
if ( typeof importSource === "string" ) {
memo [ importSource ] = { message : null } ;
} else {
memo [ importSource . name ] = {
message : importSource . message ,
importNames : importSource . importNames
} ;
}
return memo ;
} , { } ) ;
// Handle patterns too, either as strings or groups
2024-08-21 06:34:30 +00:00
let restrictedPatterns = ( isPathAndPatternsObject ? options [ 0 ] . patterns : [ ] ) || [ ] ;
// standardize to array of objects if we have an array of strings
if ( restrictedPatterns . length > 0 && typeof restrictedPatterns [ 0 ] === "string" ) {
restrictedPatterns = [ { group : restrictedPatterns } ] ;
}
2024-08-09 12:04:48 +00:00
2024-08-21 06:34:30 +00:00
// relative paths are supported for this rule
const restrictedPatternGroups = restrictedPatterns . map ( ( { group , message , caseSensitive , importNames , importNamePattern } ) => ( {
matcher : ignore ( { allowRelativePaths : true , ignorecase : ! caseSensitive } ) . add ( group ) ,
customMessage : message ,
importNames ,
importNamePattern
} ) ) ;
// if no imports are restricted we don't need to check
2024-08-09 12:04:48 +00:00
if ( Object . keys ( restrictedPaths ) . length === 0 && restrictedPatternGroups . length === 0 ) {
return { } ;
}
/ * *
* Report a restricted path .
* @ param { string } importSource path of the import
* @ param { Map < string , Object [ ] > } importNames Map of import names that are being imported
* @ param { node } node representing the restricted path reference
* @ returns { void }
* @ private
* /
function checkRestrictedPathAndReport ( importSource , importNames , node ) {
if ( ! Object . prototype . hasOwnProperty . call ( restrictedPathMessages , importSource ) ) {
return ;
}
const customMessage = restrictedPathMessages [ importSource ] . message ;
const restrictedImportNames = restrictedPathMessages [ importSource ] . importNames ;
if ( restrictedImportNames ) {
if ( importNames . has ( "*" ) ) {
const specifierData = importNames . get ( "*" ) [ 0 ] ;
context . report ( {
node ,
messageId : customMessage ? "everythingWithCustomMessage" : "everything" ,
loc : specifierData . loc ,
data : {
importSource ,
importNames : restrictedImportNames ,
customMessage
}
} ) ;
}
restrictedImportNames . forEach ( importName => {
if ( importNames . has ( importName ) ) {
const specifiers = importNames . get ( importName ) ;
specifiers . forEach ( specifier => {
context . report ( {
node ,
messageId : customMessage ? "importNameWithCustomMessage" : "importName" ,
loc : specifier . loc ,
data : {
importSource ,
customMessage ,
importName
}
} ) ;
} ) ;
}
} ) ;
} else {
context . report ( {
node ,
messageId : customMessage ? "pathWithCustomMessage" : "path" ,
data : {
importSource ,
customMessage
}
} ) ;
}
}
/ * *
* Report a restricted path specifically for patterns .
* @ param { node } node representing the restricted path reference
2024-08-21 06:34:30 +00:00
* @ param { Object } group contains an Ignore instance for paths , the customMessage to show on failure ,
* and any restricted import names that have been specified in the config
* @ param { Map < string , Object [ ] > } importNames Map of import names that are being imported
2024-08-09 12:04:48 +00:00
* @ returns { void }
* @ private
* /
2024-08-21 06:34:30 +00:00
function reportPathForPatterns ( node , group , importNames ) {
2024-08-09 12:04:48 +00:00
const importSource = node . source . value . trim ( ) ;
2024-08-21 06:34:30 +00:00
const customMessage = group . customMessage ;
const restrictedImportNames = group . importNames ;
const restrictedImportNamePattern = group . importNamePattern ? new RegExp ( group . importNamePattern , "u" ) : null ;
/ *
* If we are not restricting to any specific import names and just the pattern itself ,
* report the error and move on
* /
if ( ! restrictedImportNames && ! restrictedImportNamePattern ) {
context . report ( {
node ,
messageId : customMessage ? "patternWithCustomMessage" : "patterns" ,
data : {
importSource ,
customMessage
}
} ) ;
return ;
}
importNames . forEach ( ( specifiers , importName ) => {
if ( importName === "*" ) {
const [ specifier ] = specifiers ;
if ( restrictedImportNames ) {
context . report ( {
node ,
messageId : customMessage ? "patternAndEverythingWithCustomMessage" : "patternAndEverything" ,
loc : specifier . loc ,
data : {
importSource ,
importNames : restrictedImportNames ,
customMessage
}
} ) ;
} else {
context . report ( {
node ,
messageId : customMessage ? "patternAndEverythingWithRegexImportNameAndCustomMessage" : "patternAndEverythingWithRegexImportName" ,
loc : specifier . loc ,
data : {
importSource ,
importNames : restrictedImportNamePattern ,
customMessage
}
} ) ;
}
return ;
}
if (
( restrictedImportNames && restrictedImportNames . includes ( importName ) ) ||
( restrictedImportNamePattern && restrictedImportNamePattern . test ( importName ) )
) {
specifiers . forEach ( specifier => {
context . report ( {
node ,
messageId : customMessage ? "patternAndImportNameWithCustomMessage" : "patternAndImportName" ,
loc : specifier . loc ,
data : {
importSource ,
customMessage ,
importName
}
} ) ;
} ) ;
2024-08-09 12:04:48 +00:00
}
} ) ;
}
/ * *
* Check if the given importSource is restricted by a pattern .
* @ param { string } importSource path of the import
* @ param { Object } group contains a Ignore instance for paths , and the customMessage to show if it fails
* @ returns { boolean } whether the variable is a restricted pattern or not
* @ private
* /
function isRestrictedPattern ( importSource , group ) {
return group . matcher . ignores ( importSource ) ;
}
/ * *
* Checks a node to see if any problems should be reported .
* @ param { ASTNode } node The node to check .
* @ returns { void }
* @ private
* /
function checkNode ( node ) {
const importSource = node . source . value . trim ( ) ;
const importNames = new Map ( ) ;
if ( node . type === "ExportAllDeclaration" ) {
const starToken = sourceCode . getFirstToken ( node , 1 ) ;
importNames . set ( "*" , [ { loc : starToken . loc } ] ) ;
} else if ( node . specifiers ) {
for ( const specifier of node . specifiers ) {
let name ;
const specifierData = { loc : specifier . loc } ;
if ( specifier . type === "ImportDefaultSpecifier" ) {
name = "default" ;
} else if ( specifier . type === "ImportNamespaceSpecifier" ) {
name = "*" ;
} else if ( specifier . imported ) {
2024-08-21 06:34:30 +00:00
name = astUtils . getModuleExportName ( specifier . imported ) ;
2024-08-09 12:04:48 +00:00
} else if ( specifier . local ) {
2024-08-21 06:34:30 +00:00
name = astUtils . getModuleExportName ( specifier . local ) ;
2024-08-09 12:04:48 +00:00
}
2024-08-21 06:34:30 +00:00
if ( typeof name === "string" ) {
2024-08-09 12:04:48 +00:00
if ( importNames . has ( name ) ) {
importNames . get ( name ) . push ( specifierData ) ;
} else {
importNames . set ( name , [ specifierData ] ) ;
}
}
}
}
checkRestrictedPathAndReport ( importSource , importNames , node ) ;
restrictedPatternGroups . forEach ( group => {
if ( isRestrictedPattern ( importSource , group ) ) {
2024-08-21 06:34:30 +00:00
reportPathForPatterns ( node , group , importNames ) ;
2024-08-09 12:04:48 +00:00
}
} ) ;
}
return {
ImportDeclaration : checkNode ,
ExportNamedDeclaration ( node ) {
if ( node . source ) {
checkNode ( node ) ;
}
} ,
ExportAllDeclaration : checkNode
} ;
}
} ;