@@ -62,7 +62,6 @@ function *iterateCharacterSequence(nodes) {
62
62
}
63
63
}
64
64
65
-
66
65
/**
67
66
* Checks whether the given character node is a Unicode code point escape or not.
68
67
* @param {Character } char the character node to check.
@@ -73,80 +72,120 @@ function isUnicodeCodePointEscape(char) {
73
72
}
74
73
75
74
/**
76
- * Each function returns `true` if it detects that kind of problem.
77
- * @type {Record<string, (chars: Character[]) => boolean > }
75
+ * Each function returns matched characters if it detects that kind of problem.
76
+ * @type {Record<string, (char: Character, index: number, chars: Character[]) => Character[] | null > }
78
77
*/
79
- const hasCharacterSequence = {
80
- surrogatePairWithoutUFlag ( chars ) {
81
- return chars . some ( ( c , i ) => {
82
- if ( i === 0 ) {
83
- return false ;
84
- }
85
- const c1 = chars [ i - 1 ] ;
86
-
87
- return (
88
- isSurrogatePair ( c1 . value , c . value ) &&
89
- ! isUnicodeCodePointEscape ( c1 ) &&
90
- ! isUnicodeCodePointEscape ( c )
91
- ) ;
92
- } ) ;
78
+ const characterSequenceIndexFilters = {
79
+ surrogatePairWithoutUFlag ( char , index , chars ) {
80
+ if ( index === 0 ) {
81
+ return null ;
82
+ }
83
+
84
+ const previous = chars [ index - 1 ] ;
85
+
86
+ if (
87
+ isSurrogatePair ( previous . value , char . value ) &&
88
+ ! isUnicodeCodePointEscape ( previous ) &&
89
+ ! isUnicodeCodePointEscape ( char )
90
+ ) {
91
+ return [ previous , char ] ;
92
+ }
93
+
94
+ return null ;
93
95
} ,
94
96
95
- surrogatePair ( chars ) {
96
- return chars . some ( ( c , i ) => {
97
- if ( i === 0 ) {
98
- return false ;
99
- }
100
- const c1 = chars [ i - 1 ] ;
101
-
102
- return (
103
- isSurrogatePair ( c1 . value , c . value ) &&
104
- (
105
- isUnicodeCodePointEscape ( c1 ) ||
106
- isUnicodeCodePointEscape ( c )
107
- )
108
- ) ;
109
- } ) ;
97
+ surrogatePair ( char , index , chars ) {
98
+ if ( index === 0 ) {
99
+ return null ;
100
+ }
101
+
102
+ const previous = chars [ index - 1 ] ;
103
+
104
+ if (
105
+ isSurrogatePair ( previous . value , char . value ) &&
106
+ (
107
+ isUnicodeCodePointEscape ( previous ) ||
108
+ isUnicodeCodePointEscape ( char )
109
+ )
110
+ ) {
111
+ return [ previous , char ] ;
112
+ }
113
+
114
+ return null ;
110
115
} ,
111
116
112
- combiningClass ( chars ) {
113
- return chars . some ( ( c , i ) => (
114
- i !== 0 &&
115
- isCombiningCharacter ( c . value ) &&
116
- ! isCombiningCharacter ( chars [ i - 1 ] . value )
117
- ) ) ;
117
+ combiningClass ( char , index , chars ) {
118
+ if (
119
+ index !== 0 &&
120
+ isCombiningCharacter ( char . value ) &&
121
+ ! isCombiningCharacter ( chars [ index - 1 ] . value )
122
+ ) {
123
+ return [ chars [ index - 1 ] , char ] ;
124
+ }
125
+
126
+ return null ;
118
127
} ,
119
128
120
- emojiModifier ( chars ) {
121
- return chars . some ( ( c , i ) => (
122
- i !== 0 &&
123
- isEmojiModifier ( c . value ) &&
124
- ! isEmojiModifier ( chars [ i - 1 ] . value )
125
- ) ) ;
129
+ emojiModifier ( char , index , chars ) {
130
+ if (
131
+ index !== 0 &&
132
+ isEmojiModifier ( char . value ) &&
133
+ ! isEmojiModifier ( chars [ index - 1 ] . value )
134
+ ) {
135
+ return [ chars [ index - 1 ] , char ] ;
136
+ }
137
+
138
+ return null ;
126
139
} ,
127
140
128
- regionalIndicatorSymbol ( chars ) {
129
- return chars . some ( ( c , i ) => (
130
- i !== 0 &&
131
- isRegionalIndicatorSymbol ( c . value ) &&
132
- isRegionalIndicatorSymbol ( chars [ i - 1 ] . value )
133
- ) ) ;
141
+ regionalIndicatorSymbol ( char , index , chars ) {
142
+ if (
143
+ index !== 0 &&
144
+ isRegionalIndicatorSymbol ( char . value ) &&
145
+ isRegionalIndicatorSymbol ( chars [ index - 1 ] . value )
146
+ ) {
147
+ return [ chars [ index - 1 ] , char ] ;
148
+ }
149
+
150
+ return null ;
134
151
} ,
135
152
136
- zwj ( chars ) {
137
- const lastIndex = chars . length - 1 ;
153
+ zwj ( char , index , chars ) {
154
+ if (
155
+ index !== 0 &&
156
+ index !== chars . length - 1 &&
157
+ char . value === 0x200d &&
158
+ chars [ index - 1 ] . value !== 0x200d &&
159
+ chars [ index + 1 ] . value !== 0x200d
160
+ ) {
161
+ return chars . slice ( index - 1 , index + 2 ) ;
162
+ }
138
163
139
- return chars . some ( ( c , i ) => (
140
- i !== 0 &&
141
- i !== lastIndex &&
142
- c . value === 0x200d &&
143
- chars [ i - 1 ] . value !== 0x200d &&
144
- chars [ i + 1 ] . value !== 0x200d
145
- ) ) ;
164
+ return null ;
146
165
}
147
166
} ;
148
167
149
- const kinds = Object . keys ( hasCharacterSequence ) ;
168
+ const kinds = Object . keys ( characterSequenceIndexFilters ) ;
169
+
170
+ /**
171
+ * Collects the indices where the filter returns an array.
172
+ * @param {Character[] } chars Characters to run the filter on.
173
+ * @param {(char: Character, index: number, chars: Character[]) => Character[] | null } filter Finds matches for an index.
174
+ * @returns {Character[][] } Indices where the filter returned true.
175
+ */
176
+ function accumulate ( chars , filter ) {
177
+ const matchingChars = [ ] ;
178
+
179
+ chars . forEach ( ( char , index ) => {
180
+ const matches = filter ( char , index , chars ) ;
181
+
182
+ if ( matches ) {
183
+ matchingChars . push ( matches ) ;
184
+ }
185
+ } ) ;
186
+
187
+ return matchingChars ;
188
+ }
150
189
151
190
//------------------------------------------------------------------------------
152
191
// Rule Definition
@@ -181,6 +220,62 @@ module.exports = {
181
220
const sourceCode = context . sourceCode ;
182
221
const parser = new RegExpParser ( ) ;
183
222
223
+ /**
224
+ * Generates a granular loc for context.report, if directly calculable.
225
+ * @param {Character[] } chars Individual characters being reported on.
226
+ * @param {Node } node Parent string node to report within.
227
+ * @returns {Object | null } Granular loc for context.report, if directly calculable.
228
+ * @see https://github.com/eslint/eslint/pull/17515
229
+ */
230
+ function generateReportLocation ( chars , node ) {
231
+
232
+ // Limit to to literals and expression-less templates with raw values === their value.
233
+ switch ( node . type ) {
234
+ case "TemplateLiteral" :
235
+ if ( node . expressions . length || node . quasis [ 0 ] . value . raw !== node . quasis [ 0 ] . value . cooked ) {
236
+ return null ;
237
+ }
238
+ break ;
239
+
240
+ case "Literal" :
241
+ if ( typeof node . value === "string" && node . value !== node . raw . slice ( 1 , - 1 ) ) {
242
+ return null ;
243
+ }
244
+ break ;
245
+
246
+ default :
247
+ return null ;
248
+ }
249
+
250
+ return {
251
+ start : sourceCode . getLocFromIndex ( node . range [ 0 ] + 1 + chars [ 0 ] . start ) ,
252
+ end : sourceCode . getLocFromIndex ( node . range [ 0 ] + 1 + chars . at ( - 1 ) . end )
253
+ } ;
254
+ }
255
+
256
+ /**
257
+ * Finds the report loc(s) for a range of matches.
258
+ * @param {Character[][] } matches Characters that should trigger a report.
259
+ * @param {Node } node The node to report.
260
+ * @returns {Object | null } Node loc(s) for context.report.
261
+ */
262
+ function getNodeReportLocations ( matches , node ) {
263
+ const locs = [ ] ;
264
+
265
+ for ( const chars of matches ) {
266
+ const loc = generateReportLocation ( chars , node ) ;
267
+
268
+ // If a report can't match to a range, don't report any others
269
+ if ( ! loc ) {
270
+ return [ node . loc ] ;
271
+ }
272
+
273
+ locs . push ( loc ) ;
274
+ }
275
+
276
+ return locs ;
277
+ }
278
+
184
279
/**
185
280
* Verify a given regular expression.
186
281
* @param {Node } node The node to report.
@@ -208,21 +303,26 @@ module.exports = {
208
303
return ;
209
304
}
210
305
211
- const foundKinds = new Set ( ) ;
306
+ const foundKindMatches = new Map ( ) ;
212
307
213
308
visitRegExpAST ( patternNode , {
214
309
onCharacterClassEnter ( ccNode ) {
215
310
for ( const chars of iterateCharacterSequence ( ccNode . elements ) ) {
216
311
for ( const kind of kinds ) {
217
- if ( hasCharacterSequence [ kind ] ( chars ) ) {
218
- foundKinds . add ( kind ) ;
312
+ const matches = accumulate ( chars , characterSequenceIndexFilters [ kind ] ) ;
313
+
314
+ if ( foundKindMatches . has ( kind ) ) {
315
+ foundKindMatches . get ( kind ) . push ( ...matches ) ;
316
+ } else {
317
+ foundKindMatches . set ( kind , matches ) ;
219
318
}
319
+
220
320
}
221
321
}
222
322
}
223
323
} ) ;
224
324
225
- for ( const kind of foundKinds ) {
325
+ for ( const [ kind , matches ] of foundKindMatches ) {
226
326
let suggest ;
227
327
228
328
if ( kind === "surrogatePairWithoutUFlag" ) {
@@ -232,11 +332,27 @@ module.exports = {
232
332
} ] ;
233
333
}
234
334
235
- context . report ( {
236
- node,
237
- messageId : kind ,
238
- suggest
239
- } ) ;
335
+ const locs = getNodeReportLocations ( matches , node ) ;
336
+
337
+ // Grapheme zero-width joiners (e.g. in 👨👩👦) visually show as one emoji
338
+ if ( kind === "zwj" && locs . length > 1 ) {
339
+ context . report ( {
340
+ loc : {
341
+ start : locs [ 0 ] . start ,
342
+ end : locs [ 1 ] . end
343
+ } ,
344
+ messageId : kind ,
345
+ suggest
346
+ } ) ;
347
+ } else {
348
+ for ( const loc of locs ) {
349
+ context . report ( {
350
+ loc,
351
+ messageId : kind ,
352
+ suggest
353
+ } ) ;
354
+ }
355
+ }
240
356
}
241
357
}
242
358
@@ -267,7 +383,7 @@ module.exports = {
267
383
const flags = getStringIfConstant ( flagsNode , scope ) ;
268
384
269
385
if ( typeof pattern === "string" ) {
270
- verify ( refNode , pattern , flags || "" , fixer => {
386
+ verify ( patternNode , pattern , flags || "" , fixer => {
271
387
272
388
if ( ! isValidWithUnicodeFlag ( context . languageOptions . ecmaVersion , pattern ) ) {
273
389
return null ;
0 commit comments