FIR/FIR IDE: Use entire FirVariableAssignment when reporting UNSAFE_CALL

(e.g., `nullable.a = b`), and use positioning strategies to locate the
dot in the LHS expression.

Without it, only the callee reference is reported on, which makes the
highlighting of the error and application of quickfixes incorrect in the
IDE.

Also fixed issue with annotated and/or labeled expressions on LHS of
assignment (e.g., `(@Ann label@ i) = 34`).
This commit is contained in:
Mark Punzalan
2021-05-13 09:24:29 +00:00
committed by Ilya Kirillov
parent 1d9247ae0f
commit d2b8204fdc
28 changed files with 342 additions and 92 deletions
@@ -58,7 +58,7 @@ enum class PositioningStrategy(private val strategy: String? = null) {
VALUE_ARGUMENTS,
SUPERTYPES_LIST,
RETURN_WITH_LABEL,
ASSIGNMENT_VALUE,
PROPERTY_INITIALIZER,
WHOLE_ELEMENT,
INT_LITERAL_OUT_OF_RANGE,
FLOAT_LITERAL_OUT_OF_RANGE,
@@ -71,6 +71,7 @@ enum class PositioningStrategy(private val strategy: String? = null) {
RESERVED_UNDERSCORE,
QUESTION_MARK_BY_TYPE,
ANNOTATION_USE_SITE,
ASSIGNMENT_LHS,
;
@@ -55,7 +55,7 @@ object DIAGNOSTICS_LIST : DiagnosticList() {
val ASSIGNMENT_IN_EXPRESSION_CONTEXT by error<KtBinaryExpression>()
val BREAK_OR_CONTINUE_OUTSIDE_A_LOOP by error<PsiElement>()
val NOT_A_LOOP_LABEL by error<PsiElement>()
val VARIABLE_EXPECTED by error<PsiElement>()
val VARIABLE_EXPECTED by error<PsiElement>(PositioningStrategy.ASSIGNMENT_LHS)
val DELEGATION_IN_INTERFACE by error<PsiElement>()
val NESTED_CLASS_NOT_ALLOWED by error<KtNamedDeclaration>(PositioningStrategy.DECLARATION_NAME) {
parameter<String>("declaration")
@@ -629,7 +629,7 @@ object DIAGNOSTICS_LIST : DiagnosticList() {
parameter<ConeKotlinType>("expectedType")
parameter<ConeKotlinType>("actualType")
}
val INITIALIZER_TYPE_MISMATCH by error<KtProperty>(PositioningStrategy.ASSIGNMENT_VALUE) {
val INITIALIZER_TYPE_MISMATCH by error<KtProperty>(PositioningStrategy.PROPERTY_INITIALIZER) {
parameter<ConeKotlinType>("expectedType")
parameter<ConeKotlinType>("actualType")
}
@@ -87,7 +87,7 @@ object FirErrors {
val ASSIGNMENT_IN_EXPRESSION_CONTEXT by error0<KtBinaryExpression>()
val BREAK_OR_CONTINUE_OUTSIDE_A_LOOP by error0<PsiElement>()
val NOT_A_LOOP_LABEL by error0<PsiElement>()
val VARIABLE_EXPECTED by error0<PsiElement>()
val VARIABLE_EXPECTED by error0<PsiElement>(SourceElementPositioningStrategies.ASSIGNMENT_LHS)
val DELEGATION_IN_INTERFACE by error0<PsiElement>()
val NESTED_CLASS_NOT_ALLOWED by error1<KtNamedDeclaration, String>(SourceElementPositioningStrategies.DECLARATION_NAME)
val INCORRECT_CHARACTER_LITERAL by error0<PsiElement>()
@@ -384,7 +384,7 @@ object FirErrors {
val CONST_VAL_WITHOUT_INITIALIZER by error0<KtProperty>(SourceElementPositioningStrategies.CONST_MODIFIER)
val CONST_VAL_WITH_NON_CONST_INITIALIZER by error0<KtExpression>()
val WRONG_SETTER_PARAMETER_TYPE by error2<KtTypeReference, ConeKotlinType, ConeKotlinType>()
val INITIALIZER_TYPE_MISMATCH by error2<KtProperty, ConeKotlinType, ConeKotlinType>(SourceElementPositioningStrategies.ASSIGNMENT_VALUE)
val INITIALIZER_TYPE_MISMATCH by error2<KtProperty, ConeKotlinType, ConeKotlinType>(SourceElementPositioningStrategies.PROPERTY_INITIALIZER)
val GETTER_VISIBILITY_DIFFERS_FROM_PROPERTY_VISIBILITY by error0<KtModifierListOwner>(SourceElementPositioningStrategies.VISIBILITY_MODIFIER)
val SETTER_VISIBILITY_INCONSISTENT_WITH_PROPERTY_VISIBILITY by error0<KtModifierListOwner>(SourceElementPositioningStrategies.VISIBILITY_MODIFIER)
val WRONG_SETTER_RETURN_TYPE by error0<KtTypeReference>()
@@ -47,9 +47,9 @@ class ErrorNodeDiagnosticCollectorComponent(
override fun visitErrorNamedReference(errorNamedReference: FirErrorNamedReference, data: CheckerContext) {
val source = errorNamedReference.source ?: return
val qualifiedAccessOrAnnotationCall = data.qualifiedAccessOrAnnotationCalls.lastOrNull()?.takeIf {
// Use the source of the enclosing FirQualifiedAccessExpression if it is exactly the call to the erroneous callee.
// Use the source of the enclosing FirQualifiedAccess if it is exactly the call to the erroneous callee.
when (it) {
is FirQualifiedAccessExpression -> it.calleeReference == errorNamedReference
is FirQualifiedAccess -> it.calleeReference == errorNamedReference
is FirAnnotationCall -> it.calleeReference == errorNamedReference
else -> false
}
@@ -425,6 +425,13 @@ object LightTreePositioningStrategies {
endOffset: Int,
tree: FlyweightCapableTreeStructure<LighterASTNode>
): List<TextRange> {
if (node.tokenType == KtNodeTypes.BINARY_EXPRESSION &&
tree.findDescendantByTypes(node, KtTokens.ALL_ASSIGNMENTS) != null
) {
tree.findDescendantByType(node, KtNodeTypes.DOT_QUALIFIED_EXPRESSION)?.let {
return markElement(tree.dotOperator(it) ?: it, startOffset, endOffset, tree, node)
}
}
if (node.tokenType == KtNodeTypes.DOT_QUALIFIED_EXPRESSION) {
return markElement(tree.dotOperator(node) ?: node, startOffset, endOffset, tree, node)
}
@@ -707,6 +714,33 @@ object LightTreePositioningStrategies {
}
}
val ASSIGNMENT_LHS: LightTreePositioningStrategy = object : LightTreePositioningStrategy() {
override fun mark(
node: LighterASTNode,
startOffset: Int,
endOffset: Int,
tree: FlyweightCapableTreeStructure<LighterASTNode>
): List<TextRange> {
if ((node.tokenType == KtNodeTypes.BINARY_EXPRESSION &&
tree.findDescendantByTypes(node, KtTokens.ALL_ASSIGNMENTS) != null) ||
((node.tokenType == KtNodeTypes.PREFIX_EXPRESSION || node.tokenType == KtNodeTypes.POSTFIX_EXPRESSION) &&
tree.findDescendantByTypes(node, KtTokens.INCREMENT_AND_DECREMENT) != null)
) {
val lhs = if (node.tokenType == KtNodeTypes.PREFIX_EXPRESSION) {
tree.lastChildExpression(node)
} else {
tree.firstChildExpression(node)
}
lhs?.let {
tree.unwrapParenthesesLabelsAndAnnotations(it)?.let { unwrapped ->
return markElement(unwrapped, startOffset, endOffset, tree, node)
}
}
}
return super.mark(node, startOffset, endOffset, tree)
}
}
val ANNOTATION_USE_SITE: LightTreePositioningStrategy = object : LightTreePositioningStrategy() {
override fun mark(
node: LighterASTNode,
@@ -810,6 +844,18 @@ private fun FlyweightCapableTreeStructure<LighterASTNode>.referenceExpression(
return result
}
private fun FlyweightCapableTreeStructure<LighterASTNode>.unwrapParenthesesLabelsAndAnnotations(node: LighterASTNode): LighterASTNode? {
var unwrapped = node
while (true) {
unwrapped = when (unwrapped.tokenType) {
KtNodeTypes.PARENTHESIZED -> firstChildExpression(unwrapped) ?: return unwrapped
KtNodeTypes.LABELED_EXPRESSION -> lastChildExpression(unwrapped) ?: return unwrapped
KtNodeTypes.ANNOTATED_EXPRESSION -> firstChildExpression(unwrapped) ?: return unwrapped
else -> return unwrapped
}
}
}
private fun FlyweightCapableTreeStructure<LighterASTNode>.findExpressionDeep(node: LighterASTNode): LighterASTNode? =
findFirstDescendant(node) { it.isExpression() }
@@ -898,6 +944,18 @@ fun FlyweightCapableTreeStructure<LighterASTNode>.selector(node: LighterASTNode)
}
fun FlyweightCapableTreeStructure<LighterASTNode>.firstChildExpression(node: LighterASTNode): LighterASTNode? {
val childrenRef = Ref<Array<LighterASTNode?>>()
getChildren(node, childrenRef)
return childrenRef.get()?.firstOrNull { it?.isExpression() == true }
}
fun FlyweightCapableTreeStructure<LighterASTNode>.lastChildExpression(node: LighterASTNode): LighterASTNode? {
val childrenRef = Ref<Array<LighterASTNode?>>()
getChildren(node, childrenRef)
return childrenRef.get()?.lastOrNull { it?.isExpression() == true }
}
fun FlyweightCapableTreeStructure<LighterASTNode>.findChildByType(node: LighterASTNode, type: IElementType): LighterASTNode? {
val childrenRef = Ref<Array<LighterASTNode?>>()
getChildren(node, childrenRef)
@@ -208,9 +208,9 @@ object SourceElementPositioningStrategies {
PositioningStrategies.RETURN_WITH_LABEL
)
val ASSIGNMENT_VALUE = SourceElementPositioningStrategy(
val PROPERTY_INITIALIZER = SourceElementPositioningStrategy(
LightTreePositioningStrategies.LAST_CHILD,
PositioningStrategies.ASSIGNMENT_VALUE
PositioningStrategies.PROPERTY_INITIALIZER
)
val WHOLE_ELEMENT = SourceElementPositioningStrategy(
@@ -247,4 +247,9 @@ object SourceElementPositioningStrategies {
LightTreePositioningStrategies.ANNOTATION_USE_SITE,
PositioningStrategies.ANNOTATION_USE_SITE
)
val ASSIGNMENT_LHS = SourceElementPositioningStrategy(
LightTreePositioningStrategies.ASSIGNMENT_LHS,
PositioningStrategies.ASSIGNMENT_LHS
)
}
@@ -83,20 +83,31 @@ abstract class AbstractRawFirBuilderTestCase : KtParsingTestCase(
if (!result.add(this)) {
return result
}
propertyLoop@ for (property in this::class.memberProperties) {
val childElement = property.getter.apply { isAccessible = true }.call(this)
for (property in this::class.memberProperties) {
if (hasNoAcceptAndTransform(this::class.simpleName, property.name)) continue
when (childElement) {
is FirNoReceiverExpression -> continue@propertyLoop
when (val childElement = property.getter.apply { isAccessible = true }.call(this)) {
is FirNoReceiverExpression -> continue
is FirElement -> childElement.traverseChildren(result)
is List<*> -> childElement.filterIsInstance<FirElement>().forEach { it.traverseChildren(result) }
else -> continue@propertyLoop
else -> continue
}
}
return result
}
private val firImplClassPropertiesWithNoAcceptAndTransform = mapOf(
"FirResolvedImportImpl" to "delegate",
"FirErrorTypeRefImpl" to "delegatedTypeRef",
"FirResolvedTypeRefImpl" to "delegatedTypeRef"
)
private fun hasNoAcceptAndTransform(className: String?, propertyName: String): Boolean {
if (className == null) return false
return firImplClassPropertiesWithNoAcceptAndTransform[className] == propertyName
}
private fun FirFile.visitChildren(): Set<FirElement> =
ConsistencyVisitor().let {
this@visitChildren.accept(it)
@@ -445,9 +445,8 @@ abstract class BaseFirBuilder<T>(val baseSession: FirSession, val context: Conte
*/
// TODO:
// 1. Support receiver capturing for `array.b++` (elementType == ARRAY_ACCESS_EXPRESSION).
// 2. Support receiver capturing for `a?.b++` (elementType == SAFE_ACCESS_EXPRESSION).
// 3. Add box test cases for #1 and #2 where receiver expression has side effects.
// 1. Support receiver capturing for `a?.b++` (elementType == SAFE_ACCESS_EXPRESSION).
// 2. Add box test cases for #1 where receiver expression has side effects.
fun generateIncrementOrDecrementBlock(
baseExpression: T,
operationReference: T?,
@@ -456,21 +455,8 @@ abstract class BaseFirBuilder<T>(val baseSession: FirSession, val context: Conte
prefix: Boolean,
convert: T.() -> FirExpression
): FirExpression {
// NOTE: By removing surrounding parentheses and labels, FirLabels will NOT be created for those labels.
// This should be fine since the label is meaningless and unusable for a ++/-- argument.
var unwrappedArgument = argument
while (true) {
unwrappedArgument = when (unwrappedArgument?.elementType) {
PARENTHESIZED -> unwrappedArgument?.getExpressionInParentheses()
LABELED_EXPRESSION -> unwrappedArgument?.getLabeledExpression()
else -> break
}
}
if (unwrappedArgument == null) {
return buildErrorExpression {
diagnostic = ConeSimpleDiagnostic("Inc/dec without operand", DiagnosticKind.Syntax)
}
val unwrappedArgument = argument.unwrap() ?: return buildErrorExpression {
diagnostic = ConeSimpleDiagnostic("Inc/dec without operand", DiagnosticKind.Syntax)
}
if (unwrappedArgument.elementType == DOT_QUALIFIED_EXPRESSION) {
@@ -566,6 +552,20 @@ abstract class BaseFirBuilder<T>(val baseSession: FirSession, val context: Conte
}
}
private fun T?.unwrap(): T? {
// NOTE: By removing surrounding parentheses and labels, FirLabels will NOT be created for those labels.
// This should be fine since the label is meaningless and unusable for a ++/-- argument or assignment LHS.
var unwrapped = this
while (true) {
unwrapped = when (unwrapped?.elementType) {
PARENTHESIZED -> unwrapped?.getExpressionInParentheses()
LABELED_EXPRESSION -> unwrapped?.getLabeledExpression()
ANNOTATED_EXPRESSION -> unwrapped?.getAnnotatedExpression()
else -> return unwrapped
}
}
}
/**
* given:
* a.b++
@@ -860,12 +860,6 @@ abstract class BaseFirBuilder<T>(val baseSession: FirSession, val context: Conte
}
}
}
PARENTHESIZED -> {
return initializeLValue(left.getExpressionInParentheses(), convertQualified)
}
ANNOTATED_EXPRESSION -> {
return initializeLValue(left.getAnnotatedExpression(), convertQualified)
}
}
}
return buildErrorNamedReference {
@@ -881,19 +875,19 @@ abstract class BaseFirBuilder<T>(val baseSession: FirSession, val context: Conte
operation: FirOperation,
convert: T.() -> FirExpression
): FirStatement {
val tokenType = this?.elementType
if (tokenType == PARENTHESIZED) {
return this!!.getExpressionInParentheses().generateAssignment(baseSource, rhs, value, operation, convert)
val unwrappedLhs = this.unwrap() ?: return buildErrorExpression {
diagnostic = ConeSimpleDiagnostic("Inc/dec without operand", DiagnosticKind.Syntax)
}
val tokenType = unwrappedLhs.elementType
if (tokenType == ARRAY_ACCESS_EXPRESSION) {
require(this != null)
if (operation == FirOperation.ASSIGN) {
context.arraySetArgument[this] = value
context.arraySetArgument[unwrappedLhs] = value
}
return if (operation == FirOperation.ASSIGN) {
this.convert()
unwrappedLhs.convert()
} else {
generateAugmentedArraySetCall(baseSource, operation, rhs, convert)
generateAugmentedArraySetCall(unwrappedLhs, baseSource, operation, rhs, convert)
}
}
@@ -924,7 +918,7 @@ abstract class BaseFirBuilder<T>(val baseSession: FirSession, val context: Conte
return buildVariableAssignment {
source = baseSource
rValue = value
calleeReference = initializeLValue(this@generateAssignment) { convert() as? FirQualifiedAccess }
calleeReference = initializeLValue(unwrappedLhs) { convert() as? FirQualifiedAccess }
}
}
@@ -950,7 +944,8 @@ abstract class BaseFirBuilder<T>(val baseSession: FirSession, val context: Conte
return safeCallNonAssignment
}
private fun T.generateAugmentedArraySetCall(
private fun generateAugmentedArraySetCall(
unwrappedReceiver: T,
baseSource: FirSourceElement?,
operation: FirOperation,
rhs: T?,
@@ -959,12 +954,13 @@ abstract class BaseFirBuilder<T>(val baseSession: FirSession, val context: Conte
return buildAugmentedArraySetCall {
source = baseSource
this.operation = operation
assignCall = generateAugmentedCallForAugmentedArraySetCall(operation, rhs, convert)
setGetBlock = generateSetGetBlockForAugmentedArraySetCall(baseSource, operation, rhs, convert)
assignCall = generateAugmentedCallForAugmentedArraySetCall(unwrappedReceiver, operation, rhs, convert)
setGetBlock = generateSetGetBlockForAugmentedArraySetCall(unwrappedReceiver, baseSource, operation, rhs, convert)
}
}
private fun T.generateAugmentedCallForAugmentedArraySetCall(
private fun generateAugmentedCallForAugmentedArraySetCall(
unwrappedReceiver: T,
operation: FirOperation,
rhs: T?,
convert: T.() -> FirExpression
@@ -977,7 +973,7 @@ abstract class BaseFirBuilder<T>(val baseSession: FirSession, val context: Conte
calleeReference = buildSimpleNamedReference {
name = FirOperationNameConventions.ASSIGNMENTS.getValue(operation)
}
explicitReceiver = convert()
explicitReceiver = unwrappedReceiver.convert()
argumentList = buildArgumentList {
arguments += rhs?.convert() ?: buildErrorExpression(
null,
@@ -989,7 +985,8 @@ abstract class BaseFirBuilder<T>(val baseSession: FirSession, val context: Conte
}
private fun T.generateSetGetBlockForAugmentedArraySetCall(
private fun generateSetGetBlockForAugmentedArraySetCall(
unwrappedReceiver: T,
baseSource: FirSourceElement?,
operation: FirOperation,
rhs: T?,
@@ -1005,7 +1002,7 @@ abstract class BaseFirBuilder<T>(val baseSession: FirSession, val context: Conte
* }
*/
return buildBlock {
val baseCall = convert() as FirFunctionCall
val baseCall = unwrappedReceiver.convert() as FirFunctionCall
val arrayVariable = generateTemporaryVariable(
baseModuleData,
@@ -775,11 +775,14 @@ object PositioningStrategies {
val DOT_BY_QUALIFIED: PositioningStrategy<PsiElement> = object : PositioningStrategy<PsiElement>() {
override fun mark(element: PsiElement): List<TextRange> {
when (element) {
is KtDotQualifiedExpression -> {
return mark(element.operationTokenNode.psi)
if (element is KtBinaryExpression && element.operationToken in KtTokens.ALL_ASSIGNMENTS) {
element.left?.let { left ->
left.findDescendantOfType<KtDotQualifiedExpression>()?.let { return mark(it) }
}
}
if (element is KtDotQualifiedExpression) {
return mark(element.operationTokenNode.psi)
}
// Fallback to mark the callee reference.
return REFERENCE_BY_QUALIFIED.mark(element)
}
@@ -848,9 +851,9 @@ object PositioningStrategies {
val REIFIED_MODIFIER: PositioningStrategy<KtModifierListOwner> = modifierSetPosition(KtTokens.REIFIED_KEYWORD)
val ASSIGNMENT_VALUE: PositioningStrategy<KtProperty> = object : PositioningStrategy<PsiElement>() {
override fun mark(element: PsiElement): List<TextRange> {
return markElement(if (element is KtProperty) element.initializer ?: element else element)
val PROPERTY_INITIALIZER: PositioningStrategy<KtProperty> = object : PositioningStrategy<KtProperty>() {
override fun mark(element: KtProperty): List<TextRange> {
return markElement(element.initializer ?: element)
}
}
@@ -871,6 +874,17 @@ object PositioningStrategies {
}
}
val ASSIGNMENT_LHS: PositioningStrategy<PsiElement> = object : PositioningStrategy<PsiElement>() {
override fun mark(element: PsiElement): List<TextRange> {
if (element is KtBinaryExpression && element.operationToken in KtTokens.ALL_ASSIGNMENTS) {
element.left.let { left -> left.unwrapParenthesesLabelsAndAnnotations()?.let { return markElement(it) } }
}
if (element is KtUnaryExpression && element.operationToken in KtTokens.INCREMENT_AND_DECREMENT) {
element.baseExpression.let { arg -> arg.unwrapParenthesesLabelsAndAnnotations()?.let { return markElement(it) } }
}
return super.mark(element)
}
}
/**
* @param locateReferencedName whether to remove any nested parentheses while locating the reference element. This is useful for
@@ -265,4 +265,5 @@ public interface KtTokens {
TokenSet AUGMENTED_ASSIGNMENTS = TokenSet.create(PLUSEQ, MINUSEQ, MULTEQ, PERCEQ, DIVEQ);
TokenSet ALL_ASSIGNMENTS = TokenSet.create(EQ, PLUSEQ, MINUSEQ, MULTEQ, PERCEQ, DIVEQ);
TokenSet INCREMENT_AND_DECREMENT = TokenSet.create(PLUSPLUS, MINUSMINUS);
}
@@ -498,4 +498,16 @@ fun KtExpression.isNull(): Boolean {
returns(true) implies (this@isNull is KtConstantExpression)
}
return this is KtConstantExpression && this.node.elementType == KtNodeTypes.NULL
}
}
fun PsiElement?.unwrapParenthesesLabelsAndAnnotations(): PsiElement? {
var unwrapped = this
while (true) {
unwrapped = when (unwrapped) {
is KtParenthesizedExpression -> unwrapped.expression
is KtLabeledExpression -> unwrapped.baseExpression
is KtAnnotatedExpression -> unwrapped.baseExpression
else -> return unwrapped
}
}
}
+26 -7
View File
@@ -51,18 +51,25 @@ fun cannotBe() {
<!VARIABLE_EXPECTED!>5<!> = 34
}
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.EXPRESSION)
annotation class Ann
fun canBe(i0: Int, j: Int) {
var i = i0
(<!VARIABLE_EXPECTED!>label@ i<!>) = 34
(label@ i) = 34
(<!VARIABLE_EXPECTED!>label@ j<!>) = 34 //repeat for j
(label@ j) = 34 //repeat for j
val a = A()
(<!VARIABLE_EXPECTED!>l@ a.a<!>) = 3894
(l@ a.a) = 3894
@Ann
l@ (i) = 123
}
fun canBe2(j: Int) {
(<!VARIABLE_EXPECTED!>label@ j<!>) = 34
(label@ j) = 34
}
class A() {
@@ -78,10 +85,13 @@ class Test() {
(f@ <!VARIABLE_EXPECTED!>getInt()<!>) += 343
<!VARIABLE_EXPECTED!>1<!>++
(r@ <!VARIABLE_EXPECTED!>1<!>)++
(r@ <!VARIABLE_EXPECTED!>1<!>)--
<!VARIABLE_EXPECTED!>getInt()<!>++
(m@ <!VARIABLE_EXPECTED!>getInt()<!>)++
(m@ <!VARIABLE_EXPECTED!>getInt()<!>)--
++<!VARIABLE_EXPECTED!>2<!>
--(r@ <!VARIABLE_EXPECTED!>2<!>)
this<!UNRESOLVED_REFERENCE!>++<!>
@@ -89,6 +99,9 @@ class Test() {
s += "ss"
s += this
s += (a@ 2)
@Ann
l@ (<!VARIABLE_EXPECTED!>1<!>) = 123
}
fun testIncompleteSyntax() {
@@ -106,8 +119,11 @@ class Test() {
<!VAL_REASSIGNMENT!>b<!> += 34
a++
(l@ a)++
(@Ann l@ a)--
(a)++
--a
++(@Ann l@ a)
--(a)
}
fun testVariables1() {
@@ -122,6 +138,9 @@ class Test() {
a[3] = 4
a[4]++
a[6] += 43
@Ann
a[7] = 7
(@Ann l@ (a))[8] = 8
ab.getArray()[54] = 23
ab.getArray()[54]++
+22 -3
View File
@@ -51,6 +51,10 @@ fun cannotBe() {
<!VARIABLE_EXPECTED!>5<!> = 34
}
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.EXPRESSION)
annotation class Ann
fun canBe(i0: Int, j: Int) {
var <!ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE!>i<!> = i0
(label@ i) = 34
@@ -59,6 +63,9 @@ fun canBe(i0: Int, j: Int) {
val a = A()
(l@ a.a) = 3894
@Ann
l@ (i) = 123
}
fun canBe2(j: Int) {
@@ -78,10 +85,13 @@ class Test() {
(f@ <!VARIABLE_EXPECTED!>getInt()<!>) += 343
<!VARIABLE_EXPECTED!>1<!>++
(<!REDUNDANT_LABEL_WARNING!>r@<!> <!VARIABLE_EXPECTED!>1<!>)++
(<!REDUNDANT_LABEL_WARNING!>r@<!> <!VARIABLE_EXPECTED!>1<!>)--
<!VARIABLE_EXPECTED!>getInt()<!>++
(<!REDUNDANT_LABEL_WARNING!>m@<!> <!VARIABLE_EXPECTED!>getInt()<!>)++
(<!REDUNDANT_LABEL_WARNING!>m@<!> <!VARIABLE_EXPECTED!>getInt()<!>)--
++<!VARIABLE_EXPECTED!>2<!>
--(<!REDUNDANT_LABEL_WARNING!>r@<!> <!VARIABLE_EXPECTED!>2<!>)
this<!UNRESOLVED_REFERENCE!>++<!>
@@ -89,6 +99,9 @@ class Test() {
s += "ss"
s += this
s += (<!REDUNDANT_LABEL_WARNING!>a@<!> 2)
@Ann
l@ (<!VARIABLE_EXPECTED!>1<!>) = 123
}
fun testIncompleteSyntax() {
@@ -106,8 +119,11 @@ class Test() {
<!VAL_REASSIGNMENT!>b<!> += 34
a++
(<!REDUNDANT_LABEL_WARNING!>l@<!> a)++
(@Ann <!REDUNDANT_LABEL_WARNING!>l@<!> a)--
(a)++
--a
++(@Ann <!REDUNDANT_LABEL_WARNING!>l@<!> a)
--(a)
}
fun testVariables1() {
@@ -122,6 +138,9 @@ class Test() {
a[3] = 4
a[4]++
a[6] += 43
@Ann
a[7] = 7
(@Ann <!REDUNDANT_LABEL_WARNING!>l@<!> (a))[8] = 8
ab.getArray()[54] = 23
ab.getArray()[54]++
@@ -24,6 +24,13 @@ package lvalue_assignment {
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
@kotlin.annotation.Retention(value = AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets = {AnnotationTarget.EXPRESSION}) public final annotation class Ann : kotlin.Annotation {
public constructor Ann()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
public open class B {
public constructor B()
public final var b: kotlin.Int
@@ -47,7 +47,7 @@ class R {
fun test() {
val o = object {
fun run() {
<!UNRESOLVED_REFERENCE!>p<!>.<!UNRESOLVED_REFERENCE!>x<!> = 43
<!UNRESOLVED_REFERENCE!>p<!>.x = 43
}
}
}
@@ -1,6 +1,7 @@
// !DIAGNOSTICS: -DEBUG_INFO_SMARTCAST
class Foo {
fun foo(a: Foo): Foo = a
var f: Foo? = null
}
fun main() {
@@ -27,4 +28,14 @@ fun main() {
val z: Foo? = null
z!!.foo(z<!UNNECESSARY_NOT_NULL_ASSERTION!>!!<!>)
val w: Foo? = null
w<!UNSAFE_CALL!>.<!>f = z
(w<!UNSAFE_CALL!>.<!>f) = z
(label@ w<!UNSAFE_CALL!>.<!>f) = z
w!!.f = z
w.f = z
w<!UNNECESSARY_NOT_NULL_ASSERTION!>!!<!>.f = z
w.f<!UNSAFE_CALL!>.<!>f = z
w.f!!.f = z
}
@@ -1,6 +1,7 @@
// !DIAGNOSTICS: -DEBUG_INFO_SMARTCAST
class Foo {
fun foo(a: Foo): Foo = a
var f: Foo? = null
}
fun main() {
@@ -27,4 +28,14 @@ fun main() {
val z: Foo? = null
z!!.foo(z<!UNNECESSARY_NOT_NULL_ASSERTION!>!!<!>)
val w: Foo? = null
w<!UNSAFE_CALL!>.<!>f = z
(w<!UNSAFE_CALL!>.<!>f) = z
(label@ w<!UNSAFE_CALL!>.<!>f) = z
w!!.f = z
w.f = z
w<!UNNECESSARY_NOT_NULL_ASSERTION!>!!<!>.f = z
<!SMARTCAST_IMPOSSIBLE!>w.f<!>.f = z
w.f!!.f = z
}
@@ -4,6 +4,7 @@ public fun main(): kotlin.Unit
public final class Foo {
public constructor Foo()
public final var f: Foo?
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public final fun foo(/*0*/ a: Foo): Foo
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.idea.frontend.api.fir.diagnostics.KtFirDiagnostic
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtFunctionSymbol
import org.jetbrains.kotlin.idea.quickfix.AddExclExclCallFix
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.unwrapParenthesesLabelsAndAnnotations
import org.jetbrains.kotlin.util.OperatorNameConventions
object AddExclExclCallFixFactories {
@@ -29,46 +30,51 @@ object AddExclExclCallFixFactories {
}
private fun KtAnalysisSession.getFixForUnsafeCall(psi: PsiElement): List<IntentionAction> {
val (target, hasImplicitReceiver) = when (psi) {
val (target, hasImplicitReceiver) = when (val unwrapped = psi.unwrapParenthesesLabelsAndAnnotations()) {
// `foo.bar` -> `foo!!.bar`
is KtDotQualifiedExpression -> psi.receiverExpression to false
is KtDotQualifiedExpression -> unwrapped.receiverExpression to false
// `foo[bar]` -> `foo!![bar]`
is KtArrayAccessExpression -> psi.arrayExpression to false
is KtArrayAccessExpression -> unwrapped.arrayExpression to false
is KtCallableReferenceExpression -> psi.lhs.let { lhs ->
is KtCallableReferenceExpression -> unwrapped.lhs.let { lhs ->
if (lhs != null) {
// `foo::bar` -> `foo!!::bar`
lhs to false
} else {
// `::bar -> this!!::bar`
psi to true
unwrapped to true
}
}
// `bar` -> `this!!.bar`
is KtNameReferenceExpression -> psi to true
is KtNameReferenceExpression -> unwrapped to true
// `bar()` -> `this!!.bar()`
is KtCallExpression -> psi to true
is KtCallExpression -> unwrapped to true
// `-foo` -> `-foo!!`
// NOTE: Unsafe unary operator call is reported as UNSAFE_CALL, _not_ UNSAFE_OPERATOR_CALL
is KtUnaryExpression -> psi.baseExpression to false
is KtUnaryExpression -> unwrapped.baseExpression to false
is KtBinaryExpression -> {
val receiver = if (KtPsiUtil.isInOrNotInOperation(psi)) {
// `bar in foo` -> `bar in foo!!`
psi.right
} else {
// `foo + bar` -> `foo!! + bar` OR `foo infixFun bar` -> `foo!! infixFun bar`
psi.left
val receiver = when {
KtPsiUtil.isInOrNotInOperation(unwrapped) ->
// `bar in foo` -> `bar in foo!!`
unwrapped.right
KtPsiUtil.isAssignment(unwrapped) ->
// UNSAFE_CALL for assignments (e.g., `foo.bar = value`) is reported on the entire statement (KtBinaryExpression).
// The unsafe call is on the LHS of the assignment.
return getFixForUnsafeCall(unwrapped.left ?: return emptyList())
else ->
// `foo + bar` -> `foo!! + bar` OR `foo infixFun bar` -> `foo!! infixFun bar`
unwrapped.left
}
receiver to false
}
// UNSAFE_INFIX_CALL/UNSAFE_OPERATOR_CALL on KtBinaryExpression is reported on the child KtOperationReferenceExpression
is KtOperationReferenceExpression -> return getFixForUnsafeCall(psi.parent)
is KtOperationReferenceExpression -> return getFixForUnsafeCall(unwrapped.parent)
else -> return emptyList()
}
@@ -11,9 +11,9 @@ import org.jetbrains.kotlin.idea.frontend.api.types.KtTypeNullability
import org.jetbrains.kotlin.idea.frontend.api.types.KtTypeWithNullability
import org.jetbrains.kotlin.idea.quickfix.ReplaceImplicitReceiverCallFix
import org.jetbrains.kotlin.idea.quickfix.ReplaceWithSafeCallFix
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.unwrapParenthesesLabelsAndAnnotations
object ReplaceCallFixFactories {
val unsafeCallFactory =
@@ -25,14 +25,23 @@ object ReplaceCallFixFactories {
return expectedType?.nullability == KtTypeNullability.NON_NULLABLE
}
when (val psi = diagnostic.psi) {
is KtDotQualifiedExpression -> listOf(ReplaceWithSafeCallFix(psi, psi.shouldHaveNotNullType()))
val psi = diagnostic.psi
val target = if (psi is KtBinaryExpression && psi.operationToken in KtTokens.ALL_ASSIGNMENTS) {
// UNSAFE_CALL for assignments (e.g., `foo.bar = value`) is reported on the entire statement (KtBinaryExpression).
// The unsafe call is on the LHS of the assignment.
psi.left
} else {
psi
}.unwrapParenthesesLabelsAndAnnotations()
when (target) {
is KtDotQualifiedExpression -> listOf(ReplaceWithSafeCallFix(target, target.shouldHaveNotNullType()))
is KtNameReferenceExpression -> {
// TODO: As a safety precaution, resolve the expression to determine if it is a call with an implicit receiver.
// This is a defensive check to ensure that the diagnostic was reported on such a call and not some other name reference.
// This isn't strictly needed because FIR checkers aren't reporting on wrong elements, but ReplaceWithSafeCallFixFactory
// in FE1.0 does so.
listOf(ReplaceImplicitReceiverCallFix(psi, psi.shouldHaveNotNullType()))
listOf(ReplaceImplicitReceiverCallFix(target, target.shouldHaveNotNullType()))
}
else -> emptyList()
}
@@ -229,6 +229,16 @@ public class HighLevelQuickFixTestGenerated extends AbstractHighLevelQuickFixTes
runTest("idea/testData/quickfix/addExclExclCall/array4.kt");
}
@TestMetadata("assignment.kt")
public void testAssignment() throws Exception {
runTest("idea/testData/quickfix/addExclExclCall/assignment.kt");
}
@TestMetadata("assignmentToUnsafeCallExpression.kt")
public void testAssignmentToUnsafeCallExpression() throws Exception {
runTest("idea/testData/quickfix/addExclExclCall/assignmentToUnsafeCallExpression.kt");
}
@TestMetadata("functionReference.kt")
public void testFunctionReference() throws Exception {
runTest("idea/testData/quickfix/addExclExclCall/functionReference.kt");
@@ -1271,6 +1281,11 @@ public class HighLevelQuickFixTestGenerated extends AbstractHighLevelQuickFixTes
runTest("idea/testData/quickfix/replaceWithSafeCall/assignmentToPropertyWithNoExplicitType.kt");
}
@TestMetadata("assignmentToUnsafeCallExpression.kt")
public void testAssignmentToUnsafeCallExpression() throws Exception {
runTest("idea/testData/quickfix/replaceWithSafeCall/assignmentToUnsafeCallExpression.kt");
}
@TestMetadata("comment.kt")
public void testComment() throws Exception {
runTest("idea/testData/quickfix/replaceWithSafeCall/comment.kt");
+7
View File
@@ -0,0 +1,7 @@
// "Add non-null asserted (!!) call" "true"
// WITH_RUNTIME
var i = 0
fun foo(s: String?) {
i = s<caret>.length
}
@@ -0,0 +1,7 @@
// "Add non-null asserted (!!) call" "true"
// WITH_RUNTIME
var i = 0
fun foo(s: String?) {
i = s!!.length
}
@@ -0,0 +1,6 @@
// "Add non-null asserted (!!) call" "true"
class A(var s: String)
fun foo(a: A?) {
a<caret>.s = ""
}
@@ -0,0 +1,6 @@
// "Add non-null asserted (!!) call" "true"
class A(var s: String)
fun foo(a: A?) {
a!!.s = ""
}
@@ -0,0 +1,6 @@
// "Replace with safe (?.) call" "true"
class A(var s: String? = null)
fun foo(a: A?) {
a<caret>.s = ""
}
@@ -0,0 +1,6 @@
// "Replace with safe (?.) call" "true"
class A(var s: String? = null)
fun foo(a: A?) {
a?.s = ""
}
@@ -642,6 +642,16 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
runTest("idea/testData/quickfix/addExclExclCall/array4.kt");
}
@TestMetadata("assignment.kt")
public void testAssignment() throws Exception {
runTest("idea/testData/quickfix/addExclExclCall/assignment.kt");
}
@TestMetadata("assignmentToUnsafeCallExpression.kt")
public void testAssignmentToUnsafeCallExpression() throws Exception {
runTest("idea/testData/quickfix/addExclExclCall/assignmentToUnsafeCallExpression.kt");
}
@TestMetadata("functionReference.kt")
public void testFunctionReference() throws Exception {
runTest("idea/testData/quickfix/addExclExclCall/functionReference.kt");
@@ -11825,6 +11835,11 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
runTest("idea/testData/quickfix/replaceWithSafeCall/assignmentToPropertyWithNoExplicitType.kt");
}
@TestMetadata("assignmentToUnsafeCallExpression.kt")
public void testAssignmentToUnsafeCallExpression() throws Exception {
runTest("idea/testData/quickfix/replaceWithSafeCall/assignmentToUnsafeCallExpression.kt");
}
@TestMetadata("comment.kt")
public void testComment() throws Exception {
runTest("idea/testData/quickfix/replaceWithSafeCall/comment.kt");