blob: 05e40efb635d2d5a103de0495d5f3be4d7b856b9 [file] [log] [blame]
// Copyright (c) 2022, 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:analysis_server/src/services/snippets/dart_snippet_request.dart';
import 'package:analysis_server/src/services/snippets/snippet.dart';
import 'package:analysis_server_plugin/edit/correction_utils.dart';
import 'package:analyzer/dart/analysis/code_style_options.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/analysis/session_helper.dart';
import 'package:analyzer/src/utilities/extensions/ast.dart';
import 'package:analyzer/src/utilities/extensions/flutter.dart';
import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_dart.dart'
show DartFileEditBuilderImpl;
import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
import 'package:meta/meta.dart';
abstract class DartSnippetProducer extends SnippetProducer {
final AnalysisSessionHelper sessionHelper;
final CorrectionUtils utils;
final LibraryElement2 libraryElement;
final bool useSuperParams;
/// A cache of mappings from Elements to their public Library Elements.
///
/// Callers can share this cache across multiple snippet producers to avoid
/// repeated searches where they may add imports for the same elements.
final Map<Element2, LibraryElement2?> _elementImportCache;
DartSnippetProducer(
super.request, {
required Map<Element2, LibraryElement2?> elementImportCache,
}) : sessionHelper = AnalysisSessionHelper(request.analysisSession),
utils = CorrectionUtils(request.unit),
libraryElement = request.unit.libraryElement2,
useSuperParams = request.unit.libraryElement2.featureSet.isEnabled(
Feature.super_parameters,
),
_elementImportCache = elementImportCache;
CodeStyleOptions get codeStyleOptions =>
sessionHelper.session.analysisContext
.getAnalysisOptionsForFile(request.unit.file)
.codeStyleOptions;
bool get isInTestDirectory => request.unit.unit.inTestDir;
}
abstract class FlutterSnippetProducer extends DartSnippetProducer {
late ClassElement2? _classWidget;
late ClassElement2? _classPlaceholder;
/// Elements that need to be imported for generated code to be valid.
///
/// Calling [addImports] will add any required imports to the supplied
/// builder.
final Set<Element2> _requiredElementImports = {};
FlutterSnippetProducer(super.request, {required super.elementImportCache});
/// Adds public imports for any elements fetched by [getClass] and [getMixin]
/// to [builder].
Future<void> addImports(DartFileEditBuilder builder) async {
var dartBuilder = builder as DartFileEditBuilderImpl;
await Future.wait(
_requiredElementImports.map(
(element) => dartBuilder.importElementLibrary(
element,
resultCache: _elementImportCache,
),
),
);
}
Future<ClassElement2?> getClass(String name) async {
var class_ = await sessionHelper.getFlutterClass(name);
if (class_ != null) {
_requiredElementImports.add(class_);
}
return class_;
}
Future<MixinElement2?> getMixin(String name) async {
var mixin = await sessionHelper.getMixin(widgetsUri, name);
if (mixin != null) {
_requiredElementImports.add(mixin);
}
return mixin;
}
DartType getType(
InterfaceElement2 classElement, [
NullabilitySuffix nullabilitySuffix = NullabilitySuffix.none,
]) => classElement.instantiate(
typeArguments: const [],
nullabilitySuffix: nullabilitySuffix,
);
@override
@mustCallSuper
Future<bool> isValid() async {
if ((_classWidget = await getClass('Widget')) == null) {
return false;
}
if ((_classPlaceholder = await getClass('Placeholder')) == null) {
return false;
}
return super.isValid();
}
}
/// A mixin that provides some common methods for producers that build snippets
/// for Flutter widget classes.
mixin FlutterWidgetSnippetProducerMixin on FlutterSnippetProducer {
ClassElement2? get classBuildContext;
ClassElement2? get classKey;
String get widgetClassName => 'MyWidget';
void writeBuildMethod(DartEditBuilder builder) {
// Checked by isValid() before this will be called.
var classBuildContext = this.classBuildContext!;
var classWidget = _classWidget!;
var classPlaceholder = _classPlaceholder!;
// Add the build method.
builder.writeln(' @override');
builder.write(' ');
builder.writeFunctionDeclaration(
'build',
returnType: getType(classWidget),
parameterWriter: () {
builder.writeParameter('context', type: getType(classBuildContext));
},
bodyWriter: () {
builder.writeln('{');
builder.write(' return ');
builder.selectAll(() {
builder.write('const ');
builder.writeType(getType(classPlaceholder));
builder.write('()');
});
builder.writeln(';');
builder.writeln(' }');
},
);
}
void writeCreateStateMethod(DartEditBuilder builder) {
builder.writeln(' @override');
builder.write(' State<');
builder.addSimpleLinkedEdit('name', widgetClassName);
builder.write('> createState() => _');
builder.addSimpleLinkedEdit('name', widgetClassName);
builder.writeln('State();');
}
void writeWidgetConstructor(DartEditBuilder builder) {
// Checked by isValid() before this will be called.
var classKey = this.classKey!;
String keyName;
DartType? keyType;
void Function()? keyInitializer;
if (useSuperParams) {
keyName = 'super.key';
} else {
keyName = 'key';
keyType = getType(classKey, NullabilitySuffix.question);
keyInitializer = () => builder.write('super(key: key)');
}
builder.write(' ');
builder.writeConstructorDeclaration(
widgetClassName,
classNameGroupName: 'name',
isConst: true,
parameterWriter: () {
builder.write('{');
builder.writeParameter(keyName, type: keyType);
builder.write('}');
},
initializerWriter: keyInitializer,
);
}
}
abstract class SnippetProducer {
final DartSnippetRequest request;
SnippetProducer(this.request);
/// The prefix a user types to use this snippet.
String get snippetPrefix;
Future<Snippet> compute();
Future<bool> isValid() async {
// File edit builders will not produce edits for files outside of the
// analysis roots so we should not try to produce any snippets.
var analysisContext = request.analysisSession.analysisContext;
return analysisContext.contextRoot.isAnalyzed(request.filePath);
}
}