FIR checker: report violations related to API mode

This commit is contained in:
Tianyu Geng
2021-10-12 02:01:45 +03:00
committed by Mikhail Glukhikh
parent 52b247937e
commit c262f17d75
30 changed files with 298 additions and 222 deletions
@@ -381,6 +381,12 @@ object DIAGNOSTICS_LIST : DiagnosticList("FirErrors") {
val INAPPLICABLE_OPERATOR_MODIFIER by error<PsiElement>(PositioningStrategy.OPERATOR_MODIFIER) {
parameter<String>("message")
}
val NO_EXPLICIT_VISIBILITY_IN_API_MODE by error<KtDeclaration>(PositioningStrategy.DECLARATION_START_TO_NAME)
val NO_EXPLICIT_VISIBILITY_IN_API_MODE_WARNING by warning<KtDeclaration>(PositioningStrategy.DECLARATION_START_TO_NAME)
val NO_EXPLICIT_RETURN_TYPE_IN_API_MODE by error<KtDeclaration>(PositioningStrategy.DECLARATION_NAME)
val NO_EXPLICIT_RETURN_TYPE_IN_API_MODE_WARNING by warning<KtDeclaration>(PositioningStrategy.DECLARATION_NAME)
}
val INLINE_CLASSES by object : DiagnosticGroup("Inline classes") {
@@ -108,8 +108,9 @@ enum class PositioningStrategy(private val strategy: String? = null) {
ENUM_MODIFIER,
FIELD_KEYWORD,
TAILREC_MODIFIER,
IMPORT_ALIAS,
PROPERTY_DELEGATE,
IMPORT_ALIAS,
DECLARATION_START_TO_NAME,
;
@@ -284,6 +284,10 @@ object FirErrors {
val WRONG_MODIFIER_CONTAINING_DECLARATION by error2<PsiElement, KtModifierKeywordToken, String>()
val DEPRECATED_MODIFIER_CONTAINING_DECLARATION by warning2<PsiElement, KtModifierKeywordToken, String>()
val INAPPLICABLE_OPERATOR_MODIFIER by error1<PsiElement, String>(SourceElementPositioningStrategies.OPERATOR_MODIFIER)
val NO_EXPLICIT_VISIBILITY_IN_API_MODE by error0<KtDeclaration>(SourceElementPositioningStrategies.DECLARATION_START_TO_NAME)
val NO_EXPLICIT_VISIBILITY_IN_API_MODE_WARNING by warning0<KtDeclaration>(SourceElementPositioningStrategies.DECLARATION_START_TO_NAME)
val NO_EXPLICIT_RETURN_TYPE_IN_API_MODE by error0<KtDeclaration>(SourceElementPositioningStrategies.DECLARATION_NAME)
val NO_EXPLICIT_RETURN_TYPE_IN_API_MODE_WARNING by warning0<KtDeclaration>(SourceElementPositioningStrategies.DECLARATION_NAME)
// Inline classes
val INLINE_CLASS_NOT_TOP_LEVEL by error0<KtDeclaration>(SourceElementPositioningStrategies.INLINE_OR_VALUE_MODIFIER)
@@ -29,6 +29,7 @@ object CommonDeclarationCheckers : DeclarationCheckers() {
FirExpectActualDeclarationChecker,
FirInvalidAndDangerousCharactersChecker,
FirAmbiguousAnonymousTypeChecker,
FirExplicitApiDeclarationChecker,
)
override val functionCheckers: Set<FirFunctionChecker>
@@ -0,0 +1,146 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.fir.analysis.checkers.syntax
import org.jetbrains.kotlin.KtNodeTypes
import org.jetbrains.kotlin.config.AnalysisFlags
import org.jetbrains.kotlin.config.ExplicitApiMode
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.fir.FirFakeSourceElementKind
import org.jetbrains.kotlin.fir.FirRealSourceElementKind
import org.jetbrains.kotlin.fir.FirSourceElement
import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext
import org.jetbrains.kotlin.fir.analysis.checkers.declaration.isLocalMember
import org.jetbrains.kotlin.fir.analysis.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors
import org.jetbrains.kotlin.fir.analysis.diagnostics.reportOn
import org.jetbrains.kotlin.fir.analysis.getChild
import org.jetbrains.kotlin.fir.declarations.*
import org.jetbrains.kotlin.fir.declarations.impl.FirPrimaryConstructor
import org.jetbrains.kotlin.fir.declarations.utils.effectiveVisibility
import org.jetbrains.kotlin.fir.declarations.utils.isData
import org.jetbrains.kotlin.fir.declarations.utils.isOverride
import org.jetbrains.kotlin.fir.languageVersionSettings
import org.jetbrains.kotlin.fir.resolve.transformers.publishedApiEffectiveVisibility
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtDeclaration
object FirExplicitApiDeclarationChecker : FirDeclarationSyntaxChecker<FirDeclaration, KtDeclaration>() {
private val codeFragmentTypes =
setOf(KtNodeTypes.BLOCK_CODE_FRAGMENT, KtNodeTypes.EXPRESSION_CODE_FRAGMENT, KtNodeTypes.TYPE_CODE_FRAGMENT)
override fun checkLightTree(element: FirDeclaration, source: FirSourceElement, context: CheckerContext, reporter: DiagnosticReporter) {
if ((source.kind !is FirRealSourceElementKind && source.kind != FirFakeSourceElementKind.PropertyFromParameter) ||
element !is FirMemberDeclaration
) {
return
}
val state = context.session.languageVersionSettings.getFlag(AnalysisFlags.explicitApiMode)
if (state == ExplicitApiMode.DISABLED) return
// Enum entries do not have visibilities
if (element is FirEnumEntry) return
if (!element.effectiveVisibility.publicApi && element.publishedApiEffectiveVisibility == null) return
checkVisibilityModifier(state, element, source, context, reporter)
checkExplicitReturnType(state, element, source, context, reporter)
}
private fun checkVisibilityModifier(
state: ExplicitApiMode,
declaration: FirMemberDeclaration,
source: FirSourceElement,
context: CheckerContext,
reporter: DiagnosticReporter
) {
val visibilityModifier = source.getChild(KtNodeTypes.MODIFIER_LIST)?.getChild(KtTokens.VISIBILITY_MODIFIERS)
if (visibilityModifier != null) return
if (explicitVisibilityIsNotRequired(declaration, context)) return
val factory = if (state == ExplicitApiMode.STRICT)
FirErrors.NO_EXPLICIT_VISIBILITY_IN_API_MODE
else
FirErrors.NO_EXPLICIT_VISIBILITY_IN_API_MODE_WARNING
reporter.reportOn(source, factory, context)
}
/**
* Exclusion list:
* 1. Primary constructors of public API classes
* 2. Properties of data classes in public API
* 3. Overrides of public API. Effectively, this means 'no report on overrides at all'
* 4. Getters and setters (because getters can't change visibility and setter-only explicit visibility looks ugly)
* 5. Properties of annotations in public API
* 6. Value parameter declaration
*
* TODO: Do we need something like @PublicApiFile to disable (or invert) this inspection per-file?
*/
private fun explicitVisibilityIsNotRequired(declaration: FirMemberDeclaration, context: CheckerContext): Boolean {
return when (declaration) {
// 1,
is FirPrimaryConstructor,
// 4
is FirPropertyAccessor,
// 6
is FirValueParameter -> true
is FirCallableDeclaration -> {
val containingClass = context.containingDeclarations.lastOrNull() as? FirRegularClass
// 2, 5
if (declaration is FirProperty &&
containingClass != null &&
(containingClass.isData || containingClass.classKind == ClassKind.ANNOTATION_CLASS)
) {
return true
}
// 3
declaration.isOverride
}
else -> false
}
}
private fun checkExplicitReturnType(
state: ExplicitApiMode,
declaration: FirMemberDeclaration,
source: FirSourceElement,
context: CheckerContext,
reporter: DiagnosticReporter
) {
if (declaration !is FirCallableDeclaration) return
if (!returnTypeCheckIsApplicable(source, context)) return
val shouldReport = returnTypeRequired(declaration, context)
if (shouldReport) {
val factory =
if (state == ExplicitApiMode.STRICT)
FirErrors.NO_EXPLICIT_RETURN_TYPE_IN_API_MODE
else
FirErrors.NO_EXPLICIT_RETURN_TYPE_IN_API_MODE_WARNING
reporter.reportOn(source, factory, context)
}
}
private fun returnTypeCheckIsApplicable(source: FirSourceElement, context: CheckerContext): Boolean {
// Note that by default getChild uses `depth = -1`, which would find all descendents.
if (source.getChild(KtNodeTypes.TYPE_REFERENCE, depth = 1) != null) return false
// Do not check if the containing file is not a physical file.
val containingFile = context.containingDeclarations.first()
if (containingFile.source?.elementType in codeFragmentTypes) return false
return when (source.elementType) {
// Only require return type if the function is defined via `=`. If it has a block body or is abstract, we don't require return
// type because not declaring means it returns `Unit`.
KtNodeTypes.FUN -> source.getChild(KtTokens.EQ, depth = 1) != null
KtNodeTypes.PROPERTY -> true
else -> false
}
}
private fun returnTypeRequired(declaration: FirCallableDeclaration, context: CheckerContext): Boolean {
// If current declaration is local or it's a member in a local declaration (local class, etc), then we do not require return type.
return !declaration.isLocalMember && context.containingDeclarations.lastOrNull()?.isLocalMember != true
}
}
@@ -308,6 +308,10 @@ import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.NO_ACTUAL_CLASS_M
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.NO_ACTUAL_FOR_EXPECT
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.NO_COMPANION_OBJECT
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.NO_ELSE_IN_WHEN
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.NO_EXPLICIT_RETURN_TYPE_IN_API_MODE
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.NO_EXPLICIT_RETURN_TYPE_IN_API_MODE_WARNING
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.NO_EXPLICIT_VISIBILITY_IN_API_MODE
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.NO_EXPLICIT_VISIBILITY_IN_API_MODE_WARNING
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.NO_GET_METHOD
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.NO_RECEIVER_ALLOWED
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.NO_RETURN_IN_FUNCTION_WITH_BLOCK_BODY
@@ -799,6 +803,11 @@ class FirDefaultErrorMessages {
map.put(DEPRECATED_MODIFIER_PAIR, "Modifier ''{0}'' is deprecated in presence of ''{1}''", TO_STRING, TO_STRING)
map.put(DEPRECATED_MODIFIER_FOR_TARGET, "Modifier ''{0}'' is deprecated for ''{1}''", TO_STRING, STRING)
map.put(REDUNDANT_MODIFIER_FOR_TARGET, "Modifier ''{0}'' is redundant for ''{1}''", TO_STRING, STRING)
map.put(NO_EXPLICIT_VISIBILITY_IN_API_MODE, "Visibility must be specified in explicit API mode");
map.put(NO_EXPLICIT_RETURN_TYPE_IN_API_MODE, "Return type must be specified in explicit API mode");
map.put(NO_EXPLICIT_VISIBILITY_IN_API_MODE_WARNING, "Visibility must be specified in explicit API mode");
map.put(NO_EXPLICIT_RETURN_TYPE_IN_API_MODE_WARNING, "Return type must be specified in explicit API mode");
map.put(INCOMPATIBLE_MODIFIERS, "Modifier ''{0}'' is incompatible with ''{1}''", TO_STRING, TO_STRING)
map.put(REDUNDANT_OPEN_IN_INTERFACE, "Modifier 'open' is redundant for abstract interface members")
map.put(WRONG_MODIFIER_TARGET, "Modifier ''{0}'' is not applicable to ''{1}''", TO_STRING, STRING)
@@ -148,6 +148,43 @@ object LightTreePositioningStrategies {
}
}
val DECLARATION_START_TO_NAME: LightTreePositioningStrategy = object : LightTreePositioningStrategy() {
private fun FlyweightCapableTreeStructure<LighterASTNode>.firstNonCommentNonAnnotationLeaf(node: LighterASTNode): LighterASTNode? {
val childrenArray = getChildrenArray(node).filterNotNull()
// this is leaf
if (childrenArray.isEmpty()) return node
for (child in childrenArray) {
val childTokenType = child.tokenType ?: return null
if (childTokenType in KtTokens.WHITE_SPACE_OR_COMMENT_BIT_SET || childTokenType == KtNodeTypes.ANNOTATION_ENTRY) {
continue
}
return firstNonCommentNonAnnotationLeaf(child) ?: continue
}
return null
}
override fun mark(
node: LighterASTNode,
startOffset: Int,
endOffset: Int,
tree: FlyweightCapableTreeStructure<LighterASTNode>
): List<TextRange> {
val startNode = tree.firstNonCommentNonAnnotationLeaf(node) ?: node
val nameIdentifier = tree.nameIdentifier(node)
return if (nameIdentifier != null) {
markRange(startNode, nameIdentifier, startOffset, endOffset, tree, node)
} else {
val endNode = when (node.tokenType) {
KtNodeTypes.PRIMARY_CONSTRUCTOR, KtNodeTypes.SECONDARY_CONSTRUCTOR -> tree.constructorKeyword(node)
KtNodeTypes.OBJECT_DECLARATION -> tree.objectKeyword(node)
else -> return DEFAULT.mark(node, startOffset, endOffset, tree)
}
markRange(startNode, endNode ?: node, startOffset, endOffset, tree, node)
}
}
}
val DECLARATION_NAME: LightTreePositioningStrategy = object : LightTreePositioningStrategy() {
override fun mark(
node: LighterASTNode,
@@ -1083,6 +1120,12 @@ fun LighterASTNode.isExpression(): Boolean {
}
}
fun FlyweightCapableTreeStructure<LighterASTNode>.getChildrenArray(node: LighterASTNode): Array<LighterASTNode?> {
val childrenRef = Ref<Array<LighterASTNode?>>()
getChildren(node, childrenRef)
return childrenRef.get() ?: emptyArray()
}
/**
* @param locateReferencedName whether to remove any nested parentheses while locating the reference element. This is useful for diagnostics
* on super and unresolved references. For example, with the following, only the part inside the parentheses should be highlighted.
@@ -330,13 +330,18 @@ object SourceElementPositioningStrategies {
PositioningStrategies.TAILREC_MODIFIER
)
val PROPERTY_DELEGATE = SourceElementPositioningStrategy(
LightTreePositioningStrategies.PROPERTY_DELEGATE,
PositioningStrategies.PROPERTY_DELEGATE
)
val IMPORT_ALIAS = SourceElementPositioningStrategy(
LightTreePositioningStrategies.IMPORT_ALIAS,
PositioningStrategies.IMPORT_ALIAS
)
val PROPERTY_DELEGATE = SourceElementPositioningStrategy(
LightTreePositioningStrategies.PROPERTY_DELEGATE,
PositioningStrategies.PROPERTY_DELEGATE
val DECLARATION_START_TO_NAME = SourceElementPositioningStrategy(
LightTreePositioningStrategies.DECLARATION_START_TO_NAME,
PositioningStrategies.DECLARATION_START_TO_NAME
)
}