@@ -10,6 +10,29 @@ const argumentsVariable = templates.spreadVariable();
1010const substrCallTemplate = templates . template `${ objectVariable } .substr(${ argumentsVariable } )` ;
1111const substringCallTemplate = templates . template `${ objectVariable } .substring(${ argumentsVariable } )` ;
1212
13+ const isLiteralNumber = node => node && node . type === 'Literal' && typeof node . value === 'number' ;
14+
15+ const getNumericValue = node => {
16+ if ( isLiteralNumber ( node ) ) {
17+ return node . value ;
18+ }
19+
20+ if ( node . type === 'UnaryExpression' && node . operator === '-' ) {
21+ return - getNumericValue ( node . argument ) ;
22+ }
23+ } ;
24+
25+ // This handles cases where the argument is very likely to be a number, such as `.substring('foo'.length)`.
26+ const isLengthProperty = node => (
27+ node &&
28+ node . type === 'MemberExpression' &&
29+ node . computed === false &&
30+ node . property . type === 'Identifier' &&
31+ node . property . name === 'length'
32+ ) ;
33+
34+ const isLikelyNumeric = node => isLiteralNumber ( node ) || isLengthProperty ( node ) ;
35+
1336const create = context => {
1437 const sourceCode = context . getSourceCode ( ) ;
1538
@@ -23,10 +46,31 @@ const create = context => {
2346 message : 'Prefer `String#slice()` over `String#substr()`.'
2447 } ;
2548
26- const canFix = argumentNodes . length === 0 ;
49+ const firstArgument = argumentNodes [ 0 ] ? sourceCode . getText ( argumentNodes [ 0 ] ) : undefined ;
50+ const secondArgument = argumentNodes [ 1 ] ? sourceCode . getText ( argumentNodes [ 1 ] ) : undefined ;
51+
52+ let slice ;
2753
28- if ( canFix ) {
29- problem . fix = fixer => fixer . replaceText ( node , sourceCode . getText ( objectNode ) + '.slice()' ) ;
54+ if ( argumentNodes . length === 0 ) {
55+ slice = [ ] ;
56+ } else if ( argumentNodes . length === 1 ) {
57+ slice = [ firstArgument ] ;
58+ } else if ( argumentNodes . length === 2 ) {
59+ if ( firstArgument === '0' ) {
60+ slice = [ firstArgument , secondArgument ] ;
61+ } else if ( isLiteralNumber ( argumentNodes [ 0 ] ) && isLiteralNumber ( argumentNodes [ 1 ] ) ) {
62+ slice = [ firstArgument , argumentNodes [ 0 ] . value + argumentNodes [ 1 ] . value ] ;
63+ } else if ( isLikelyNumeric ( argumentNodes [ 0 ] ) && isLikelyNumeric ( argumentNodes [ 1 ] ) ) {
64+ slice = [ firstArgument , firstArgument + ' + ' + secondArgument ] ;
65+ }
66+ }
67+
68+ if ( slice ) {
69+ const objectText = objectNode . type === 'LogicalExpression' ?
70+ `(${ sourceCode . getText ( objectNode ) } )` :
71+ sourceCode . getText ( objectNode ) ;
72+
73+ problem . fix = fixer => fixer . replaceText ( node , `${ objectText } .slice(${ slice . join ( ', ' ) } )` ) ;
3074 }
3175
3276 context . report ( problem ) ;
@@ -41,10 +85,48 @@ const create = context => {
4185 message : 'Prefer `String#slice()` over `String#substring()`.'
4286 } ;
4387
44- const canFix = argumentNodes . length === 0 ;
88+ const firstArgument = argumentNodes [ 0 ] ? sourceCode . getText ( argumentNodes [ 0 ] ) : undefined ;
89+ const secondArgument = argumentNodes [ 1 ] ? sourceCode . getText ( argumentNodes [ 1 ] ) : undefined ;
90+
91+ const firstNumber = argumentNodes [ 0 ] ? getNumericValue ( argumentNodes [ 0 ] ) : undefined ;
92+
93+ let slice ;
94+
95+ if ( argumentNodes . length === 0 ) {
96+ slice = [ ] ;
97+ } else if ( argumentNodes . length === 1 ) {
98+ if ( firstNumber !== undefined ) {
99+ slice = [ Math . max ( 0 , firstNumber ) ] ;
100+ } else if ( isLengthProperty ( argumentNodes [ 0 ] ) ) {
101+ slice = [ firstArgument ] ;
102+ } else {
103+ slice = [ `Math.max(0, ${ firstArgument } )` ] ;
104+ }
105+ } else if ( argumentNodes . length === 2 ) {
106+ const secondNumber = argumentNodes [ 1 ] ? getNumericValue ( argumentNodes [ 1 ] ) : undefined ;
107+
108+ if ( firstNumber !== undefined && secondNumber !== undefined ) {
109+ slice = firstNumber > secondNumber ?
110+ [ Math . max ( 0 , secondNumber ) , Math . max ( 0 , firstNumber ) ] :
111+ [ Math . max ( 0 , firstNumber ) , Math . max ( 0 , secondNumber ) ] ;
112+ } else if ( firstNumber === 0 || secondNumber === 0 ) {
113+ slice = [ 0 , `Math.max(0, ${ firstNumber === 0 ? secondArgument : firstArgument } )` ] ;
114+ } else {
115+ // As values aren't Literal, we can not know whether secondArgument will become smaller than the first or not, causing an issue:
116+ // .substring(0, 2) and .substring(2, 0) returns the same result
117+ // .slice(0, 2) and .slice(2, 0) doesn't return the same result
118+ // There's also an issue with us now knowing whether the value will be negative or not, due to:
119+ // .substring() treats a negative number the same as it treats a zero.
120+ // The latter issue could be solved by wrapping all dynamic numbers in Math.max(0, <value>), but the resulting code would not be nice
121+ }
122+ }
123+
124+ if ( slice ) {
125+ const objectText = objectNode . type === 'LogicalExpression' ?
126+ `(${ sourceCode . getText ( objectNode ) } )` :
127+ sourceCode . getText ( objectNode ) ;
45128
46- if ( canFix ) {
47- problem . fix = fixer => fixer . replaceText ( node , sourceCode . getText ( objectNode ) + '.slice()' ) ;
129+ problem . fix = fixer => fixer . replaceText ( node , `${ objectText } .slice(${ slice . join ( ', ' ) } )` ) ;
48130 }
49131
50132 context . report ( problem ) ;
0 commit comments