@@ -42,6 +42,7 @@ export const rule: Rule.RuleModule = {
4242 create ( context : Rule . RuleContext ) {
4343 const services = context . sourceCode . parserServices ;
4444 const canResolveType = isRequiredParserServices ( services ) ;
45+ const COMPARISON_OPERATORS = [ '==' , '!=' , '===' , '!==' , '<' , '<=' , '>' , '>=' ] as const ;
4546
4647 function checkArguments ( functionCall : estree . CallExpression ) {
4748 // Extract argument names first (cheap operation)
@@ -84,7 +85,8 @@ export const rule: Rule.RuleModule = {
8485 swappedArgumentName &&
8586 ! areComparedArguments ( [ argumentName , swappedArgumentName ] , functionCall ) &&
8687 ! isIntentionalComparatorReversal ( functionCall , argumentName , swappedArgumentName ) &&
87- ! isInDirectionalContext ( functionCall )
88+ ! isInDirectionalContext ( functionCall ) &&
89+ ! isIntentionalTernarySwap ( functionCall , argumentName , swappedArgumentName )
8890 ) {
8991 raiseIssue ( argumentName , swappedArgumentName , functionDeclaration , functionCall ) ;
9092 return ;
@@ -121,7 +123,7 @@ export const rule: Rule.RuleModule = {
121123 switch ( test . type ) {
122124 case 'BinaryExpression' : {
123125 const binExpr = test ;
124- if ( [ '==' , '!=' , '===' , '!==' , '<' , '<=' , '>' , '>=' ] . includes ( binExpr . operator ) ) {
126+ if ( ( COMPARISON_OPERATORS as readonly string [ ] ) . includes ( binExpr . operator ) ) {
125127 const { left : lhs , right : rhs } = binExpr ;
126128 return checkComparedArguments ( lhs , rhs ) ;
127129 }
@@ -226,6 +228,100 @@ export const rule: Rule.RuleModule = {
226228 return false ;
227229 }
228230
231+ /**
232+ * Returns true when the detected argument swap is in one branch of a ConditionalExpression,
233+ * the other branch calls the same function with those arguments in the opposite (normal) order,
234+ * AND the ternary condition itself is a comparison that involves both of those same arguments.
235+ *
236+ * This ensures the condition is actually selecting the correct ordering rather than being an
237+ * unrelated boolean. For example:
238+ * `start < stop ? fn(start, stop) : fn(stop, start)` — suppressed (condition compares the pair)
239+ * `legacy ? fn(stop, start) : fn(start, stop)` — reported (condition unrelated to arg order)
240+ */
241+ function isIntentionalTernarySwap (
242+ functionCall : estree . CallExpression ,
243+ arg1Name : string ,
244+ arg2Name : string ,
245+ ) : boolean {
246+ const ancestors = context . sourceCode . getAncestors ( functionCall ) ;
247+ const parent = ancestors . at ( - 1 ) ;
248+
249+ if ( parent ?. type !== 'ConditionalExpression' ) {
250+ return false ;
251+ }
252+
253+ const conditional = parent ;
254+
255+ // Determine the "other" branch of the ternary
256+ let otherBranch : estree . Node | null = null ;
257+ if ( conditional . consequent === functionCall ) {
258+ otherBranch = conditional . alternate ;
259+ } else if ( conditional . alternate === functionCall ) {
260+ otherBranch = conditional . consequent ;
261+ }
262+
263+ if ( otherBranch ?. type !== 'CallExpression' ) {
264+ return false ;
265+ }
266+
267+ const otherCall = otherBranch as estree . CallExpression ;
268+
269+ // Both calls must target the same callee (by source text)
270+ if (
271+ context . sourceCode . getText ( functionCall . callee ) !==
272+ context . sourceCode . getText ( otherCall . callee )
273+ ) {
274+ return false ;
275+ }
276+
277+ if ( otherCall . arguments . length !== functionCall . arguments . length ) {
278+ return false ;
279+ }
280+
281+ // Find positions of arg1 and arg2 in the flagged call
282+ const args = functionCall . arguments ;
283+ const idx1 = args . findIndex ( a => a . type === 'Identifier' && a . name === arg1Name ) ;
284+ const idx2 = args . findIndex ( a => a . type === 'Identifier' && a . name === arg2Name ) ;
285+
286+ if ( idx1 < 0 || idx2 < 0 ) {
287+ return false ;
288+ }
289+
290+ // In the other branch, those same positions must carry arg2 and arg1 (reversed)
291+ const otherArgs = otherCall . arguments ;
292+ const otherAtIdx1 = otherArgs [ idx1 ] ;
293+ const otherAtIdx2 = otherArgs [ idx2 ] ;
294+
295+ if (
296+ ! (
297+ otherAtIdx1 ?. type === 'Identifier' &&
298+ otherAtIdx1 . name === arg2Name &&
299+ otherAtIdx2 ?. type === 'Identifier' &&
300+ otherAtIdx2 . name === arg1Name
301+ )
302+ ) {
303+ return false ;
304+ }
305+
306+ // The ternary condition must itself compare the same argument pair. This ties the
307+ // suppression to condition-controlled ordering (e.g. `a < b ? fn(a, b) : fn(b, a)`)
308+ // rather than an arbitrary boolean selector (e.g. `flag ? fn(b, a) : fn(a, b)`).
309+ const test = conditional . test ;
310+ if ( test . type !== 'BinaryExpression' ) {
311+ return false ;
312+ }
313+ if ( ! ( COMPARISON_OPERATORS as readonly string [ ] ) . includes ( test . operator ) ) {
314+ return false ;
315+ }
316+ const leftName = test . left . type === 'Identifier' ? test . left . name : undefined ;
317+ const rightName = test . right . type === 'Identifier' ? test . right . name : undefined ;
318+ if ( ! leftName || ! rightName ) {
319+ return false ;
320+ }
321+ const conditionNames = new Set ( [ leftName , rightName ] ) ;
322+ return conditionNames . has ( arg1Name ) && conditionNames . has ( arg2Name ) ;
323+ }
324+
229325 function resolveFunctionDeclaration ( node : estree . CallExpression ) : FunctionSignature | null {
230326 if ( canResolveType ) {
231327 return resolveFromTSSignature ( node ) ;
0 commit comments