Refine INSTANCE_ACCESS_BEFORE_SUPER_CALL check
- Detect usages of unitialized `this` as extension receiver argument - Make it work within object literal created before super call #KT-9120 Fixed #KT-8289 Fixed
This commit is contained in:
@@ -135,17 +135,22 @@ public class BodyResolver {
|
||||
) {
|
||||
ForceResolveUtil.forceResolveAllContents(descriptor.getAnnotations());
|
||||
|
||||
final CallChecker callChecker = new ConstructorHeaderCallChecker(descriptor);
|
||||
resolveFunctionBody(outerDataFlowInfo, trace, constructor, descriptor, declaringScope,
|
||||
new Function1<LexicalScope, DataFlowInfo>() {
|
||||
@Override
|
||||
public DataFlowInfo invoke(@NotNull LexicalScope headerInnerScope) {
|
||||
return resolveSecondaryConstructorDelegationCall(outerDataFlowInfo, trace, headerInnerScope,
|
||||
constructor, descriptor,
|
||||
callChecker);
|
||||
constructor, descriptor);
|
||||
}
|
||||
},
|
||||
callChecker);
|
||||
new Function1<LexicalScope, LexicalScope>() {
|
||||
@Override
|
||||
public LexicalScope invoke(LexicalScope scope) {
|
||||
return new LexicalScopeImpl(
|
||||
scope, descriptor, scope.isOwnerDescriptorAccessibleByLabel(), scope.getImplicitReceiver(),
|
||||
LexicalScopeKind.CONSTRUCTOR_HEADER);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -154,13 +159,11 @@ public class BodyResolver {
|
||||
@NotNull BindingTrace trace,
|
||||
@NotNull LexicalScope scope,
|
||||
@NotNull KtSecondaryConstructor constructor,
|
||||
@NotNull ConstructorDescriptor descriptor,
|
||||
@NotNull CallChecker callChecker
|
||||
@NotNull ConstructorDescriptor descriptor
|
||||
) {
|
||||
OverloadResolutionResults<?> results = callResolver.resolveConstructorDelegationCall(
|
||||
trace, scope, outerDataFlowInfo,
|
||||
descriptor, constructor.getDelegationCall(),
|
||||
callChecker);
|
||||
descriptor, constructor.getDelegationCall());
|
||||
|
||||
if (results != null && results.isSingleResult()) {
|
||||
ResolvedCall<? extends CallableDescriptor> resolvedCall = results.getResultingCall();
|
||||
@@ -775,27 +778,29 @@ public class BodyResolver {
|
||||
) {
|
||||
computeDeferredType(functionDescriptor.getReturnType());
|
||||
|
||||
resolveFunctionBody(outerDataFlowInfo, trace, function, functionDescriptor, declaringScope, null, CallChecker.DoNothing.INSTANCE$);
|
||||
resolveFunctionBody(outerDataFlowInfo, trace, function, functionDescriptor, declaringScope, null, null);
|
||||
|
||||
assert functionDescriptor.getReturnType() != null;
|
||||
}
|
||||
|
||||
public void resolveFunctionBody(
|
||||
private void resolveFunctionBody(
|
||||
@NotNull DataFlowInfo outerDataFlowInfo,
|
||||
@NotNull BindingTrace trace,
|
||||
@NotNull KtDeclarationWithBody function,
|
||||
@NotNull FunctionDescriptor functionDescriptor,
|
||||
@NotNull LexicalScope scope,
|
||||
@Nullable Function1<LexicalScope, DataFlowInfo> beforeBlockBody,
|
||||
@NotNull CallChecker callChecker
|
||||
// Creates wrapper scope for header resolution if necessary (see resolveSecondaryConstructorBody)
|
||||
@Nullable Function1<LexicalScope, LexicalScope> headerScopeFactory
|
||||
) {
|
||||
PreliminaryDeclarationVisitor.Companion.createForDeclaration(function, trace);
|
||||
LexicalScope innerScope = FunctionDescriptorUtil.getFunctionInnerScope(scope, functionDescriptor, trace);
|
||||
List<KtParameter> valueParameters = function.getValueParameters();
|
||||
List<ValueParameterDescriptor> valueParameterDescriptors = functionDescriptor.getValueParameters();
|
||||
|
||||
LexicalScope headerScope = headerScopeFactory != null ? headerScopeFactory.invoke(innerScope) : innerScope;
|
||||
valueParameterResolver.resolveValueParameters(
|
||||
valueParameters, valueParameterDescriptors, innerScope, outerDataFlowInfo, trace, callChecker
|
||||
valueParameters, valueParameterDescriptors, headerScope, outerDataFlowInfo, trace
|
||||
);
|
||||
|
||||
// Synthetic "field" creation
|
||||
@@ -823,7 +828,7 @@ public class BodyResolver {
|
||||
DataFlowInfo dataFlowInfo = null;
|
||||
|
||||
if (beforeBlockBody != null) {
|
||||
dataFlowInfo = beforeBlockBody.invoke(innerScope);
|
||||
dataFlowInfo = beforeBlockBody.invoke(headerScope);
|
||||
}
|
||||
|
||||
if (function.hasBody()) {
|
||||
|
||||
+27
-27
@@ -19,40 +19,40 @@ package org.jetbrains.kotlin.resolve
|
||||
import org.jetbrains.kotlin.descriptors.CallableDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.ClassDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.ConstructorDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
|
||||
import org.jetbrains.kotlin.diagnostics.Errors
|
||||
import org.jetbrains.kotlin.psi.KtConstructorDelegationCall
|
||||
import org.jetbrains.kotlin.psi.KtInstanceExpressionWithLabel
|
||||
import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker
|
||||
import org.jetbrains.kotlin.resolve.calls.context.BasicCallResolutionContext
|
||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
|
||||
import org.jetbrains.kotlin.resolve.calls.resolvedCallUtil.hasImplicitThisOrSuperDispatchReceiver
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.getOwnerForEffectiveDispatchReceiverParameter
|
||||
|
||||
|
||||
public class ConstructorHeaderCallChecker(constructor: ConstructorDescriptor) : CallChecker {
|
||||
private val containingClass = constructor.getContainingDeclaration()
|
||||
import org.jetbrains.kotlin.resolve.scopes.LexicalScope
|
||||
import org.jetbrains.kotlin.resolve.scopes.LexicalScopeKind
|
||||
import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitReceiver
|
||||
import org.jetbrains.kotlin.resolve.scopes.receivers.Receiver
|
||||
import org.jetbrains.kotlin.resolve.scopes.utils.parentsWithSelf
|
||||
|
||||
public object ConstructorHeaderCallChecker : CallChecker {
|
||||
override fun <F : CallableDescriptor> check(resolvedCall: ResolvedCall<F>, context: BasicCallResolutionContext) {
|
||||
if (resolvedCall.getStatus().isSuccess() &&
|
||||
resolvedCall.hasImplicitThisOrSuperDispatchReceiver(context.trace.getBindingContext()) &&
|
||||
containingClass == resolvedCall.getResultingDescriptor().getOwnerForEffectiveDispatchReceiverParameter()
|
||||
) {
|
||||
reportError(context, resolvedCall)
|
||||
}
|
||||
else {
|
||||
val callElement = resolvedCall.getCall().getCallElement()
|
||||
if (callElement is KtInstanceExpressionWithLabel) {
|
||||
val descriptor = context.trace.get(BindingContext.REFERENCE_TARGET, callElement.getInstanceReference())
|
||||
if (containingClass == descriptor) {
|
||||
reportError(context, resolvedCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val dispatchReceiverClass = resolvedCall.dispatchReceiver.classDescriptorForImplicitReceiver
|
||||
val extensionReceiverClass = resolvedCall.extensionReceiver.classDescriptorForImplicitReceiver
|
||||
|
||||
private fun reportError(context: BasicCallResolutionContext, resolvedCall: ResolvedCall<*>) {
|
||||
context.trace.report(
|
||||
Errors.INSTANCE_ACCESS_BEFORE_SUPER_CALL.on(context.call.getCalleeExpression(), resolvedCall.getResultingDescriptor()))
|
||||
val labelReferenceClass =
|
||||
(resolvedCall.call.callElement as? KtInstanceExpressionWithLabel)?.let {
|
||||
instanceExpressionWithLabel ->
|
||||
context.trace.get(BindingContext.REFERENCE_TARGET, instanceExpressionWithLabel.instanceReference) as? ClassDescriptor
|
||||
}
|
||||
|
||||
if (dispatchReceiverClass == null && extensionReceiverClass == null && labelReferenceClass == null) return
|
||||
|
||||
if (context.scope.parentsWithSelf.any() {
|
||||
it is LexicalScope && it.kind == LexicalScopeKind.CONSTRUCTOR_HEADER
|
||||
&& (it.ownerDescriptor as ConstructorDescriptor).containingDeclaration in
|
||||
setOf(dispatchReceiverClass, extensionReceiverClass, labelReferenceClass)
|
||||
}) {
|
||||
context.trace.report(
|
||||
Errors.INSTANCE_ACCESS_BEFORE_SUPER_CALL.on(context.call.calleeExpression ?: return, resolvedCall.resultingDescriptor))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Receiver?.classDescriptorForImplicitReceiver: ClassDescriptor?
|
||||
get() = (this as? ImplicitReceiver)?.declarationDescriptor as? ClassDescriptor
|
||||
|
||||
@@ -61,7 +61,8 @@ private val DEFAULT_DECLARATION_CHECKERS = listOf(
|
||||
InfixModifierChecker())
|
||||
|
||||
private val DEFAULT_CALL_CHECKERS = listOf(CapturingInClosureChecker(), InlineCheckerWrapper(), ReifiedTypeParameterSubstitutionChecker(),
|
||||
SafeCallChecker(), InvokeConventionChecker(), CallReturnsArrayOfNothingChecker())
|
||||
SafeCallChecker(), InvokeConventionChecker(), CallReturnsArrayOfNothingChecker(),
|
||||
ConstructorHeaderCallChecker)
|
||||
private val DEFAULT_TYPE_CHECKERS = emptyList<AdditionalTypeChecker>()
|
||||
private val DEFAULT_VALIDATORS = listOf(DeprecatedSymbolValidator(), OperatorValidator(), InfixValidator())
|
||||
|
||||
|
||||
@@ -387,7 +387,7 @@ public class CallResolver {
|
||||
public OverloadResolutionResults<FunctionDescriptor> resolveConstructorDelegationCall(
|
||||
@NotNull BindingTrace trace, @NotNull LexicalScope scope, @NotNull DataFlowInfo dataFlowInfo,
|
||||
@NotNull ConstructorDescriptor constructorDescriptor,
|
||||
@NotNull KtConstructorDelegationCall call, @NotNull CallChecker callChecker
|
||||
@NotNull KtConstructorDelegationCall call
|
||||
) {
|
||||
// Method returns `null` when there is nothing to resolve in trivial cases like `null` call expression or
|
||||
// when super call should be conventional enum constructor and super call should be empty
|
||||
@@ -397,7 +397,7 @@ public class CallResolver {
|
||||
CallMaker.makeCall(null, null, call),
|
||||
NO_EXPECTED_TYPE,
|
||||
dataFlowInfo, ContextDependency.INDEPENDENT, CheckArgumentTypesMode.CHECK_VALUE_ARGUMENTS,
|
||||
callChecker, false);
|
||||
CallChecker.DoNothing.INSTANCE, false);
|
||||
|
||||
if (call.getCalleeExpression() == null) return checkArgumentTypesAndFail(context);
|
||||
|
||||
|
||||
@@ -33,26 +33,18 @@ import org.jetbrains.kotlin.resolve.scopes.receivers.ReceiverValue
|
||||
// val x
|
||||
// val y = other.x // return false for `other.x` as it's receiver is not `this`
|
||||
// }
|
||||
public fun ResolvedCall<*>.hasThisOrNoDispatchReceiver(context: BindingContext): Boolean =
|
||||
hasThisOrNoDispatchReceiver(context, true, true)
|
||||
|
||||
public fun ResolvedCall<*>.hasImplicitThisOrSuperDispatchReceiver(context: BindingContext): Boolean =
|
||||
hasThisOrNoDispatchReceiver(context, false, false)
|
||||
|
||||
private fun ResolvedCall<*>.hasThisOrNoDispatchReceiver(
|
||||
context: BindingContext,
|
||||
returnForNoReceiver: Boolean,
|
||||
considerExplicitReceivers: Boolean
|
||||
fun ResolvedCall<*>.hasThisOrNoDispatchReceiver(
|
||||
context: BindingContext
|
||||
): Boolean {
|
||||
val dispatchReceiverValue = getDispatchReceiver()
|
||||
if (getResultingDescriptor().getDispatchReceiverParameter() == null || dispatchReceiverValue == null) return returnForNoReceiver
|
||||
val dispatchReceiverValue = dispatchReceiver
|
||||
if (resultingDescriptor.dispatchReceiverParameter == null || dispatchReceiverValue == null) return true
|
||||
|
||||
var dispatchReceiverDescriptor: DeclarationDescriptor? = null
|
||||
if (dispatchReceiverValue is ImplicitReceiver) {
|
||||
// foo() -- implicit receiver
|
||||
dispatchReceiverDescriptor = dispatchReceiverValue.declarationDescriptor
|
||||
}
|
||||
else if (dispatchReceiverValue is ExpressionReceiver && considerExplicitReceivers) {
|
||||
else if (dispatchReceiverValue is ExpressionReceiver) {
|
||||
val expression = KtPsiUtil.deparenthesize(dispatchReceiverValue.expression)
|
||||
if (expression is KtThisExpression) {
|
||||
// this.foo() -- explicit receiver
|
||||
|
||||
+2
-4
@@ -36,18 +36,16 @@ public class ValueParameterResolver(
|
||||
private val constantExpressionEvaluator: ConstantExpressionEvaluator
|
||||
) {
|
||||
|
||||
@JvmOverloads
|
||||
public fun resolveValueParameters(
|
||||
valueParameters: List<KtParameter>,
|
||||
valueParameterDescriptors: List<ValueParameterDescriptor>,
|
||||
declaringScope: LexicalScope,
|
||||
dataFlowInfo: DataFlowInfo,
|
||||
trace: BindingTrace,
|
||||
callChecker: CallChecker = CallChecker.DoNothing
|
||||
trace: BindingTrace
|
||||
) {
|
||||
val scopeForDefaultValue = LexicalScopeImpl(declaringScope, declaringScope.ownerDescriptor, false, null, LexicalScopeKind.DEFAULT_VALUE)
|
||||
|
||||
val contextForDefaultValue = ExpressionTypingContext.newContext(trace, scopeForDefaultValue, dataFlowInfo, TypeUtils.NO_EXPECTED_TYPE, callChecker)
|
||||
val contextForDefaultValue = ExpressionTypingContext.newContext(trace, scopeForDefaultValue, dataFlowInfo, TypeUtils.NO_EXPECTED_TYPE, CallChecker.DoNothing)
|
||||
|
||||
for ((descriptor, parameter) in valueParameterDescriptors.zip(valueParameters)) {
|
||||
ForceResolveUtil.forceResolveAllContents(descriptor.getAnnotations())
|
||||
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
// !DIAGNOSTICS: -UNUSED_PARAMETER
|
||||
|
||||
fun A.foobar() = 3
|
||||
|
||||
class A {
|
||||
fun foo() = 1
|
||||
constructor(x: () -> Int)
|
||||
constructor() : this(
|
||||
{
|
||||
<!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>foo<!>() +
|
||||
<!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>this<!>.foo() +
|
||||
<!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>this@A<!>.foo() +
|
||||
<!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>foobar<!>()
|
||||
})
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
package
|
||||
|
||||
public fun A.foobar(): kotlin.Int
|
||||
|
||||
public final class A {
|
||||
public constructor A()
|
||||
public constructor A(/*0*/ x: () -> kotlin.Int)
|
||||
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
|
||||
public final fun foo(): kotlin.Int
|
||||
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
|
||||
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// !DIAGNOSTICS: -UNUSED_PARAMETER
|
||||
|
||||
fun A.foobar() = 3
|
||||
|
||||
class A {
|
||||
fun foo() = 1
|
||||
constructor(x: Any?)
|
||||
constructor() : this(object {
|
||||
fun bar() = <!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>foo<!>() + <!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>this@A<!>.foo() +
|
||||
<!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>foobar<!>() + <!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>super@A<!>.hashCode()
|
||||
})
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package
|
||||
|
||||
public fun A.foobar(): kotlin.Int
|
||||
|
||||
public final class A {
|
||||
public constructor A()
|
||||
public constructor A(/*0*/ x: kotlin.Any?)
|
||||
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
|
||||
public final fun foo(): kotlin.Int
|
||||
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
|
||||
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// !DIAGNOSTICS: -UNUSED_PARAMETER
|
||||
|
||||
fun A.foobar() = 3
|
||||
|
||||
class A {
|
||||
fun foo() = 1
|
||||
constructor( x: Any = object {
|
||||
fun bar() = <!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>foo<!>() + <!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>this@A<!>.foo() +
|
||||
<!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>foobar<!>()
|
||||
})
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package
|
||||
|
||||
public fun A.foobar(): kotlin.Int
|
||||
|
||||
public final class A {
|
||||
public constructor A(/*0*/ x: kotlin.Any = ...)
|
||||
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
|
||||
public final fun foo(): kotlin.Int
|
||||
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
|
||||
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// !DIAGNOSTICS: -UNUSED_PARAMETER
|
||||
|
||||
fun A.foobar() = 1
|
||||
val A.prop: Int get() = 2
|
||||
|
||||
class A {
|
||||
constructor(x: Int)
|
||||
constructor() : this(
|
||||
<!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>foobar<!>() +
|
||||
<!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>this<!>.foobar() +
|
||||
<!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>prop<!> +
|
||||
<!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>this<!>.prop +
|
||||
<!INSTANCE_ACCESS_BEFORE_SUPER_CALL!>this@A<!>.prop
|
||||
)
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package
|
||||
|
||||
public val A.prop: kotlin.Int
|
||||
public fun A.foobar(): kotlin.Int
|
||||
|
||||
public final class A {
|
||||
public constructor A()
|
||||
public constructor A(/*0*/ x: kotlin.Int)
|
||||
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
|
||||
}
|
||||
@@ -14831,12 +14831,30 @@ public class DiagnosticsTestGenerated extends AbstractDiagnosticsTest {
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("lambdaAsArgument.kt")
|
||||
public void testLambdaAsArgument() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/secondaryConstructors/headerCallChecker/lambdaAsArgument.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("memberFunAccess.kt")
|
||||
public void testMemberFunAccess() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/secondaryConstructors/headerCallChecker/memberFunAccess.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("objectLiteralAsArgument.kt")
|
||||
public void testObjectLiteralAsArgument() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/secondaryConstructors/headerCallChecker/objectLiteralAsArgument.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("objectLiteralAsDefaultValueParameter.kt")
|
||||
public void testObjectLiteralAsDefaultValueParameter() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/secondaryConstructors/headerCallChecker/objectLiteralAsDefaultValueParameter.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("passingInstance.kt")
|
||||
public void testPassingInstance() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/secondaryConstructors/headerCallChecker/passingInstance.kt");
|
||||
@@ -14873,6 +14891,12 @@ public class DiagnosticsTestGenerated extends AbstractDiagnosticsTest {
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("thisAsExtensionReceiver.kt")
|
||||
public void testThisAsExtensionReceiver() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/secondaryConstructors/headerCallChecker/thisAsExtensionReceiver.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("usingOuterInstance.kt")
|
||||
public void testUsingOuterInstance() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/secondaryConstructors/headerCallChecker/usingOuterInstance.kt");
|
||||
|
||||
@@ -1 +1 @@
|
||||
<error>a</error> + b + c + a1
|
||||
<error>a</error> + b + <error>c</error> + <error>a1</error>
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<error>a</error> + b + c + a1
|
||||
<error>a</error> + b + <error>c</error> + <error>a1</error>
|
||||
Reference in New Issue
Block a user