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 *
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 *
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 *
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 *
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 *
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