@@ -9,7 +9,12 @@ import type {
9
9
Quantifier ,
10
10
} from "regexpp/ast"
11
11
import type { RegExpContext } from "../utils"
12
- import { createRule , defineRegexpVisitor } from "../utils"
12
+ import {
13
+ createRule ,
14
+ defineRegexpVisitor ,
15
+ fixRemoveCharacterClassElement ,
16
+ fixRemoveAlternative ,
17
+ } from "../utils"
13
18
import { isCoveredNode , isEqualNodes } from "../utils/regexp-ast"
14
19
import type { Expression , FiniteAutomaton , NoParent , ReadonlyNFA } from "refa"
15
20
import {
@@ -35,6 +40,8 @@ import { canReorder } from "../utils/reorder-alternatives"
35
40
import { mention , mentionChar } from "../utils/mention"
36
41
import type { NestedAlternative } from "../utils/partial-parser"
37
42
import { PartialParser } from "../utils/partial-parser"
43
+ import type { Rule } from "eslint"
44
+ import { getAllowedCharRanges , inRange } from "../utils/char-ranges"
38
45
39
46
type ParentNode = Group | CapturingGroup | Pattern | LookaroundAssertion
40
47
@@ -837,6 +844,34 @@ function mentionNested(nested: NestedAlternative): string {
837
844
return mentionChar ( nested )
838
845
}
839
846
847
+ /**
848
+ * Returns a fix that removes the given alternative.
849
+ */
850
+ function fixRemoveNestedAlternative (
851
+ context : RegExpContext ,
852
+ alternative : NestedAlternative ,
853
+ ) {
854
+ switch ( alternative . type ) {
855
+ case "Alternative" :
856
+ return fixRemoveAlternative ( context , alternative )
857
+
858
+ case "Character" :
859
+ case "CharacterClassRange" :
860
+ case "CharacterSet" : {
861
+ if ( alternative . parent . type !== "CharacterClass" ) {
862
+ // This isn't supposed to happen. We can't just remove the only
863
+ // alternative of its parent
864
+ return ( ) => null
865
+ }
866
+
867
+ return fixRemoveCharacterClassElement ( context , alternative )
868
+ }
869
+
870
+ default :
871
+ throw assertNever ( alternative )
872
+ }
873
+ }
874
+
840
875
const enum ReportOption {
841
876
all = "all" ,
842
877
trivial = "trivial" ,
@@ -873,6 +908,7 @@ export default createRule("no-dupe-disjunctions", {
873
908
category : "Possible Errors" ,
874
909
recommended : true ,
875
910
} ,
911
+ hasSuggestions : true ,
876
912
schema : [
877
913
{
878
914
type : "object" ,
@@ -905,6 +941,9 @@ export default createRule("no-dupe-disjunctions", {
905
941
"Unexpected superset. This alternative is a superset of {{others}}. It might be possible to remove the other alternative(s).{{cap}}{{exp}}" ,
906
942
overlap :
907
943
"Unexpected overlap. This alternative overlaps with {{others}}. The overlap is {{expr}}.{{cap}}{{exp}}" ,
944
+
945
+ remove : "Remove the {{alternative}} {{type}}." ,
946
+ replaceRange : "Replace {{range}} with {{replacement}}." ,
908
947
} ,
909
948
type : "suggestion" , // "problem",
910
949
} ,
@@ -917,6 +956,8 @@ export default createRule("no-dupe-disjunctions", {
917
956
const report : ReportOption =
918
957
context . options [ 0 ] ?. report ?? ReportOption . trivial
919
958
959
+ const allowedRanges = getAllowedCharRanges ( undefined , context )
960
+
920
961
/**
921
962
* Create visitor
922
963
*/
@@ -1104,6 +1145,103 @@ export default createRule("no-dupe-disjunctions", {
1104
1145
}
1105
1146
}
1106
1147
1148
+ /** Prints the given character. */
1149
+ function printChar ( char : number ) : string {
1150
+ if ( inRange ( allowedRanges , char ) ) {
1151
+ return String . fromCodePoint ( char )
1152
+ }
1153
+
1154
+ if ( char === 0 ) return "\\0"
1155
+ if ( char <= 0xff )
1156
+ return `\\x${ char . toString ( 16 ) . padStart ( 2 , "0" ) } `
1157
+ if ( char <= 0xffff )
1158
+ return `\\u${ char . toString ( 16 ) . padStart ( 4 , "0" ) } `
1159
+
1160
+ return `\\u{${ char . toString ( 16 ) } }`
1161
+ }
1162
+
1163
+ /** Returns suggestions for fixing the given report */
1164
+ function getSuggestions (
1165
+ result : Result ,
1166
+ ) : Rule . SuggestionReportDescriptor [ ] {
1167
+ if ( result . type === "Overlap" || result . type === "Superset" ) {
1168
+ // the types of results cannot be trivially fixed by
1169
+ // removing an alternative.
1170
+ return [ ]
1171
+ }
1172
+
1173
+ const alternative =
1174
+ result . type === "NestedSubset" ||
1175
+ result . type === "PrefixNestedSubset"
1176
+ ? result . nested
1177
+ : result . alternative
1178
+
1179
+ const containsCapturingGroup = hasSomeDescendant (
1180
+ alternative ,
1181
+ ( d ) => d . type === "CapturingGroup" ,
1182
+ )
1183
+ if ( containsCapturingGroup ) {
1184
+ // we can't just remove a capturing group
1185
+ return [ ]
1186
+ }
1187
+
1188
+ if (
1189
+ alternative . type === "Character" &&
1190
+ alternative . parent . type === "CharacterClassRange"
1191
+ ) {
1192
+ const range = alternative . parent
1193
+
1194
+ let replacement
1195
+ if ( range . min . value + 1 === range . max . value ) {
1196
+ replacement =
1197
+ range . min === alternative
1198
+ ? range . max . raw
1199
+ : range . min . raw
1200
+ } else {
1201
+ if ( range . min === alternative ) {
1202
+ // replace with {min+1}-{max}
1203
+ const min = printChar ( range . min . value + 1 )
1204
+ replacement = `${ min } -${ range . max . raw } `
1205
+ } else {
1206
+ // replace with {min}-{max-1}
1207
+ const max = printChar ( range . max . value - 1 )
1208
+ replacement = `${ range . min . raw } -${ max } `
1209
+ }
1210
+ }
1211
+
1212
+ return [
1213
+ {
1214
+ messageId : "replaceRange" ,
1215
+ data : {
1216
+ range : mentionChar ( range ) ,
1217
+ replacement : mention ( replacement ) ,
1218
+ } ,
1219
+ fix : regexpContext . fixReplaceNode (
1220
+ range ,
1221
+ replacement ,
1222
+ ) ,
1223
+ } ,
1224
+ ]
1225
+ }
1226
+
1227
+ return [
1228
+ {
1229
+ messageId : "remove" ,
1230
+ data : {
1231
+ alternative : mentionNested ( alternative ) ,
1232
+ type :
1233
+ alternative . type === "Alternative"
1234
+ ? "alternative"
1235
+ : "element" ,
1236
+ } ,
1237
+ fix : fixRemoveNestedAlternative (
1238
+ regexpContext ,
1239
+ alternative ,
1240
+ ) ,
1241
+ } ,
1242
+ ]
1243
+ }
1244
+
1107
1245
/** Report the given result. */
1108
1246
function reportResult ( result : Result , { stared } : FilterInfo ) {
1109
1247
let exp
@@ -1136,13 +1274,16 @@ export default createRule("no-dupe-disjunctions", {
1136
1274
result . others . map ( ( a ) => a . raw ) . join ( "|" ) ,
1137
1275
)
1138
1276
1277
+ const suggest = getSuggestions ( result )
1278
+
1139
1279
switch ( result . type ) {
1140
1280
case "Duplicate" :
1141
1281
context . report ( {
1142
1282
node,
1143
1283
loc,
1144
1284
messageId : "duplicate" ,
1145
1285
data : { exp, cap, others } ,
1286
+ suggest,
1146
1287
} )
1147
1288
break
1148
1289
@@ -1152,6 +1293,7 @@ export default createRule("no-dupe-disjunctions", {
1152
1293
loc,
1153
1294
messageId : "subset" ,
1154
1295
data : { exp, cap, others } ,
1296
+ suggest,
1155
1297
} )
1156
1298
break
1157
1299
@@ -1167,6 +1309,7 @@ export default createRule("no-dupe-disjunctions", {
1167
1309
root : mention ( result . alternative ) ,
1168
1310
nested : mentionNested ( result . nested ) ,
1169
1311
} ,
1312
+ suggest,
1170
1313
} )
1171
1314
break
1172
1315
@@ -1176,6 +1319,7 @@ export default createRule("no-dupe-disjunctions", {
1176
1319
loc,
1177
1320
messageId : "prefixSubset" ,
1178
1321
data : { exp, cap, others } ,
1322
+ suggest,
1179
1323
} )
1180
1324
break
1181
1325
@@ -1191,6 +1335,7 @@ export default createRule("no-dupe-disjunctions", {
1191
1335
root : mention ( result . alternative ) ,
1192
1336
nested : mentionNested ( result . nested ) ,
1193
1337
} ,
1338
+ suggest,
1194
1339
} )
1195
1340
break
1196
1341
@@ -1200,6 +1345,7 @@ export default createRule("no-dupe-disjunctions", {
1200
1345
loc,
1201
1346
messageId : "superset" ,
1202
1347
data : { exp, cap, others } ,
1348
+ suggest,
1203
1349
} )
1204
1350
break
1205
1351
@@ -1216,6 +1362,7 @@ export default createRule("no-dupe-disjunctions", {
1216
1362
faToSource ( result . overlap , flags ) ,
1217
1363
) ,
1218
1364
} ,
1365
+ suggest,
1219
1366
} )
1220
1367
break
1221
1368
0 commit comments