Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use kotlinx-metadata for more accurate Kotlin checks #75

Merged
merged 5 commits into from
May 29, 2023
Merged

Conversation

ZacSweers
Copy link
Collaborator

Resolves #53

This implements a new decorating SlackJavaEvaluator that leverages Kotlin Metadata to understand kotlin langauge features of externally compiled elements. This is important as these elements otherwise appear to lint as just plain Java classes. I've done a bunch of testing with this in the Slack Android repo, and the timings are always ~1ms, rarely exceeding it to 2ms. Just in case though, I've gated this behavior with a system property.

Currently this is only used in the DoNotMock checkers, but is usable in other future checkers.

@ZacSweers ZacSweers requested a review from kierse May 29, 2023 15:50
Copy link
Contributor

@kierse kierse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple minor nits


/** Flag to disable as needed. */
private val checkMetadata = System.getProperty("slack.lint.checkMetadata", "true").toBoolean()
private val cachedClasses = ConcurrentHashMap<String, Optional<KmClass>>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need Optional here? Couldn't you just use KmClass?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #73 🙃

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do want presence modeled because if it's empty/null, we can also use that as signal to know that we already looked and there was none

Comment on lines +72 to +129
// region Delegating functions
override val dependencies: LintModelDependencies?
get() = delegate.dependencies

override fun extendsClass(cls: PsiClass?, className: String, strict: Boolean): Boolean =
delegate.extendsClass(cls, className, strict)

override fun findAnnotation(
listOwner: PsiModifierListOwner?,
vararg annotationNames: String
): PsiAnnotation? = delegate.findAnnotation(listOwner, *annotationNames)

override fun findAnnotationInHierarchy(
listOwner: PsiModifierListOwner,
vararg annotationNames: String
): PsiAnnotation? = delegate.findAnnotationInHierarchy(listOwner, *annotationNames)

override fun findClass(qualifiedName: String): PsiClass? = delegate.findClass(qualifiedName)

override fun findJarPath(element: PsiElement): String? = delegate.findJarPath(element)

override fun findJarPath(element: UElement): String? = delegate.findJarPath(element)

override fun getAllAnnotations(
owner: PsiModifierListOwner,
inHierarchy: Boolean
): Array<PsiAnnotation> = delegate.getAllAnnotations(owner, inHierarchy)

override fun getAllAnnotations(owner: UAnnotated, inHierarchy: Boolean): List<UAnnotation> =
delegate.getAllAnnotations(owner, inHierarchy)

override fun getAnnotation(
listOwner: PsiModifierListOwner?,
vararg annotationNames: String
): UAnnotation? = delegate.getAnnotation(listOwner, *annotationNames)

override fun getAnnotationInHierarchy(
listOwner: PsiModifierListOwner,
vararg annotationNames: String
): UAnnotation? = delegate.getAnnotationInHierarchy(listOwner, *annotationNames)

override fun getAnnotations(
owner: PsiModifierListOwner?,
inHierarchy: Boolean,
parent: UElement?
): List<UAnnotation> = delegate.getAnnotations(owner, inHierarchy, parent)

override fun getClassType(psiClass: PsiClass?): PsiClassType? = delegate.getClassType(psiClass)

override fun getPackage(node: PsiElement): PsiPackage? = delegate.getPackage(node)

override fun getPackage(node: UElement): PsiPackage? = delegate.getPackage(node)

override fun getTypeClass(psiType: PsiType?): PsiClass? = delegate.getTypeClass(psiType)

override fun implementsInterface(cls: PsiClass, interfaceName: String, strict: Boolean): Boolean =
delegate.implementsInterface(cls, interfaceName, strict)
// endregion
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These all look to be calling straight through to the JavaEvaluator. Did you consider delegating to the delegate (in the constructor) so that none of this is needed?

class SlackJavaEvaluator(private val file: String, private val delegate: JavaEvaluator) : JavaEvaluator() by delegate {
  // first override
  override fun hasModifier(...) { ... }
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't do class delegation with classes, only interfaces :/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well that's unfortunate :-\

// Don't use getOrPut. Kotlin's extension may still invoke the body and we don't want that
.computeIfAbsent(qualifiedName!!) { key ->
val annotation =
cls.findAnnotation("kotlin.Metadata") ?: return@computeIfAbsent Optional.empty()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. It seems like you should be able to return@computeIfAbsent null here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dislike returns with labels and do my best to avoid them when possible (I think they negatively affect readability). It isn't a huge deal (I understand not everyone feels this way), but did you consider something like this:

var metadata = cachedClasses[qualifiedName!!]
if (metadata == null) {
  val annotation = cls.findAnnotation("kotlin.Metadata") ?: return null
  val (durationMillis, result) = measureTimeMillisWithResult { annotation.parseMetadata(key) }
  slackLintLog("Took ${durationMillis}ms to parse metadata for $key.")
  metadata = result
}

return metadata

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above for the nullability issue

For computeIfAbsent - we have to use it because it guarantees the currency safety and that the block is only computed once. The code in your snippet is actually more or less what kotlin's ConcurrentHashmap.getOrPut does, which is actually racy and could call the computation block more than once. See the comment above this call too :P

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh yes 👍 . I read the docs for getOrPut but didn't think to look at the docs for computeIfAbsent

Comment on lines 186 to 196
return when (metadata) {
is KotlinClassMetadata.Class -> metadata.toKmClass()
is KotlinClassMetadata.FileFacade -> null
is KotlinClassMetadata.MultiFileClassFacade -> null
is KotlinClassMetadata.MultiFileClassPart -> null
is KotlinClassMetadata.SyntheticClass -> null
is KotlinClassMetadata.Unknown -> null
null -> {
slackLintLog("Could not load metadata for $classNameHint from file $file")
null
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot going on in this method. Consider extracting this when to a helper

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaned it up in 26cdd85

Copy link
Contributor

@kierse kierse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple minor nits/suggestions

@ZacSweers ZacSweers enabled auto-merge May 29, 2023 17:44
@ZacSweers ZacSweers added this pull request to the merge queue May 29, 2023
Merged via the queue into main with commit 2ad70a1 May 29, 2023
@ZacSweers ZacSweers deleted the z/metadata branch May 29, 2023 17:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

DoNotMock false negatives
2 participants