| // 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/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, | |
| }) { | |
| var fragment = node.declaredFragment!; | |
| if (node.factoryKeyword != null || | |
| node.factoryRedirectionTarget != null || | |
| fragment.element.isExternal) { | |
| return; | |
| } | |
| 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.allFormalParameters, | |
| ); | |
| } | |
| 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.allFormalParameters, | |
| ); | |
| 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.fieldName2; | |
| var fieldElement = initializer.fieldElement; | |
| 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(List<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, | |
| ); | |
| } | |
| } |