2022-09-26 01:08:59 -07:00
'use strict' ;
2023-05-24 01:12:41 -07:00
const path = require ( 'path' ) ;
2022-09-26 01:08:59 -07:00
/ * *
* This file contains any locally defined ESLint rules . They are picked up by
* eslint - plugin - n8n - local - rules and exposed as 'n8n-local-rules/<rule-name>' .
* /
module . exports = {
/ * *
* A rule to detect calls to JSON . parse ( ) that are not wrapped inside try / c a t c h b l o c k s .
*
* Valid :
* ` ` ` js
* try { JSON . parse ( foo ) } catch ( err ) { baz ( ) }
* ` ` `
*
* Invalid :
* ` ` ` js
* JSON . parse ( foo )
* ` ` `
*
* The pattern where an object is cloned with JSON . parse ( JSON . stringify ( ) ) is allowed
* ( abundant in the n8n codebase ) :
*
* Valid :
* ` ` ` js
* JSON . parse ( JSON . stringify ( foo ) )
* ` ` `
* /
'no-uncaught-json-parse' : {
meta : {
type : 'problem' ,
docs : {
2022-10-24 03:48:16 -07:00
description :
'Calls to `JSON.parse()` must be replaced with `jsonParse()` from `n8n-workflow` or surrounded with a try/catch block.' ,
2022-09-26 01:08:59 -07:00
recommended : 'error' ,
} ,
schema : [ ] ,
messages : {
2022-10-24 03:48:16 -07:00
noUncaughtJsonParse :
'Use `jsonParse()` from `n8n-workflow` or surround the `JSON.parse()` call with a try/catch block.' ,
2022-09-26 01:08:59 -07:00
} ,
} ,
defaultOptions : [ ] ,
create ( context ) {
return {
CallExpression ( node ) {
if ( ! isJsonParseCall ( node ) ) {
return ;
}
2022-10-19 00:36:25 -07:00
if ( isJsonStringifyCall ( node ) ) {
2022-09-26 01:08:59 -07:00
return ;
}
if ( context . getAncestors ( ) . find ( ( node ) => node . type === 'TryStatement' ) !== undefined ) {
return ;
}
// Found a JSON.parse() call not wrapped into a try/catch, so report it
context . report ( {
messageId : 'noUncaughtJsonParse' ,
node ,
} ) ;
} ,
} ;
} ,
} ,
2022-10-19 00:36:25 -07:00
'no-json-parse-json-stringify' : {
meta : {
type : 'problem' ,
docs : {
description :
'Calls to `JSON.parse(JSON.stringify(arg))` must be replaced with `deepCopy(arg)` from `n8n-workflow`.' ,
recommended : 'error' ,
} ,
messages : {
noJsonParseJsonStringify : 'Replace with `deepCopy({{ argText }})`' ,
} ,
fixable : 'code' ,
} ,
create ( context ) {
return {
CallExpression ( node ) {
if ( isJsonParseCall ( node ) && isJsonStringifyCall ( node ) ) {
const [ callExpression ] = node . arguments ;
const { arguments : args } = callExpression ;
if ( ! Array . isArray ( args ) || args . length !== 1 ) return ;
const [ arg ] = args ;
if ( ! arg ) return ;
const argText = context . getSourceCode ( ) . getText ( arg ) ;
context . report ( {
messageId : 'noJsonParseJsonStringify' ,
node ,
data : { argText } ,
fix : ( fixer ) => fixer . replaceText ( node , ` deepCopy( ${ argText } ) ` ) ,
} ) ;
}
} ,
} ;
} ,
} ,
2022-12-29 03:20:43 -08:00
'no-unneeded-backticks' : {
meta : {
type : 'problem' ,
docs : {
description :
'Template literal backticks may only be used for string interpolation or multiline strings.' ,
recommended : 'error' ,
} ,
messages : {
noUneededBackticks : 'Use single or double quotes, not backticks' ,
} ,
fixable : 'code' ,
} ,
create ( context ) {
return {
TemplateLiteral ( node ) {
if ( node . expressions . length > 0 ) return ;
if ( node . quasis . every ( ( q ) => q . loc . start . line !== q . loc . end . line ) ) return ;
node . quasis . forEach ( ( q ) => {
const escaped = q . value . raw . replace ( /(?<!\\)'/g , "\\'" ) ;
context . report ( {
messageId : 'noUneededBackticks' ,
node ,
fix : ( fixer ) => fixer . replaceText ( q , ` ' ${ escaped } ' ` ) ,
} ) ;
} ) ;
} ,
} ;
} ,
} ,
2022-12-29 05:24:19 -08:00
2023-03-31 07:44:08 -07:00
'no-unused-param-in-catch-clause' : {
meta : {
type : 'problem' ,
docs : {
description : 'Unused param in catch clause must be omitted.' ,
recommended : 'error' ,
} ,
messages : {
removeUnusedParam : 'Remove unused param in catch clause' ,
} ,
fixable : 'code' ,
} ,
create ( context ) {
return {
CatchClause ( node ) {
2023-03-31 10:16:51 -07:00
if ( node . param ? . name ? . startsWith ( '_' ) ) {
2023-03-31 07:44:08 -07:00
const start = node . range [ 0 ] + 'catch ' . length ;
const end = node . param . range [ 1 ] + '()' . length ;
context . report ( {
messageId : 'removeUnusedParam' ,
node ,
fix : ( fixer ) => fixer . removeRange ( [ start , end ] ) ,
} ) ;
}
} ,
} ;
} ,
} ,
2023-05-24 00:45:37 -07:00
'no-skipped-tests' : {
meta : {
type : 'problem' ,
docs : {
description : 'Tests must not be skipped.' ,
recommended : 'error' ,
} ,
messages : {
removeSkip : 'Remove `.skip()` call' ,
removeOnly : 'Remove `.only()` call' ,
2024-08-07 06:11:22 -07:00
removeXPrefix : 'Remove `x` prefix' ,
2023-05-24 00:45:37 -07:00
} ,
fixable : 'code' ,
} ,
create ( context ) {
const TESTING _FUNCTIONS = new Set ( [ 'test' , 'it' , 'describe' ] ) ;
const SKIPPING _METHODS = new Set ( [ 'skip' , 'only' ] ) ;
2024-08-07 06:11:22 -07:00
const PREFIXED _TESTING _FUNCTIONS = new Set ( [ 'xtest' , 'xit' , 'xdescribe' ] ) ;
2023-05-24 00:45:37 -07:00
const toMessageId = ( s ) => 'remove' + s . charAt ( 0 ) . toUpperCase ( ) + s . slice ( 1 ) ;
return {
MemberExpression ( node ) {
if (
node . object . type === 'Identifier' &&
TESTING _FUNCTIONS . has ( node . object . name ) &&
node . property . type === 'Identifier' &&
SKIPPING _METHODS . has ( node . property . name )
) {
context . report ( {
messageId : toMessageId ( node . property . name ) ,
node ,
fix : ( fixer ) => {
const [ start , end ] = node . property . range ;
return fixer . removeRange ( [ start - '.' . length , end ] ) ;
} ,
} ) ;
}
} ,
2024-08-07 06:11:22 -07:00
CallExpression ( node ) {
if (
node . callee . type === 'Identifier' &&
PREFIXED _TESTING _FUNCTIONS . has ( node . callee . name )
) {
context . report ( {
messageId : 'removeXPrefix' ,
node ,
fix : ( fixer ) => fixer . replaceText ( node . callee , 'test' ) ,
} ) ;
}
} ,
2023-05-24 00:45:37 -07:00
} ;
} ,
} ,
2022-12-29 05:24:19 -08:00
'no-interpolation-in-regular-string' : {
meta : {
type : 'problem' ,
docs : {
description :
'String interpolation `${...}` requires backticks, not single or double quotes.' ,
recommended : 'error' ,
} ,
messages : {
useBackticks : 'Use backticks to interpolate' ,
} ,
fixable : 'code' ,
} ,
create ( context ) {
return {
Literal ( node ) {
if ( typeof node . value !== 'string' ) return ;
if ( /\$\{/ . test ( node . value ) ) {
context . report ( {
messageId : 'useBackticks' ,
node ,
fix : ( fixer ) => fixer . replaceText ( node , ` \` ${ node . value } \` ` ) ,
} ) ;
}
} ,
} ;
} ,
} ,
2023-05-24 01:12:41 -07:00
'dangerously-use-html-string-missing' : {
meta : {
type : 'error' ,
docs : {
description :
'Calls to the `showToast` and `showMessage` methods must include `dangerouslyUseHTMLString: true` when at least one of the values in `title` or `message` contains HTML.' ,
recommended : 'error' ,
/ * *
* @ note This rule does not yet cover cases where the result of calling
* ` this. $ locale.someMethod() ` is assigned to a variable that is then
* assigned to ` title or ` message ` , e.g. ` message : errorMessage ` .
* /
} ,
} ,
create ( context ) {
const cwd = context . getCwd ( ) ;
const locale = 'src/plugins/i18n/locales/en.json' ;
const LOCALE _NAMESPACE = '$locale' ;
const LOCALE _FILEPATH = cwd . endsWith ( 'editor-ui' )
? path . join ( cwd , locale )
: path . join ( cwd , 'packages/editor-ui' , locale ) ;
let LOCALE _MAP ;
try {
LOCALE _MAP = JSON . parse ( require ( 'fs' ) . readFileSync ( LOCALE _FILEPATH ) ) ;
} catch {
console . log (
'[dangerously-use-html-string-missing] Failed to load locale map, skipping rule...' ,
) ;
return { } ;
}
const METHODS _POSSIBLY _REQUIRING _HTML = new Set ( [ 'showToast' , 'showMessage' ] ) ;
const PROPERTIES _POSSIBLY _CONTAINING _HTML = new Set ( [ 'title' , 'message' ] ) ;
const USE _HTML _PROPERTY = 'dangerouslyUseHTMLString' ;
const isMethodPossiblyRequiringRawHtml = ( node ) =>
node . callee . type === 'MemberExpression' &&
node . callee . object . type === 'ThisExpression' &&
node . callee . property . type === 'Identifier' &&
METHODS _POSSIBLY _REQUIRING _HTML . has ( node . callee . property . name ) &&
node . arguments . length === 1 &&
node . arguments . at ( 0 ) . type === 'ObjectExpression' ;
const isPropertyWithLocaleStringAsValue = ( property ) =>
property . key . type === 'Identifier' &&
PROPERTIES _POSSIBLY _CONTAINING _HTML . has ( property . key . name ) &&
property . value . type === 'CallExpression' &&
property . value . callee . type === 'MemberExpression' &&
property . value . callee . object . type === 'MemberExpression' &&
property . value . callee . object . property . type === 'Identifier' &&
property . value . callee . object . property . name === LOCALE _NAMESPACE &&
property . value . arguments . length >= 1 &&
property . value . arguments . at ( 0 ) . type === 'Literal' &&
typeof property . value . arguments . at ( 0 ) . value === 'string' ;
const containsHtml = ( str ) => {
let insideTag = false ;
for ( let char of str ) {
if ( char === '<' ) {
insideTag = true ;
} else if ( char === '>' ) {
if ( insideTag ) return true ;
insideTag = false ;
}
}
return false ;
} ;
return {
CallExpression ( node ) {
if ( ! isMethodPossiblyRequiringRawHtml ( node ) ) return ;
const arg = node . arguments . at ( 0 ) ;
const hasArgWitHtml = arg . properties
. reduce (
( acc , p ) =>
isPropertyWithLocaleStringAsValue ( p )
? [ ... acc , p . value . arguments . at ( 0 ) . value ]
: acc ,
[ ] ,
)
. some ( ( i ) => containsHtml ( LOCALE _MAP [ i ] ) ) ;
if ( ! hasArgWitHtml ) return ;
const hasRawHtmlPropertyAsTrue = arg . properties . some (
( p ) =>
p . key . type === 'Identifier' &&
p . key . name === USE _HTML _PROPERTY &&
p . value . type === 'Literal' &&
p . value . value === true ,
) ;
if ( hasRawHtmlPropertyAsTrue ) return ;
const methodName = node . callee . property . name ;
context . report ( {
node ,
message : ` Set \` ${ USE _HTML _PROPERTY } : true \` in the argument to \` ${ methodName } \` . At least one of the values in \` title \` or \` message \` contains HTML. ` ,
} ) ;
} ,
} ;
} ,
} ,
2023-12-08 03:51:49 -08:00
'no-plain-errors' : {
meta : {
type : 'problem' ,
docs : {
description :
'Only `ApplicationError` (from the `workflow` package) or its child classes must be thrown. This ensures the error will be normalized when reported to Sentry, if applicable.' ,
recommended : 'error' ,
} ,
messages : {
useApplicationError :
'Throw an `ApplicationError` (from the `workflow` package) or its child classes.' ,
} ,
fixable : 'code' ,
} ,
create ( context ) {
return {
ThrowStatement ( node ) {
if ( ! node . argument ) return ;
const isNewError =
node . argument . type === 'NewExpression' && node . argument . callee . name === 'Error' ;
const isNewlessError =
node . argument . type === 'CallExpression' && node . argument . callee . name === 'Error' ;
if ( isNewError || isNewlessError ) {
return context . report ( {
messageId : 'useApplicationError' ,
node ,
fix : ( fixer ) =>
fixer . replaceText (
node ,
` throw new ApplicationError( ${ node . argument . arguments
. map ( ( arg ) => arg . raw )
. join ( ', ' ) } ) ` ,
) ,
} ) ;
}
} ,
} ;
} ,
} ,
2023-12-21 05:15:37 -08:00
'no-dynamic-import-template' : {
meta : {
type : 'error' ,
docs : {
description :
'Disallow non-relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario.' ,
recommended : true ,
} ,
} ,
create : function ( context ) {
return {
'AwaitExpression > ImportExpression TemplateLiteral' ( node ) {
const templateValue = node . quasis [ 0 ] . value . cooked ;
if ( ! templateValue ? . startsWith ( '@/' ) ) return ;
context . report ( {
node ,
message :
'Use relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario.' ,
} ) ;
} ,
} ;
} ,
} ,
2024-06-24 01:24:05 -07:00
'misplaced-n8n-typeorm-import' : {
meta : {
type : 'error' ,
docs : {
description :
'Ensure `@n8n/typeorm` is imported only from within the `packages/cli/src/databases` directory.' ,
recommended : 'error' ,
} ,
messages : {
moveImport : 'Move this import to `packages/cli/src/databases/**/*.ts`.' ,
} ,
} ,
create ( context ) {
return {
ImportDeclaration ( node ) {
if (
node . source . value === '@n8n/typeorm' &&
! context . getFilename ( ) . includes ( 'packages/cli/src/databases/' )
) {
context . report ( { node , messageId : 'moveImport' } ) ;
}
} ,
} ;
} ,
} ,
2024-08-02 03:01:42 -07:00
'no-type-unsafe-event-emitter' : {
meta : {
type : 'problem' ,
docs : {
description : 'Disallow extending from `EventEmitter`, which is not type-safe.' ,
recommended : 'error' ,
} ,
messages : {
noExtendsEventEmitter : 'Extend from the type-safe `TypedEmitter` class instead.' ,
} ,
} ,
create ( context ) {
return {
ClassDeclaration ( node ) {
if (
node . superClass &&
node . superClass . type === 'Identifier' &&
node . superClass . name === 'EventEmitter' &&
node . id . name !== 'TypedEmitter'
) {
context . report ( {
node : node . superClass ,
messageId : 'noExtendsEventEmitter' ,
} ) ;
}
} ,
} ;
} ,
} ,
2024-08-15 07:00:11 -07:00
'no-untyped-config-class-field' : {
meta : {
type : 'problem' ,
docs : {
description : 'Enforce explicit typing of config class fields' ,
recommended : 'error' ,
} ,
messages : {
noUntypedConfigClassField :
'Class field must have an explicit type annotation, e.g. `field: type = value`. See: https://github.com/n8n-io/n8n/pull/10433' ,
} ,
} ,
create ( context ) {
return {
PropertyDefinition ( node ) {
if ( ! node . typeAnnotation ) {
context . report ( { node : node . key , messageId : 'noUntypedConfigClassField' } ) ;
}
} ,
} ;
} ,
} ,
2022-09-26 01:08:59 -07:00
} ;
const isJsonParseCall = ( node ) =>
node . callee . type === 'MemberExpression' &&
node . callee . object . type === 'Identifier' &&
node . callee . object . name === 'JSON' &&
node . callee . property . type === 'Identifier' &&
node . callee . property . name === 'parse' ;
2022-10-19 00:36:25 -07:00
const isJsonStringifyCall = ( node ) => {
2022-09-26 01:08:59 -07:00
const parseArg = node . arguments ? . [ 0 ] ;
return (
parseArg !== undefined &&
parseArg . type === 'CallExpression' &&
parseArg . callee . type === 'MemberExpression' &&
parseArg . callee . object . type === 'Identifier' &&
parseArg . callee . object . name === 'JSON' &&
parseArg . callee . property . type === 'Identifier' &&
parseArg . callee . property . name === 'stringify'
) ;
} ;