1 /*
<lambda>null2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 @file:Suppress("UnstableApiUsage")
18 
19 package androidx.constraintlayout.compose.lint
20 
21 import com.android.tools.lint.client.api.UElementHandler
22 import com.android.tools.lint.detector.api.Category
23 import com.android.tools.lint.detector.api.Detector
24 import com.android.tools.lint.detector.api.Implementation
25 import com.android.tools.lint.detector.api.Issue
26 import com.android.tools.lint.detector.api.JavaContext
27 import com.android.tools.lint.detector.api.LintFix
28 import com.android.tools.lint.detector.api.Scope
29 import com.android.tools.lint.detector.api.Severity
30 import com.android.tools.lint.detector.api.SourceCodeScanner
31 import com.android.tools.lint.detector.api.UastLintUtils.Companion.tryResolveUDeclaration
32 import java.util.EnumSet
33 import org.jetbrains.kotlin.psi.KtDestructuringDeclaration
34 import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
35 import org.jetbrains.uast.UBinaryExpression
36 import org.jetbrains.uast.UBlockExpression
37 import org.jetbrains.uast.UCallExpression
38 import org.jetbrains.uast.UElement
39 import org.jetbrains.uast.UExpression
40 import org.jetbrains.uast.ULambdaExpression
41 import org.jetbrains.uast.UQualifiedReferenceExpression
42 import org.jetbrains.uast.USimpleNameReferenceExpression
43 import org.jetbrains.uast.getContainingUClass
44 import org.jetbrains.uast.getParentOfType
45 import org.jetbrains.uast.visitor.UastVisitor
46 
47 private const val CREATE_REFS_FOR_NAME = "createRefsFor"
48 private const val MATCH_PARENT_NAME = "matchParent"
49 private const val LINK_TO_NAME = "linkTo"
50 private const val CENTER_TO_NAME = "centerTo"
51 private const val CENTER_HORIZONTALLY_TO_NAME = "centerHorizontallyTo"
52 private const val CENTER_VERTICALLY_TO_NAME = "centerVerticallyTo"
53 private const val WIDTH_NAME = "width"
54 private const val HEIGHT_NAME = "height"
55 private const val PARENT_NAME = "parent"
56 private const val CONSTRAIN_NAME = "constrain"
57 private const val MARGIN_INDEX_IN_LINK_TO = 1
58 private const val GONE_MARGIN_INDEX_IN_LINK_TO = 2
59 private const val CREATE_HORIZONTAL_CHAIN_NAME = "createHorizontalChain"
60 private const val CREATE_VERTICAL_CHAIN_NAME = "createVerticalChain"
61 private const val WITH_CHAIN_PARAMS_NAME = "withChainParams"
62 private const val CHAIN_PARAM_START_MARGIN_NAME = "startMargin"
63 private const val CHAIN_PARAM_TOP_MARGIN_NAME = "topMargin"
64 private const val CHAIN_PARAM_END_MARGIN_NAME = "endMargin"
65 private const val CHAIN_PARAM_BOTTOM_MARGIN_NAME = "bottomMargin"
66 private const val CHAIN_PARAM_START_GONE_MARGIN_NAME = "startGoneMargin"
67 private const val CHAIN_PARAM_TOP_GONE_MARGIN_NAME = "topGoneMargin"
68 private const val CHAIN_PARAM_END_GONE_MARGIN_NAME = "endGoneMargin"
69 private const val CHAIN_PARAM_BOTTOM_GONE_MARGIN_NAME = "bottomGoneMargin"
70 
71 private const val CL_COMPOSE_DIMENSION_CLASS_NAME = "Dimension"
72 
73 private const val DIMENSION_MATCH_PARENT_EXPRESSION_NAME =
74     "$CL_COMPOSE_DIMENSION_CLASS_NAME.$MATCH_PARENT_NAME"
75 
76 private const val CL_COMPOSE_PACKAGE = "androidx.constraintlayout.compose"
77 private const val CONSTRAINT_SET_SCOPE_CLASS_FQ = "$CL_COMPOSE_PACKAGE.ConstraintSetScope"
78 private const val MOTION_SCENE_SCOPE_CLASS_FQ = "$CL_COMPOSE_PACKAGE.MotionSceneScope"
79 private const val CONSTRAIN_SCOPE_CLASS_FQ = "$CL_COMPOSE_PACKAGE.ConstrainScope"
80 private const val CONSTRAINED_LAYOUT_REFERENCE_CLASS_FQ =
81     "$CL_COMPOSE_PACKAGE.ConstrainedLayoutReference"
82 private const val LAYOUT_REFERENCE_CLASS_FQ = "$CL_COMPOSE_PACKAGE.LayoutReference"
83 
84 private val knownOwnersOfCreateRefsFor by
85     lazy(LazyThreadSafetyMode.NONE) {
86         setOf(CONSTRAINT_SET_SCOPE_CLASS_FQ, MOTION_SCENE_SCOPE_CLASS_FQ)
87     }
88 
89 private val horizontalConstraintAnchors by
<lambda>null90     lazy(LazyThreadSafetyMode.NONE) { setOf("start", "end", "absoluteLeft", "absoluteRight") }
91 
92 private val verticalConstraintAnchors by
<lambda>null93     lazy(LazyThreadSafetyMode.NONE) {
94         setOf(
95             "top",
96             "bottom",
97         )
98     }
99 
100 private val horizontalCenterMethodNames by
<lambda>null101     lazy(LazyThreadSafetyMode.NONE) {
102         setOf(
103             CENTER_TO_NAME,
104             CENTER_HORIZONTALLY_TO_NAME,
105         )
106     }
107 
108 private val verticalCenterMethodNames by
<lambda>null109     lazy(LazyThreadSafetyMode.NONE) {
110         setOf(
111             CENTER_TO_NAME,
112             CENTER_VERTICALLY_TO_NAME,
113         )
114     }
115 
116 class ConstraintLayoutDslDetector : Detector(), SourceCodeScanner {
117 
118     // TODO: Add a case to detect use cases for `ConstrainedLayoutReference.withChainParams()`
119 
getApplicableUastTypesnull120     override fun getApplicableUastTypes() =
121         listOf(UCallExpression::class.java, UBinaryExpression::class.java)
122 
123     override fun createUastHandler(context: JavaContext) =
124         object : UElementHandler() {
125 
126             /** Binary expressions are of the form `foo = bar`. */
127             override fun visitBinaryExpression(node: UBinaryExpression) {
128                 val assignedReferenceText = node.rightOperand.sourcePsi?.text ?: return
129 
130                 when (assignedReferenceText) {
131                     DIMENSION_MATCH_PARENT_EXPRESSION_NAME -> detectMatchParentUsage(node)
132                 }
133             }
134 
135             override fun visitCallExpression(node: UCallExpression) {
136                 when (node.methodName) {
137                     CREATE_REFS_FOR_NAME -> detectCreateRefsForUsage(node)
138                     CREATE_HORIZONTAL_CHAIN_NAME -> detectChainParamsUsage(node, true)
139                     CREATE_VERTICAL_CHAIN_NAME -> detectChainParamsUsage(node, false)
140                 // TODO: Detect that `withChainParams` is not called after chains are created
141                 }
142             }
143 
144             /**
145              * Verify correct usage of `Dimension.matchParent`.
146              *
147              * &nbsp;
148              *
149              * When using `Dimension.matchParent`, the user must be careful to not have custom
150              * constraints that result in different behavior from `centerTo(parent)`, otherwise,
151              * they should use `Dimension.percent(1f)` instead.
152              *
153              * ```
154              *  val (text, button) = createRefsFor("text", "button")
155              *  constrain(text) {
156              *      width = Dimension.matchParent
157              *
158              *      // Correct
159              *      start.linkTo(parent.start)
160              *      centerTo(parent)
161              *      centerHorizontallyTo(parent)
162              *
163              *      // Incorrect
164              *      start.linkTo(parent.end)
165              *      start.linkTo(button.start)
166              *      centerHorizontallyTo(button)
167              *      centerTo(button)
168              *  }
169              * ```
170              */
171             private fun detectMatchParentUsage(node: UBinaryExpression) {
172                 val assigneeNode = node.leftOperand
173                 val assigneeName = assigneeNode.sourcePsi?.text ?: return
174 
175                 // Must be assigned to either `width` or `height`
176                 val isHorizontal: Boolean =
177                     when (assigneeName) {
178                         WIDTH_NAME -> true
179                         HEIGHT_NAME -> false
180                         else -> return
181                     }
182 
183                 // Verify that the context of this Expression is within ConstrainScope
184                 if (
185                     assigneeNode.tryResolveUDeclaration()?.getContainingUClass()?.qualifiedName !=
186                         CONSTRAIN_SCOPE_CLASS_FQ
187                 ) {
188                     return
189                 }
190 
191                 val containingBlock = node.getParentOfType<UBlockExpression>() ?: return
192 
193                 // Within the Block, look for expressions supported for the check and immediately
194                 // return
195                 // if any of those expressions indicate bad usage of `Dimension.matchParent`
196                 val containsErrorProneUsage =
197                     containingBlock.expressions
198                         .asSequence()
199                         .mapNotNull { expression ->
200                             EvaluateableExpression.createForMatchParentUsage(
201                                 expression = expression,
202                                 isHorizontal = isHorizontal
203                             )
204                         }
205                         .any(EvaluateableExpression::isErrorProneForMatchParentUsage)
206 
207                 if (!containsErrorProneUsage) {
208                     return
209                 }
210 
211                 val overrideMethodName =
212                     if (isHorizontal) CENTER_HORIZONTALLY_TO_NAME else CENTER_VERTICALLY_TO_NAME
213 
214                 context.report(
215                     issue = IncorrectMatchParentUsageIssue,
216                     scope = node.rightOperand,
217                     location = context.getNameLocation(node.rightOperand),
218                     message =
219                         "`Dimension.matchParent` will override constraints to an equivalent of " +
220                             "`$overrideMethodName(parent)`.\nUse `Dimension.percent(1f)` to respect " +
221                             "constraints.",
222                     quickfixData =
223                         LintFix.create()
224                             .replace()
225                             .name("Replace `matchParent` with `percent(1f)`.")
226                             .range(context.getNameLocation(node.rightOperand))
227                             .all()
228                             .with("Dimension.percent(1f)")
229                             .autoFix()
230                             .build()
231                 )
232             }
233 
234             /**
235              * Verify correct usage of `createRefsFor("a", "b", "c")`.
236              *
237              * &nbsp;
238              *
239              * The number of assigned variables should match the number of given arguments:
240              * ```
241              * // Correct
242              * val (a, b, c) = createRefsFor("a", "b", "c")
243              *
244              * // Incorrect: Fewer variables than arguments
245              * val (a) = createRefsFor("a", "b", "c")
246              *
247              * // Incorrect: More variables than arguments
248              * val (a, b, c, d) = createRefsFor("a", "b", "c")
249              *
250              * ```
251              */
252             private fun detectCreateRefsForUsage(node: UCallExpression) {
253                 val destructuringDeclarationElement =
254                     node.sourcePsi?.getParentOfType<KtDestructuringDeclaration>(true) ?: return
255 
256                 val argsGiven = node.valueArgumentCount
257                 val varsReceived = destructuringDeclarationElement.entries.size
258                 if (argsGiven == varsReceived) {
259                     // Ids provided to call match the variables assigned, no issue
260                     return
261                 }
262 
263                 // Verify that arguments are Strings, we can't check for correctness if the argument
264                 // is
265                 // an array: `val (text1, text2) = createRefsFor(*iDsArray)`
266                 node.valueArguments.forEach { argExpression ->
267                     if (
268                         argExpression.getExpressionType()?.canonicalText != String::class.java.name
269                     ) {
270                         return
271                     }
272                 }
273 
274                 // Element resolution is relatively expensive, do last
275                 val classOwnerFqName = node.resolve()?.containingClass?.qualifiedName ?: return
276 
277                 // Make sure the method corresponds to an expected class
278                 if (!knownOwnersOfCreateRefsFor.contains(classOwnerFqName)) {
279                     return
280                 }
281 
282                 context.report(
283                     issue = IncorrectReferencesDeclarationIssue,
284                     scope = node,
285                     location = context.getNameLocation(node),
286                     message =
287                         "Arguments of `$CREATE_REFS_FOR_NAME` ($argsGiven) do not match " +
288                             "assigned variables ($varsReceived)"
289                 )
290             }
291 
292             /**
293              * Verify that margins for chains are applied correctly.
294              *
295              * &nbsp;
296              *
297              * Margins for elements in chains should be applied with
298              * `LayoutReference.withChainParams()` instinctively, users may want to create
299              * constraints with margins that mimic the chain behavior expecting those margins to be
300              * reflected in the chain. But that is not the correct way to do it.
301              *
302              * So this check detects when users create chain-like constraints, and suggests to use
303              * `withChainParams()` and delete conflicting constraints, keeping the intended margins.
304              *
305              * &nbsp;
306              *
307              * Example:
308              *
309              * Before
310              *
311              * ```
312              * val (button, text) = createRefs()
313              * createHorizontalChain(button, text)
314              *
315              * constrain(button) {
316              *  end.linkTo(text.start, 8.dp)
317              *  end.linkTo(text.start, goneMargin = 16.dp)
318              * }
319              * ```
320              *
321              * After
322              *
323              * ```
324              * val (button, text) = createRefs()
325              * createHorizontalChain(button.withChainParams(endMargin = 8.dp, endGoneMargin = 16.dp), text)
326              *
327              * constrain(button) {
328              * }
329              * ```
330              */
331             private fun detectChainParamsUsage(node: UCallExpression, isHorizontal: Boolean) {
332                 // TODO(b/268213648): Don't attempt to fix chain elements that already have
333                 //  `withChainParams` that may have been defined elsewhere out of scope. A safe
334                 //  path to take would be to look for the layout reference declaration, and skip
335                 // this
336                 //  check if it cannot be found within the current scope (code block). We could also
337                 // try
338                 //  to search within the shared scope of the layout reference and chain
339                 // declarations,
340                 //  but there's no straight-forward way to do it.
341 
342                 val containingBlock = node.getParentOfType<UBlockExpression>() ?: return
343 
344                 var previousNode: ChainNode? = null
345                 val chainNodes =
346                     node.valueArguments.filter(UExpression::isOfLayoutReferenceType).mapNotNull {
347                         argumentExpression ->
348                         argumentExpression.findChildIdentifier()?.let { identifier ->
349                             val chainNode =
350                                 ChainNode(
351                                     expression = identifier,
352                                     hasChainParams =
353                                         argumentExpression is UQualifiedReferenceExpression ||
354                                             containingBlock.isChainParamsCalledInIdentifier(
355                                                 identifier
356                                             )
357                                 )
358                             previousNode?.let { prevNode ->
359                                 chainNode.prev = prevNode
360                                 prevNode.next = chainNode
361                             }
362                             previousNode = chainNode
363                             chainNode
364                         }
365                     }
366 
367                 val resolvedChainLikeConstraintsPerNode =
368                     chainNodes.map {
369                         if (it.hasChainParams) {
370                             emptyList()
371                         } else {
372                             findChainLikeConstraints(containingBlock, it, isHorizontal)
373                         }
374                     }
375                 resolvedChainLikeConstraintsPerNode.forEachIndexed { index, chainLikeExpressions ->
376                     val chainParamsBuilder = ChainParamsMethodBuilder()
377                     val removeLinkToFixes =
378                         chainLikeExpressions.map { resolvedExpression ->
379                             resolvedExpression.marginExpression?.let {
380                                 chainParamsBuilder.append(
381                                     resolvedExpression.marginParamName,
382                                     resolvedExpression.marginExpression
383                                 )
384                             }
385                             resolvedExpression.marginGoneExpression?.let {
386                                 chainParamsBuilder.append(
387                                     resolvedExpression.marginGoneParamName,
388                                     resolvedExpression.marginGoneExpression
389                                 )
390                             }
391                             val expressionToDelete =
392                                 resolvedExpression.fullExpression.getParentOfType<
393                                     UQualifiedReferenceExpression
394                                 >()
395                             LintFix.create()
396                                 .replace()
397                                 .name("Remove conflicting `linkTo` declaration.")
398                                 .range(context.getLocation(expressionToDelete))
399                                 .all()
400                                 .with("")
401                                 .autoFix()
402                                 .build()
403                         }
404                     val chainNode = chainNodes[index]
405                     if (!chainParamsBuilder.isEmpty() && removeLinkToFixes.isNotEmpty()) {
406                         context.report(
407                             issue = IncorrectChainMarginsUsageIssue,
408                             scope = node,
409                             location = context.getLocation(chainNode.expression.sourcePsi),
410                             message =
411                                 "Margins for elements in a Chain should be applied with " +
412                                     "`LayoutReference.withChainParams(...)`.",
413                             quickfixData =
414                                 LintFix.create()
415                                     .composite()
416                                     .name(
417                                         "Add `.withChainParams(...)` and remove " +
418                                             "(${removeLinkToFixes.size}) conflicting `linkTo` declarations."
419                                     )
420                                     // `join` might overwrite previously added fixes, so add grouped
421                                     // fixes
422                                     // first, then, add the remaining fixes individually with `add`
423                                     .join(*removeLinkToFixes.toTypedArray())
424                                     .add(
425                                         LintFix.create()
426                                             .replace()
427                                             .name("Add `.withChainParams(...)`.")
428                                             .range(
429                                                 context.getLocation(chainNode.expression.sourcePsi)
430                                             )
431                                             .end()
432                                             .with(chainParamsBuilder.build())
433                                             .autoFix()
434                                             .build()
435                                     )
436                                     .build()
437                         )
438                     }
439                 }
440             }
441         }
442 
443     companion object {
444         val IncorrectReferencesDeclarationIssue =
445             Issue.create(
446                 id = "IncorrectReferencesDeclaration",
447                 briefDescription =
448                     "`$CREATE_REFS_FOR_NAME(vararg ids: Any)` should have at least one" +
449                         " argument and match assigned variables",
450                 explanation =
451                     "`$CREATE_REFS_FOR_NAME(vararg ids: Any)` conveniently allows creating " +
452                         "multiple references using destructuring. However, providing an un-equal amount " +
453                         "of arguments to the assigned variables will result in unexpected behavior since" +
454                         " the variables may reference a ConstrainedLayoutReference with unknown ID.",
455                 category = Category.CORRECTNESS,
456                 priority = 5,
457                 severity = Severity.ERROR,
458                 implementation =
459                     Implementation(
460                         ConstraintLayoutDslDetector::class.java,
461                         EnumSet.of(Scope.JAVA_FILE)
462                     )
463             )
464 
465         val IncorrectMatchParentUsageIssue =
466             Issue.create(
467                 id = "IncorrectMatchParentUsage",
468                 briefDescription =
469                     "Prefer using `Dimension.percent(1f)` when defining custom " + "constraints.",
470                 explanation =
471                     "`Dimension.matchParent` forces the constraints to be an equivalent of " +
472                         "`centerHorizontallyTo(parent)` or `centerVerticallyTo(parent)` according to the " +
473                         "assigned dimension which can lead to unexpected behavior. To avoid that, prefer " +
474                         "using `Dimension.percent(1f)`",
475                 category = Category.CORRECTNESS,
476                 priority = 5,
477                 severity = Severity.WARNING,
478                 implementation =
479                     Implementation(
480                         ConstraintLayoutDslDetector::class.java,
481                         EnumSet.of(Scope.JAVA_FILE)
482                     )
483             )
484 
485         val IncorrectChainMarginsUsageIssue =
486             Issue.create(
487                 id = "IncorrectChainMarginsUsage",
488                 briefDescription =
489                     "Use `LayoutReference.withChainParams()` to define margins for " +
490                         "elements in a Chain.",
491                 explanation =
492                     "If you understand how a chain works, it might seem obvious to add " +
493                         "margins by re-creating the constraints with the desired margin. However, in " +
494                         "Compose, helpers will ignore custom constraints in favor of their layout " +
495                         "implementation. So instead, use `LayoutReference.withChainParams()` " +
496                         "to define margins for Chains.",
497                 category = Category.CORRECTNESS,
498                 priority = 5,
499                 severity = Severity.WARNING,
500                 implementation =
501                     Implementation(
502                         ConstraintLayoutDslDetector::class.java,
503                         EnumSet.of(Scope.JAVA_FILE)
504                     )
505             )
506     }
507 }
508 
509 internal class EvaluateableExpression(
510     private val expectedArgumentText: String,
511     private val expression: UCallExpression
512 ) {
513     /**
514      * Should only return `true` when we know for certain that there's wrong usage.
515      *
516      * &nbsp;
517      *
518      * E.g.: For the following snippet we can't know if usage is incorrect since we don't know what
519      * the variable `targetAnchor` represents.
520      *
521      * ```
522      * width = Dimension.matchParent
523      *
524      * var targetAnchor: HorizontalAnchor
525      * start.linkTo(targetAnchor)
526      * ```
527      */
isErrorProneForMatchParentUsagenull528     fun isErrorProneForMatchParentUsage(): Boolean {
529         val argumentText = expression.valueArguments.firstOrNull()?.sourcePsi?.text ?: return false
530         return argumentText != expectedArgumentText
531     }
532 
533     companion object {
createForMatchParentUsagenull534         fun createForMatchParentUsage(
535             expression: UExpression,
536             isHorizontal: Boolean
537         ): EvaluateableExpression? {
538             if (expression is UQualifiedReferenceExpression) {
539                 // For the form of `start.linkTo(parent.start)`
540 
541                 val callExpression = (expression.selector as? UCallExpression) ?: return null
542                 if (callExpression.methodName != LINK_TO_NAME) {
543                     return null
544                 }
545                 val receiverAnchorName = expression.receiver.sourcePsi?.text ?: return null
546                 val supportedAnchors =
547                     if (isHorizontal) horizontalConstraintAnchors else verticalConstraintAnchors
548                 if (!supportedAnchors.contains(receiverAnchorName)) {
549                     return null
550                 }
551                 return EvaluateableExpression(
552                     expectedArgumentText = "$PARENT_NAME.$receiverAnchorName",
553                     expression = callExpression
554                 )
555             } else if (expression is UCallExpression) {
556                 // For the form of `centerTo(parent)`
557 
558                 val supportedMethodNames =
559                     if (isHorizontal) horizontalCenterMethodNames else verticalCenterMethodNames
560                 val methodName = expression.methodName ?: return null
561                 if (!supportedMethodNames.contains(methodName)) {
562                     return null
563                 }
564                 return EvaluateableExpression(
565                     expectedArgumentText = PARENT_NAME,
566                     expression = expression
567                 )
568             }
569             return null
570         }
571     }
572 }
573 
findChainLikeConstraintsnull574 internal fun findChainLikeConstraints(
575     constraintSetBlock: UBlockExpression,
576     chainNode: ChainNode,
577     isHorizontal: Boolean
578 ): List<ResolvedChainLikeExpression> {
579     val identifier = chainNode.expression
580     val constrainTargetExpressions =
581         constraintSetBlock.expressions.filter { cSetExpression ->
582             cSetExpression is UCallExpression &&
583                 cSetExpression.methodName == CONSTRAIN_NAME &&
584                 cSetExpression.valueArguments.any { argument ->
585                     argument.sourcePsi?.text == identifier.identifier
586                 }
587         }
588 
589     val expectedAnchors =
590         if (isHorizontal) horizontalConstraintAnchors else verticalConstraintAnchors
591 
592     return constrainTargetExpressions
593         .asSequence()
594         .mapNotNull { constrainExpression ->
595             (constrainExpression as? UCallExpression)
596                 ?.valueArguments
597                 ?.filterIsInstance<ULambdaExpression>()
598                 ?.lastOrNull()
599                 ?.body as? UBlockExpression
600         }
601         .flatMap { it.expressions }
602         .filterIsInstance<UQualifiedReferenceExpression>()
603         .map { it.selector }
604         .filterIsInstance<UCallExpression>()
605         .filter {
606             // No point in considering it if there's no margins applied
607             it.methodName == LINK_TO_NAME && it.valueArgumentCount >= 2
608         }
609         .mapNotNull {
610             it.receiver?.sourcePsi?.text?.let { anchorName ->
611                 if (expectedAnchors.contains(anchorName)) {
612                     Pair(it, anchorName)
613                 } else {
614                     null
615                 }
616             }
617         }
618         .mapNotNull { (linkCallExpression, anchorName) ->
619             val nextIdentifier = chainNode.next?.expression?.identifier
620             val isNextParent = nextIdentifier == null
621 
622             val prevIdentifier = chainNode.prev?.expression?.identifier
623             val isPrevParent = prevIdentifier == null
624             val expectedNextAnchorTo =
625                 if (isNextParent) {
626                     "parent.$anchorName"
627                 } else {
628                     "${nextIdentifier!!}.${anchorName.getOppositeAnchorName()}"
629                 }
630             val expectedPrevAnchorTo =
631                 if (isPrevParent) {
632                     "parent.$anchorName"
633                 } else {
634                     "${prevIdentifier!!}.${anchorName.getOppositeAnchorName()}"
635                 }
636 
637             val targetAnchorExpressionText = linkCallExpression.valueArguments[0].sourcePsi?.text
638             if (
639                 targetAnchorExpressionText == expectedPrevAnchorTo ||
640                     targetAnchorExpressionText == expectedNextAnchorTo
641             ) {
642                 ResolvedChainLikeExpression(
643                     linkCallExpression,
644                     anchorName,
645                     linkCallExpression.getArgumentForParameter(MARGIN_INDEX_IN_LINK_TO),
646                     linkCallExpression.getArgumentForParameter(GONE_MARGIN_INDEX_IN_LINK_TO)
647                 )
648             } else {
649                 null
650             }
651         }
652         .toList()
653 }
654 
655 internal class ChainNode(
656     val expression: USimpleNameReferenceExpression,
657     val hasChainParams: Boolean
658 ) {
659     var prev: ChainNode? = null
660     var next: ChainNode? = null
661 }
662 
663 internal class ResolvedChainLikeExpression(
664     val fullExpression: UCallExpression,
665     anchorName: String,
666     val marginExpression: UExpression?,
667     val marginGoneExpression: UExpression?
668 ) {
669     val marginParamName: String = anchorName.asChainParamsArgument(false)
670     val marginGoneParamName: String = anchorName.asChainParamsArgument(true)
671 }
672 
getOppositeAnchorNamenull673 private fun String.getOppositeAnchorName() =
674     when (this) {
675         "start" -> "end"
676         "end" -> "start"
677         "absoluteLeft" -> "absoluteRight"
678         "absoluteRight" -> "absoluteLeft"
679         "top" -> "bottom"
680         "bottom" -> "top"
681         else -> "start"
682     }
683 
asChainParamsArgumentnull684 internal fun String.asChainParamsArgument(isGone: Boolean = false) =
685     if (!isGone) {
686         when (this) {
687             "absoluteLeft",
688             "start" -> CHAIN_PARAM_START_MARGIN_NAME
689             "absoluteRight",
690             "end" -> CHAIN_PARAM_END_MARGIN_NAME
691             "top" -> CHAIN_PARAM_TOP_MARGIN_NAME
692             "bottom" -> CHAIN_PARAM_BOTTOM_MARGIN_NAME
693             else -> CHAIN_PARAM_START_MARGIN_NAME
694         }
695     } else {
696         when (this) {
697             "absoluteLeft",
698             "start" -> CHAIN_PARAM_START_GONE_MARGIN_NAME
699             "absoluteRight",
700             "end" -> CHAIN_PARAM_END_GONE_MARGIN_NAME
701             "top" -> CHAIN_PARAM_TOP_GONE_MARGIN_NAME
702             "bottom" -> CHAIN_PARAM_BOTTOM_GONE_MARGIN_NAME
703             else -> CHAIN_PARAM_START_GONE_MARGIN_NAME
704         }
705     }
706 
isChainParamsCalledInIdentifiernull707 internal fun UBlockExpression.isChainParamsCalledInIdentifier(
708     target: USimpleNameReferenceExpression
709 ): Boolean {
710     var found = false
711     this.accept(
712         object : UastVisitor {
713             override fun visitQualifiedReferenceExpression(
714                 node: UQualifiedReferenceExpression
715             ): Boolean {
716                 val identifier = (node.receiver as? USimpleNameReferenceExpression) ?: return true
717                 if (
718                     identifier.identifier == target.identifier &&
719                         identifier.getExpressionType() == target.getExpressionType()
720                 ) {
721                     val selector = node.selector
722                     if (
723                         selector is UCallExpression && selector.methodName == WITH_CHAIN_PARAMS_NAME
724                     ) {
725                         found = true
726                     } else {
727                         // skip
728                         return true
729                     }
730                 } else {
731                     // skip
732                     return true
733                 }
734                 return super.visitQualifiedReferenceExpression(node)
735             }
736 
737             override fun visitElement(node: UElement): Boolean {
738                 return found
739             }
740         }
741     )
742     return found
743 }
744 
745 internal class ChainParamsMethodBuilder {
746     private val modificationMap = mutableMapOf<String, UExpression>()
747 
appendnull748     fun append(paramName: String, paramExpression: UExpression) {
749         modificationMap[paramName] = paramExpression
750     }
751 
isEmptynull752     fun isEmpty() = modificationMap.isEmpty()
753 
754     fun build(): String =
755         StringBuilder()
756             .apply {
757                 append('.')
758                 append(WITH_CHAIN_PARAMS_NAME)
759                 append('(')
760                 modificationMap.forEach { (paramName, uExpression) ->
761                     uExpression.sourcePsi?.text?.let {
762                         append("$paramName = $it")
763                         append(", ")
764                     }
765                 }
766                 deleteCharAt(this.lastIndex)
767                 deleteCharAt(this.lastIndex)
768                 append(')')
769             }
770             .toString()
771 }
772 
findChildIdentifiernull773 internal fun UExpression.findChildIdentifier(): USimpleNameReferenceExpression? {
774     var identifier: USimpleNameReferenceExpression? = null
775     this.accept(
776         object : UastVisitor {
777             override fun visitSimpleNameReferenceExpression(
778                 node: USimpleNameReferenceExpression
779             ): Boolean {
780                 if (node.isOfLayoutReferenceType()) {
781                     identifier = node
782                 }
783                 return true
784             }
785 
786             // Only supported element to visit recursively, for the form of
787             // `textRef.withChainParams()`
788             override fun visitQualifiedReferenceExpression(
789                 node: UQualifiedReferenceExpression
790             ): Boolean = false
791 
792             override fun visitElement(node: UElement): Boolean = true
793         }
794     )
795     return identifier
796 }
797 
798 /**
799  * Simple way to check if the Reference has a supported LayoutReference type. Note it does not do
800  * expression resolution and takes the type as is. So we have to manually check for inheritors of
801  * LayoutReference.
802  */
isOfLayoutReferenceTypenull803 internal fun UExpression.isOfLayoutReferenceType(): Boolean {
804     val typeName = this.getExpressionType()?.canonicalText ?: return false
805     return typeName == CONSTRAINED_LAYOUT_REFERENCE_CLASS_FQ ||
806         typeName == LAYOUT_REFERENCE_CLASS_FQ
807 }
808