@@ -7,9 +7,15 @@ const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add
77
88const MESSAGE_STARTS_WITH = 'prefer-starts-with' ;
99const MESSAGE_ENDS_WITH = 'prefer-ends-with' ;
10+ const SUGGEST_STRING_CAST = 'suggest-string-cast' ;
11+ const SUGGEST_OPTIONAL_CHAINING = 'suggest-optional-chaining' ;
12+ const SUGGEST_NULLISH_COALESCING = 'suggest-nullish-coalescing' ;
1013const messages = {
1114 [ MESSAGE_STARTS_WITH ] : 'Prefer `String#startsWith()` over a regex with `^`.' ,
12- [ MESSAGE_ENDS_WITH ] : 'Prefer `String#endsWith()` over a regex with `$`.'
15+ [ MESSAGE_ENDS_WITH ] : 'Prefer `String#endsWith()` over a regex with `$`.' ,
16+ [ SUGGEST_STRING_CAST ] : 'For strings that may be `undefined` / `null`, use string casting.' ,
17+ [ SUGGEST_OPTIONAL_CHAINING ] : 'For strings that may be `undefined` / `null`, use optional chaining.' ,
18+ [ SUGGEST_NULLISH_COALESCING ] : 'For strings that may be `undefined` / `null`, use nullish coalescing.'
1319} ;
1420
1521const doesNotContain = ( string , characters ) => characters . every ( character => ! string . includes ( character ) ) ;
@@ -64,33 +70,62 @@ const create = context => {
6470 return ;
6571 }
6672
73+ function fix ( fixer , { useNullishCoalescing, useOptionalChaining, useStringCasting} = { } ) {
74+ const method = result . messageId === MESSAGE_STARTS_WITH ? 'startsWith' : 'endsWith' ;
75+ const [ target ] = node . arguments ;
76+ let targetString = sourceCode . getText ( target ) ;
77+ const isRegexParenthesized = isParenthesized ( regexNode , sourceCode ) ;
78+ const isTargetParenthesized = isParenthesized ( target , sourceCode ) ;
79+
80+ if (
81+ // If regex is parenthesized, we can use it, so we don't need add again
82+ ! isRegexParenthesized &&
83+ ( isTargetParenthesized || shouldAddParenthesesToMemberExpressionObject ( target , sourceCode ) )
84+ ) {
85+ targetString = `(${ targetString } )` ;
86+ }
87+
88+ if ( useNullishCoalescing ) {
89+ // (target ?? '').startsWith(pattern)
90+ targetString = ( isRegexParenthesized ? '' : '(' ) + targetString + ' ?? \'\'' + ( isRegexParenthesized ? '' : ')' ) ;
91+ } else if ( useStringCasting ) {
92+ // String(target).startsWith(pattern)
93+ const isTargetStringParenthesized = targetString . startsWith ( '(' ) ;
94+ targetString = 'String' + ( isTargetStringParenthesized ? '' : '(' ) + targetString + ( isTargetStringParenthesized ? '' : ')' ) ;
95+ }
96+
97+ // The regex literal always starts with `/` or `(`, so we don't need check ASI
98+
99+ return [
100+ // Replace regex with string
101+ fixer . replaceText ( regexNode , targetString ) ,
102+ // `.test` => `.startsWith` / `.endsWith`
103+ fixer . replaceText ( node . callee . property , method ) ,
104+ // Optional chaining: target.startsWith => target?.startsWith
105+ useOptionalChaining ? fixer . replaceText ( sourceCode . getTokenBefore ( node . callee . property ) , '?.' ) : undefined ,
106+ // Replace argument with result.string
107+ fixer . replaceText ( target , quoteString ( result . string ) )
108+ ] . filter ( Boolean ) ;
109+ }
110+
67111 context . report ( {
68112 node,
69113 messageId : result . messageId ,
70- fix : fixer => {
71- const method = result . messageId === MESSAGE_STARTS_WITH ? 'startsWith' : 'endsWith' ;
72- const [ target ] = node . arguments ;
73- let targetString = sourceCode . getText ( target ) ;
74-
75- if (
76- // If regex is parenthesized, we can use it, so we don't need add again
77- ! isParenthesized ( regexNode , sourceCode ) &&
78- ( isParenthesized ( target , sourceCode ) || shouldAddParenthesesToMemberExpressionObject ( target , sourceCode ) )
79- ) {
80- targetString = `(${ targetString } )` ;
114+ suggest : [
115+ {
116+ messageId : SUGGEST_STRING_CAST ,
117+ fix : fixer => fix ( fixer , { useStringCasting : true } )
118+ } ,
119+ {
120+ messageId : SUGGEST_OPTIONAL_CHAINING ,
121+ fix : fixer => fix ( fixer , { useOptionalChaining : true } )
122+ } ,
123+ {
124+ messageId : SUGGEST_NULLISH_COALESCING ,
125+ fix : fixer => fix ( fixer , { useNullishCoalescing : true } )
81126 }
82-
83- // The regex literal always starts with `/` or `(`, so we don't need check ASI
84-
85- return [
86- // Replace regex with string
87- fixer . replaceText ( regexNode , targetString ) ,
88- // `.test` => `.startsWith` / `.endsWith`
89- fixer . replaceText ( node . callee . property , method ) ,
90- // Replace argument with result.string
91- fixer . replaceText ( target , quoteString ( result . string ) )
92- ] ;
93- }
127+ ] ,
128+ fix
94129 } ) ;
95130 }
96131 } ;
@@ -102,7 +137,8 @@ module.exports = {
102137 type : 'suggestion' ,
103138 docs : {
104139 description : 'Prefer `String#startsWith()` & `String#endsWith()` over `RegExp#test()`.' ,
105- url : getDocumentationUrl ( __filename )
140+ url : getDocumentationUrl ( __filename ) ,
141+ suggest : true
106142 } ,
107143 messages,
108144 fixable : 'code' ,
0 commit comments