blob: c2c23e3b0a26fffb66b2733bebc75497ab5463ad [file]
// 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/source/source_range.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/diagnostic/diagnostic.dart' as diag;
import 'package:analyzer/src/error/listener.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,
ClassNamePartImpl primaryConstructor,
) {
var interfaceFields = _forInterface(element);
if (primaryConstructor is PrimaryConstructorDeclarationImpl) {
_addPrimaryConstructor(
diagnosticReporter: diagnosticReporter,
interfaceFields: interfaceFields,
primaryConstructor: primaryConstructor,
);
}
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,
element: fragment.element,
errorRange: node.errorRange,
isPrimary: false,
);
constructorState.secondaryConstructors.add(node);
if (!fragment.isAugmentation) {
constructorState.updateWithParameters(node.parameters.parameters);
}
constructorState.updateWithInitializers(
diagnosticReporter,
node.initializers,
);
}
void _addPrimaryConstructor({
required DiagnosticReporter diagnosticReporter,
required _Interface interfaceFields,
required PrimaryConstructorDeclarationImpl primaryConstructor,
}) {
var fragment = primaryConstructor.declaredFragment!;
var constructorState = interfaceFields.forConstructor(
diagnosticReporter: diagnosticReporter,
element: fragment.element,
errorRange: primaryConstructor.errorRange,
isPrimary: true,
);
constructorState.primaryConstructors.add(primaryConstructor);
constructorState.updateWithParameters(
primaryConstructor.formalParameters.parameters,
);
var body = primaryConstructor.body;
if (body != null) {
constructorState.updateWithInitializers(
diagnosticReporter,
body.initializers,
);
}
}
_Interface _forInterface(InterfaceElement element) {
if (_interfaces[element] case var result?) {
return result;
}
var fieldMap = <FieldElement, _InitState>{};
var fieldNameCounts = <String, int>{};
for (var field in element.fields) {
if (field.isOriginGetterSetter) {
continue;
}
if (element is EnumElement && field.name == 'index') {
continue;
}
fieldMap[field] = field.hasInitializer
? _InitState.initInDeclaration
: _InitState.notInit;
if (field.name case var name?) {
fieldNameCounts.update(name, (count) => count + 1, ifAbsent: () => 1);
}
}
return _interfaces[element] = _Interface(
typeSystem: typeSystem,
element: element,
fields: fieldMap,
duplicateFieldNames: {
for (var entry in fieldNameCounts.entries)
if (entry.value > 1) entry.key,
},
);
}
}
class _Constructor {
final TypeSystemImpl typeSystem;
final DiagnosticReporter diagnosticReporter;
final SourceRange errorRange;
final Map<FieldElement, _InitState> fields;
final Set<String> duplicateFieldNames;
final bool isPrimary;
final List<PrimaryConstructorDeclarationImpl> primaryConstructors = [];
final List<ConstructorDeclarationImpl> secondaryConstructors = [];
/// Set to `true` if the constructor redirects.
bool hasRedirectingConstructorInvocation = false;
_Constructor({
required this.typeSystem,
required this.diagnosticReporter,
required this.errorRange,
required this.fields,
required this.duplicateFieldNames,
required this.isPrimary,
});
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.name;
if (name == null) return;
if (duplicateFieldNames.contains(name)) return;
if (field.isFinal) {
notInitFinalFields.add(_Field(field, name));
} else if (typeSystem.isPotentiallyNonNullable(field.type)) {
notInitNonNullableFields.add(_Field(field, name));
}
});
var allNotInitialized = <FieldElement>[
for (var f in notInitFinalFields) f.element,
for (var f in notInitNonNullableFields) f.element,
];
for (var node in primaryConstructors) {
node.notInitializedFields = allNotInitialized;
}
for (var node in secondaryConstructors) {
node.notInitializedFields = allNotInitialized;
}
reportNotInitializedFinal(notInitFinalFields);
reportNotInitializedNonNullable(notInitNonNullableFields);
}
void reportNotInitializedFinal(List<_Field> notInitFinalFields) {
if (notInitFinalFields.isEmpty) {
return;
}
var names = notInitFinalFields.map((f) => f.name).toList();
names.sort();
diagnosticReporter.report(
switch (names) {
// `names` can't be empty since its based on `notInitFinalFields`, which
// is not empty (see `if` test above).
[] => throw StateError('unexpectedly empty name list'),
[var name] => diag.finalNotInitializedConstructor1.withArguments(
name: name,
),
[var name1, var name2] =>
diag.finalNotInitializedConstructor2.withArguments(
name1: name1,
name2: name2,
),
[var name1, var name2, ...var remaining] =>
diag.finalNotInitializedConstructor3Plus.withArguments(
name1: name1,
name2: name2,
remainingCount: remaining.length,
),
}.atSourceRange(errorRange),
);
}
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.report(
diag.notInitializedNonNullableInstanceFieldConstructor
.withArguments(name: name)
.atSourceRange(errorRange),
);
}
}
void updateWithInitializers(
DiagnosticReporter diagnosticReporter,
NodeList<ConstructorInitializer> initializers,
) {
for (var initializer in 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 (isPrimary) {
diagnosticReporter.report(
diag.fieldInitializedInDeclarationAndInitializerOfPrimaryConstructor
.at(fieldName),
);
} else if (fieldElement.isFinal || fieldElement.isConst) {
diagnosticReporter.report(
diag.fieldInitializedInInitializerAndDeclaration.at(fieldName),
);
}
fields[fieldElement] = _InitState.initInInitializer;
} else if (state == _InitState.initInFieldFormal) {
diagnosticReporter.report(
diag.fieldInitializedInParameterAndInitializer.at(fieldName),
);
} else if (state == _InitState.initInInitializer) {
diagnosticReporter.report(
diag.fieldInitializedByMultipleInitializers
.withArguments(name: fieldElement.displayName)
.at(fieldName),
);
}
}
}
}
}
void updateWithParameters(NodeList<FormalParameter> formalParameters) {
for (var formalParameter in formalParameters) {
var parameterFragment = formalParameter.declaredFragment!;
var parameterElement = parameterFragment.element;
if (parameterElement is FieldFormalParameterElement) {
var fieldElement = parameterElement.field;
if (fieldElement == null) {
continue;
}
_InitState? state = fields[fieldElement];
if (state == _InitState.notInit) {
fields[fieldElement] = _InitState.initInFieldFormal;
} else if (state == _InitState.initInDeclaration) {
if (isPrimary) {
if (formalParameter.name case var name?) {
diagnosticReporter.report(
diag.fieldInitializedInDeclarationAndParameterOfPrimaryConstructor
.at(name),
);
}
} else if (fieldElement.isFinal || fieldElement.isConst) {
if (formalParameter.name case var name?) {
diagnosticReporter.report(
diag.finalInitializedInDeclarationAndConstructor
.withArguments(name: fieldElement.displayName)
.at(name),
);
}
}
fields[fieldElement] = _InitState.initInFieldFormal;
} 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;
final Set<String> duplicateFieldNames;
/// [_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,
required this.duplicateFieldNames,
});
_Constructor forConstructor({
required DiagnosticReporter diagnosticReporter,
required SourceRange errorRange,
required ConstructorElement element,
required bool isPrimary,
}) {
return constructors[element] ??= _Constructor(
typeSystem: typeSystem,
diagnosticReporter: diagnosticReporter,
errorRange: errorRange,
fields: {...fields},
duplicateFieldNames: duplicateFieldNames,
isPrimary: isPrimary,
);
}
}