|
| 1 | +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file |
| 2 | +// for details. All rights reserved. Use of this source code is governed by a |
| 3 | +// BSD-style license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +import 'package:analysis_server/src/services/correction/assist.dart'; |
| 6 | +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; |
| 7 | +import 'package:analyzer/dart/ast/ast.dart'; |
| 8 | +import 'package:analyzer/dart/element/element.dart'; |
| 9 | +import 'package:analyzer/dart/element/type.dart'; |
| 10 | +import 'package:analyzer/source/source_range.dart'; |
| 11 | +import 'package:analyzer/src/dart/ast/ast.dart'; |
| 12 | +import 'package:analyzer_plugin/utilities/assist/assist.dart'; |
| 13 | +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; |
| 14 | +import 'package:analyzer_plugin/utilities/range_factory.dart'; |
| 15 | +import 'package:collection/collection.dart'; |
| 16 | + |
| 17 | +import 'create_constructor.dart'; |
| 18 | + |
| 19 | +/// Binds a constructor parameter to a newly created field. |
| 20 | +/// |
| 21 | +/// This assist is useful for simplifying constructor declarations by |
| 22 | +/// automatically converting a regular parameter into a `this.` field formal |
| 23 | +/// parameter and declaring the corresponding field. This matches a workflow |
| 24 | +/// with the [CreateConstructor] assist. |
| 25 | +class BindToField extends ResolvedCorrectionProducer { |
| 26 | + BindToField({required super.context}); |
| 27 | + |
| 28 | + @override |
| 29 | + CorrectionApplicability get applicability => |
| 30 | + CorrectionApplicability.singleLocation; |
| 31 | + |
| 32 | + @override |
| 33 | + AssistKind get assistKind => DartAssistKind.bindAllToFields; |
| 34 | + |
| 35 | + @override |
| 36 | + Future<void> compute(ChangeBuilder builder) async { |
| 37 | + var parameter = node.thisOrAncestorOfType<FormalParameter>(); |
| 38 | + if (parameter != null) { |
| 39 | + // Don't propose an assist for super parameters and super parameters with |
| 40 | + // a default value because they can't be bound to a field. |
| 41 | + await tryReplacingParameter(file, builder, parameter, libraryElement2); |
| 42 | + } |
| 43 | + } |
| 44 | + |
| 45 | + static Future<void> tryReplacingParameter( |
| 46 | + String file, |
| 47 | + ChangeBuilder builder, |
| 48 | + FormalParameter parameter, |
| 49 | + LibraryElement libraryElement2, |
| 50 | + ) async { |
| 51 | + if (parameter is SuperFormalParameter) { |
| 52 | + return; |
| 53 | + } |
| 54 | + if (parameter is DefaultFormalParameter && |
| 55 | + parameter.parameter is SuperFormalParameter) { |
| 56 | + return; |
| 57 | + } |
| 58 | + if (parameter is FieldFormalParameter) { |
| 59 | + return; |
| 60 | + } |
| 61 | + await _replaceParameterWithThis(file, builder, parameter, libraryElement2); |
| 62 | + } |
| 63 | + |
| 64 | + static SourceRange _replaceable(FormalParameter parameter) { |
| 65 | + return switch (parameter) { |
| 66 | + SimpleFormalParameter() => range.startEnd( |
| 67 | + parameter.type ?? parameter.keyword ?? parameter.name!, |
| 68 | + parameter, |
| 69 | + ), |
| 70 | + DefaultFormalParameter() => _replaceable(parameter.parameter), |
| 71 | + FunctionTypedFormalParameter() => range.node(parameter), |
| 72 | + // Should not happen, as these are excluded, but better to not crash. |
| 73 | + FieldFormalParameter() || SuperFormalParameter() => range.node(parameter), |
| 74 | + }; |
| 75 | + } |
| 76 | + |
| 77 | + static Future<void> _replaceParameterWithThis( |
| 78 | + String file, |
| 79 | + ChangeBuilder builder, |
| 80 | + FormalParameter parameter, |
| 81 | + LibraryElement libraryElement2, |
| 82 | + ) async { |
| 83 | + var constructor = parameter.thisOrAncestorOfType<ConstructorDeclaration>(); |
| 84 | + if (constructor == null) { |
| 85 | + return; |
| 86 | + } |
| 87 | + if (constructor.childEntities.any( |
| 88 | + (element) => element is RedirectingConstructorInvocation, |
| 89 | + )) { |
| 90 | + return; |
| 91 | + } |
| 92 | + if (constructor.factoryKeyword != null) { |
| 93 | + return; |
| 94 | + } |
| 95 | + var container = constructor.thisOrAncestorOfType<CompilationUnitMember>(); |
| 96 | + if (container == null) { |
| 97 | + return; |
| 98 | + } |
| 99 | + if (container is! ClassDeclaration && container is! EnumDeclaration) { |
| 100 | + return; |
| 101 | + } |
| 102 | + if (container is ClassDeclaration && |
| 103 | + container.members.any( |
| 104 | + (member) => switch (member) { |
| 105 | + ConstructorDeclaration() => false, |
| 106 | + FieldDeclaration() => false, |
| 107 | + MethodDeclaration() => member.name.lexeme == parameter.name?.lexeme, |
| 108 | + }, |
| 109 | + )) { |
| 110 | + return; |
| 111 | + } |
| 112 | + if (container is EnumDeclaration && |
| 113 | + container.constants.any( |
| 114 | + (constant) => constant.name.lexeme == parameter.name?.lexeme, |
| 115 | + )) { |
| 116 | + return; |
| 117 | + } |
| 118 | + |
| 119 | + await builder.addDartFileEdit(file, (builder) { |
| 120 | + var name = parameter.name; |
| 121 | + if (name != null) { |
| 122 | + DartType? type = parameter.declaredFragment?.element.type; |
| 123 | + var fixType = _whetherToCreateNewField( |
| 124 | + container, |
| 125 | + parameter, |
| 126 | + type, |
| 127 | + libraryElement2, |
| 128 | + ); |
| 129 | + if (fixType != _FixType.noop) { |
| 130 | + builder.addSimpleReplacement( |
| 131 | + _replaceable(parameter), |
| 132 | + 'this.${name.lexeme}', |
| 133 | + ); |
| 134 | + } |
| 135 | + if (fixType == _FixType.replaceWithThisAndNewField) { |
| 136 | + builder.insertField(container, (builder) { |
| 137 | + var isFinal = constructor.constKeyword != null || parameter.isFinal; |
| 138 | + builder.writeFieldDeclaration( |
| 139 | + name.lexeme, |
| 140 | + isFinal: isFinal, |
| 141 | + nameGroupName: 'NAME', |
| 142 | + type: isFinal && type is DynamicType ? null : type, |
| 143 | + typeGroupName: 'TYPE', |
| 144 | + ); |
| 145 | + }); |
| 146 | + } |
| 147 | + } |
| 148 | + }); |
| 149 | + } |
| 150 | + |
| 151 | + static _FixType _whetherToCreateNewField( |
| 152 | + CompilationUnitMember container, |
| 153 | + FormalParameter parameter, |
| 154 | + DartType? type, |
| 155 | + LibraryElement libraryElement2, |
| 156 | + ) { |
| 157 | + if (container is ClassDeclaration) { |
| 158 | + var fieldWithSameName = container.members |
| 159 | + .whereType<FieldDeclaration>() |
| 160 | + .map( |
| 161 | + (member) => member.fields.variables.firstWhereOrNull( |
| 162 | + (variable) => variable.name.lexeme == parameter.name?.lexeme, |
| 163 | + ), |
| 164 | + ) |
| 165 | + .firstOrNull; |
| 166 | + if (fieldWithSameName != null) { |
| 167 | + var fieldType = fieldWithSameName.declaredFragment?.element.type; |
| 168 | + if (type != null && fieldType != null) { |
| 169 | + if (libraryElement2.typeSystem.isAssignableTo(type, fieldType)) { |
| 170 | + return _FixType.replaceWithThis; |
| 171 | + } else { |
| 172 | + return _FixType.noop; |
| 173 | + } |
| 174 | + } else { |
| 175 | + return _FixType.replaceWithThis; |
| 176 | + } |
| 177 | + } else { |
| 178 | + return _FixType.replaceWithThisAndNewField; |
| 179 | + } |
| 180 | + } |
| 181 | + return _FixType.replaceWithThisAndNewField; |
| 182 | + } |
| 183 | +} |
| 184 | + |
| 185 | +enum _FixType { replaceWithThis, replaceWithThisAndNewField, noop } |
0 commit comments