blob: e0c8f0a139f0f062195f4f4db748bff74b06c288 [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/error/codes.dart';
/// Verifier for initializing fields in constructors.
class ConstructorFieldsVerifier {
final TypeSystemImpl typeSystem;
final Map<InstanceElement, _Interface> _interfaces = Map.identity();
ConstructorFieldsVerifier({required this.typeSystem});
void addConstructors(
DiagnosticReporter diagnosticReporter,
InterfaceElement element,
List<ClassMember> members,
) {
var interfaceFields = _forInterface(element);
var constructors = members.whereType<ConstructorDeclarationImpl>();
for (var constructor in constructors) {
_addConstructor(
diagnosticReporter: diagnosticReporter,
interfaceFields: interfaceFields,
node: constructor,
);
}
}
void report() {
for (var interface in _interfaces.values) {
for (var constructor in interface.constructors.values) {
constructor.report();
}
}
}
void _addConstructor({
required DiagnosticReporter diagnosticReporter,
required _Interface interfaceFields,
required ConstructorDeclarationImpl node,
}) {
if (node.factoryKeyword != null ||
node.redirectedConstructor != null ||
node.externalKeyword != null) {
return;
}
var fragment = node.declaredFragment!;
var constructorState = interfaceFields.forConstructor(
diagnosticReporter: diagnosticReporter,
node: node,
fragment: fragment,
);
if (!fragment.isAugmentation) {
constructorState.updateWithParameters(node);
}
constructorState.updateWithInitializers(diagnosticReporter, node);
}
_Interface _forInterface(InterfaceElement element) {
if (_interfaces[element] case var result?) {
return result;
}
var fieldMap = <FieldElement, _InitState>{};
for (var field in element.fields) {
if (field.isSynthetic) {
continue;
}
if (element is EnumElement && field.name3 == 'index') {
continue;
}
fieldMap[field] =
field.hasInitializer
? _InitState.initInDeclaration
: _InitState.notInit;
}
return _interfaces[element] = _Interface(
typeSystem: typeSystem,
element: element,
fields: fieldMap,
);
}
}
class _Constructor {
final TypeSystemImpl typeSystem;
final DiagnosticReporter diagnosticReporter;
final ConstructorDeclaration node;
final ConstructorElement element;
final Map<FieldElement, _InitState> fields;
/// Set to `true` if the constructor redirects.
bool hasRedirectingConstructorInvocation = false;
_Constructor({
required this.typeSystem,
required this.diagnosticReporter,
required this.node,
required this.element,
required this.fields,
});
void report() {
if (hasRedirectingConstructorInvocation) {
return;
}
// Prepare lists of not initialized fields.
var notInitFinalFields = <_Field>[];
var notInitNonNullableFields = <_Field>[];
fields.forEach((field, state) {
if (state != _InitState.notInit) return;
if (field.isLate) return;
if (field.isAbstract || field.isExternal) return;
if (field.isStatic) return;
var name = field.name3;
if (name == null) return;
if (field.isFinal) {
notInitFinalFields.add(_Field(field, name));
} else if (typeSystem.isPotentiallyNonNullable(field.type)) {
notInitNonNullableFields.add(_Field(field, name));
}
});
reportNotInitializedFinal(notInitFinalFields);
reportNotInitializedNonNullable(notInitNonNullableFields);
}
void reportNotInitializedFinal(List<_Field> notInitFinalFields) {
if (notInitFinalFields.isEmpty) {
return;
}
var names = notInitFinalFields.map((f) => f.name).toList();
names.sort();
if (names.length == 1) {
diagnosticReporter.atNode(
node.returnType,
CompileTimeErrorCode.FINAL_NOT_INITIALIZED_CONSTRUCTOR_1,
arguments: names,
);
} else if (names.length == 2) {
diagnosticReporter.atNode(
node.returnType,
CompileTimeErrorCode.FINAL_NOT_INITIALIZED_CONSTRUCTOR_2,
arguments: names,
);
} else {
diagnosticReporter.atNode(
node.returnType,
CompileTimeErrorCode.FINAL_NOT_INITIALIZED_CONSTRUCTOR_3_PLUS,
arguments: [names[0], names[1], names.length - 2],
);
}
}
void reportNotInitializedNonNullable(List<_Field> notInitNonNullableFields) {
if (notInitNonNullableFields.isEmpty) {
return;
}
var names = notInitNonNullableFields.map((f) => f.name).toList();
names.sort();
for (var name in names) {
diagnosticReporter.atNode(
node.returnType,
CompileTimeErrorCode
.NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD_CONSTRUCTOR,
arguments: [name],
);
}
}
void updateWithInitializers(
DiagnosticReporter diagnosticReporter,
ConstructorDeclaration node,
) {
for (var initializer in node.initializers) {
if (initializer is RedirectingConstructorInvocation) {
hasRedirectingConstructorInvocation = true;
}
if (initializer is ConstructorFieldInitializer) {
var fieldName = initializer.fieldName;
var fieldElement = fieldName.element;
if (fieldElement is FieldElement) {
var state = fields[fieldElement];
if (state == _InitState.notInit) {
fields[fieldElement] = _InitState.initInInitializer;
} else if (state == _InitState.initInDeclaration) {
if (fieldElement.isFinal || fieldElement.isConst) {
diagnosticReporter.atNode(
fieldName,
CompileTimeErrorCode
.FIELD_INITIALIZED_IN_INITIALIZER_AND_DECLARATION,
);
}
} else if (state == _InitState.initInFieldFormal) {
diagnosticReporter.atNode(
fieldName,
CompileTimeErrorCode
.FIELD_INITIALIZED_IN_PARAMETER_AND_INITIALIZER,
);
} else if (state == _InitState.initInInitializer) {
diagnosticReporter.atNode(
fieldName,
CompileTimeErrorCode.FIELD_INITIALIZED_BY_MULTIPLE_INITIALIZERS,
arguments: [fieldElement.displayName],
);
}
}
}
}
}
void updateWithParameters(ConstructorDeclaration node) {
var formalParameters = node.parameters.parameters;
for (var parameter in formalParameters) {
parameter = parameter.notDefault;
if (parameter is FieldFormalParameterImpl) {
var parameterFragment = parameter.declaredFragment!;
var fieldElement = parameterFragment.element.field2;
if (fieldElement == null) {
continue;
}
_InitState? state = fields[fieldElement];
if (state == _InitState.notInit) {
fields[fieldElement] = _InitState.initInFieldFormal;
} else if (state == _InitState.initInDeclaration) {
if (fieldElement.isFinal || fieldElement.isConst) {
diagnosticReporter.atToken(
parameter.name,
CompileTimeErrorCode
.FINAL_INITIALIZED_IN_DECLARATION_AND_CONSTRUCTOR,
arguments: [fieldElement.displayName],
);
}
} else if (state == _InitState.initInFieldFormal) {
// Reported in DuplicateDefinitionVerifier._checkDuplicateIdentifier
}
}
}
}
}
/// The field with a non `null` name.
class _Field {
final FieldElement element;
final String name;
_Field(this.element, this.name);
}
/// The four states of a field initialization state through a constructor
/// signature, not initialized, initialized in the field declaration,
/// initialized in the field formal, and finally, initialized in the
/// initializers list.
enum _InitState {
/// The field is declared without an initializer.
notInit,
/// The field is declared with an initializer.
initInDeclaration,
/// The field is initialized in a field formal parameter of the constructor
/// being verified.
initInFieldFormal,
/// The field is initialized in the list of initializers of the constructor
/// being verified.
initInInitializer,
}
class _Interface {
final TypeSystemImpl typeSystem;
final InterfaceElement element;
/// [_InitState.notInit] or [_InitState.initInDeclaration] for each field
/// in [element]. This map works as the initial state for
/// [_Constructor].
final Map<FieldElement, _InitState> fields;
final Map<ConstructorElement, _Constructor> constructors = Map.identity();
_Interface({
required this.typeSystem,
required this.element,
required this.fields,
});
_Constructor forConstructor({
required DiagnosticReporter diagnosticReporter,
required ConstructorDeclaration node,
required ConstructorFragment fragment,
}) {
var element = fragment.element;
return constructors[element] ??= _Constructor(
typeSystem: typeSystem,
diagnosticReporter: diagnosticReporter,
node: node,
element: element,
fields: {...fields},
);
}
}