Record elements and types parts of missing patterns for AddMissingSwitchCases fix.

Bug: https://github.com/dart-lang/sdk/issues/51985
Change-Id: I48e04e992ccab5cb5dedb5df328ba13ffcf6b560
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/302200
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
Konstantin Shcheglov 2023-05-09 16:29:20 +00:00 committed by Commit Queue
parent b1c110bd17
commit 4f7295ad26
10 changed files with 354 additions and 67 deletions

View file

@ -5,7 +5,10 @@
import 'package:analysis_server/src/services/correction/dart/abstract_producer.dart';
import 'package:analysis_server/src/services/correction/fix.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/src/generated/exhaustiveness.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
class AddMissingSwitchCases extends CorrectionProducer {
@ -22,8 +25,13 @@ class AddMissingSwitchCases extends CorrectionProducer {
Future<void> compute(ChangeBuilder builder) async {
final node = this.node;
final patternCode = _patternSuggestion();
if (patternCode == null) {
final diagnostic = this.diagnostic;
if (diagnostic is! AnalysisError) {
return;
}
final patternParts = diagnostic.data;
if (patternParts is! List<MissingPatternPart>) {
return;
}
@ -31,7 +39,7 @@ class AddMissingSwitchCases extends CorrectionProducer {
await _switchExpression(
builder: builder,
node: node,
patternCode: patternCode,
patternParts: patternParts,
);
}
@ -39,38 +47,15 @@ class AddMissingSwitchCases extends CorrectionProducer {
await _switchStatement(
builder: builder,
node: node,
patternCode: patternCode,
patternParts: patternParts,
);
}
}
/// Extracts the pattern code suggestion from the correction message.
///
/// TODO(scheglov) In general, this code might be not always valid code.
String? _patternSuggestion() {
final diagnostic = this.diagnostic;
if (diagnostic == null) {
return null;
}
final correctionMessage = diagnostic.correctionMessage;
if (correctionMessage == null) {
return null;
}
final regExp = RegExp("that match '(.+)'");
final match = regExp.firstMatch(correctionMessage);
if (match == null) {
return null;
}
return match.group(1);
}
Future<void> _switchExpression({
required ChangeBuilder builder,
required SwitchExpression node,
required String patternCode,
required List<MissingPatternPart> patternParts,
}) async {
final lineIndent = utils.getLinePrefix(node.offset);
final singleIndent = utils.getIndent(1);
@ -88,7 +73,7 @@ class AddMissingSwitchCases extends CorrectionProducer {
builder.writeln('// TODO: Handle this case.');
builder.write(lineIndent);
builder.write(singleIndent);
builder.write(patternCode);
_writePatternParts(builder, patternParts);
builder.writeln(' => null,');
builder.write(location.suffix);
});
@ -98,7 +83,7 @@ class AddMissingSwitchCases extends CorrectionProducer {
Future<void> _switchStatement({
required ChangeBuilder builder,
required SwitchStatement node,
required String patternCode,
required List<MissingPatternPart> patternParts,
}) async {
final lineIndent = utils.getLinePrefix(node.offset);
final singleIndent = utils.getIndent(1);
@ -114,7 +99,7 @@ class AddMissingSwitchCases extends CorrectionProducer {
builder.write(lineIndent);
builder.write(singleIndent);
builder.write('case ');
builder.write(patternCode);
_writePatternParts(builder, patternParts);
builder.writeln(':');
builder.write(lineIndent);
builder.write(singleIndent);
@ -124,4 +109,21 @@ class AddMissingSwitchCases extends CorrectionProducer {
});
});
}
void _writePatternParts(
DartEditBuilder builder,
List<MissingPatternPart> parts,
) {
for (final part in parts) {
if (part is MissingPatternEnumValuePart) {
builder.writeReference(part.enumElement);
builder.write('.');
builder.write(part.value.name);
} else if (part is MissingPatternTextPart) {
builder.write(part.text);
} else if (part is MissingPatternTypePart) {
builder.writeType(part.type);
}
}
}
}

View file

@ -347,6 +347,9 @@ class MockAnalysisError implements engine.AnalysisError {
@override
String? get correctionMessage => _correctionMessage;
@override
Object? get data => throw UnimplementedError();
@override
engine.ErrorCode get errorCode => _errorCode!;

View file

@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/src/services/correction/fix.dart';
import 'package:analysis_server/src/services/linter/lint_names.dart';
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
@ -10,16 +11,55 @@ import 'fix_processor.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(AddMissingSwitchCasesTest);
defineReflectiveTests(AddMissingSwitchCasesTest_SwitchExpression);
defineReflectiveTests(AddMissingSwitchCasesTest_SwitchStatement);
});
}
@reflectiveTest
class AddMissingSwitchCasesTest extends FixProcessorTest {
class AddMissingSwitchCasesTest_SwitchExpression extends FixProcessorTest {
@override
FixKind get kind => DartFixKind.ADD_MISSING_SWITCH_CASES;
Future<void> test_switchExpression_enum_hasFirst() async {
Future<void> test_bool_hasFalse() async {
await resolveTestCode('''
int f(bool x) {
return switch (x) {
false => 0,
};
}
''');
await assertHasFix('''
int f(bool x) {
return switch (x) {
false => 0,
// TODO: Handle this case.
true => null,
};
}
''');
}
Future<void> test_bool_hasTrue() async {
await resolveTestCode('''
int f(bool x) {
return switch (x) {
true => 0,
};
}
''');
await assertHasFix('''
int f(bool x) {
return switch (x) {
true => 0,
// TODO: Handle this case.
false => null,
};
}
''');
}
Future<void> test_enum_hasFirst() async {
await resolveTestCode('''
enum E {
first, second, third
@ -46,7 +86,74 @@ int f(E x) {
''');
}
Future<void> test_switchExpression_num_anyDouble_intProperty() async {
Future<void> test_enum_importedWithPrefix() async {
newFile('$testPackageLibPath/a.dart', r'''
enum E {
first, second, third
}
''');
await resolveTestCode('''
import 'a.dart' as prefix;
int f(prefix.E x) {
return switch (x) {
};
}
''');
await assertHasFix('''
import 'a.dart' as prefix;
int f(prefix.E x) {
return switch (x) {
// TODO: Handle this case.
prefix.E.first => null,
};
}
''');
}
Future<void> test_enum_notImported() async {
newFile('$testPackageLibPath/a.dart', r'''
enum E {
first, second, third
}
''');
newFile('$testPackageLibPath/b.dart', r'''
import 'a.dart';
var value = E.first;
''');
createAnalysisOptionsFile(
lints: [
LintNames.prefer_relative_imports,
],
);
await resolveTestCode('''
import 'b.dart';
int f() {
return switch (value) {
};
}
''');
await assertHasFix('''
import 'a.dart';
import 'b.dart';
int f() {
return switch (value) {
// TODO: Handle this case.
E.first => null,
};
}
''');
}
Future<void> test_num_anyDouble_intProperty() async {
await resolveTestCode('''
int f(num x) {
return switch (x) {
@ -67,7 +174,7 @@ int f(num x) {
''');
}
Future<void> test_switchExpression_num_doubleAny() async {
Future<void> test_num_doubleAny() async {
await resolveTestCode('''
int f(num x) {
return switch (x) {
@ -86,7 +193,30 @@ int f(num x) {
''');
}
Future<void> test_switchExpression_num_doubleAny_intWhen() async {
Future<void> test_num_doubleAny_coreWithPrefix() async {
await resolveTestCode('''
import 'dart:core' as core;
core.int f(core.num x) {
return switch (x) {
core.double() => 0,
};
}
''');
await assertHasFix('''
import 'dart:core' as core;
core.int f(core.num x) {
return switch (x) {
core.double() => 0,
// TODO: Handle this case.
core.int() => null,
};
}
''');
}
Future<void> test_num_doubleAny_intWhen() async {
await resolveTestCode('''
int f(num x) {
return switch (x) {
@ -107,7 +237,7 @@ int f(num x) {
''');
}
Future<void> test_switchExpression_num_empty() async {
Future<void> test_num_empty() async {
await resolveTestCode('''
int f(num x) {
return switch (x) {};
@ -122,8 +252,14 @@ int f(num x) {
}
''');
}
}
Future<void> test_switchStatement_num_doubleAny() async {
@reflectiveTest
class AddMissingSwitchCasesTest_SwitchStatement extends FixProcessorTest {
@override
FixKind get kind => DartFixKind.ADD_MISSING_SWITCH_CASES;
Future<void> test_num_doubleAny() async {
await resolveTestCode('''
void f(num x) {
switch (x) {
@ -144,7 +280,7 @@ void f(num x) {
''');
}
Future<void> test_switchStatement_num_doubleAny_intProperty() async {
Future<void> test_num_doubleAny_intProperty() async {
await resolveTestCode('''
void f(num x) {
switch (x) {
@ -169,7 +305,7 @@ void f(num x) {
''');
}
Future<void> test_switchStatement_num_empty() async {
Future<void> test_num_empty() async {
await resolveTestCode('''
void f(num x) {
switch (x) {}

View file

@ -84,6 +84,9 @@ class AnalysisError implements Diagnostic {
/// if there are no context messages.
final List<DiagnosticMessage> _contextMessages;
/// Data associated with this error, specific for [errorCode].
final Object? data;
/// The correction to be displayed for this error, or `null` if there is no
/// correction information for this error.
String? _correctionMessage;
@ -104,6 +107,7 @@ class AnalysisError implements Diagnostic {
ErrorCode errorCode, [
List<Object?>? arguments,
List<DiagnosticMessage> contextMessages = const [],
Object? data,
]) {
return AnalysisError.tmp(
source: source,
@ -112,6 +116,7 @@ class AnalysisError implements Diagnostic {
errorCode: errorCode,
arguments: arguments ?? const [],
contextMessages: contextMessages,
data: data,
);
}
@ -124,6 +129,7 @@ class AnalysisError implements Diagnostic {
required String message,
String? correctionMessage,
List<DiagnosticMessage> contextMessages = const [],
this.data,
}) : _correctionMessage = correctionMessage,
_contextMessages = contextMessages {
_problemMessage = DiagnosticMessageImpl(
@ -147,6 +153,7 @@ class AnalysisError implements Diagnostic {
required this.errorCode,
List<Object?> arguments = const [],
List<DiagnosticMessage> contextMessages = const [],
this.data,
}) : _contextMessages = contextMessages {
assert(
arguments.length == errorCode.numParameters,

View file

@ -97,16 +97,27 @@ class ErrorReporter {
/// Report an error with the given [errorCode] and [arguments].
/// The [node] is used to compute the location of the error.
void reportErrorForNode(ErrorCode errorCode, AstNode node,
[List<Object>? arguments, List<DiagnosticMessage>? messages]) {
void reportErrorForNode(
ErrorCode errorCode,
AstNode node, [
List<Object>? arguments,
List<DiagnosticMessage>? messages,
Object? data,
]) {
reportErrorForOffset(
errorCode, node.offset, node.length, arguments, messages);
errorCode, node.offset, node.length, arguments, messages, data);
}
/// Report an error with the given [errorCode] and [arguments]. The location
/// of the error is specified by the given [offset] and [length].
void reportErrorForOffset(ErrorCode errorCode, int offset, int length,
[List<Object>? arguments, List<DiagnosticMessage>? messages]) {
void reportErrorForOffset(
ErrorCode errorCode,
int offset,
int length, [
List<Object>? arguments,
List<DiagnosticMessage>? messages,
Object? data,
]) {
if (lockLevel != 0) {
return;
}
@ -122,6 +133,7 @@ class ErrorReporter {
errorCode: errorCode,
arguments: arguments ?? const [],
contextMessages: messages,
data: data,
),
);
}
@ -135,10 +147,15 @@ class ErrorReporter {
/// Report an error with the given [errorCode] and [arguments]. The [token] is
/// used to compute the location of the error.
void reportErrorForToken(ErrorCode errorCode, Token token,
[List<Object>? arguments, List<DiagnosticMessage>? messages]) {
void reportErrorForToken(
ErrorCode errorCode,
Token token, [
List<Object>? arguments,
List<DiagnosticMessage>? messages,
Object? data,
]) {
reportErrorForOffset(
errorCode, token.offset, token.length, arguments, messages);
errorCode, token.offset, token.length, arguments, messages, data);
}
/// Report an error with the given [errorCode] and [arguments]. The [node] is

View file

@ -1346,8 +1346,8 @@ class AnalysisDriver implements AnalysisDriverGeneric {
testingData: testingData,
).analyze();
late UnitAnalysisResult fileResult;
late Uint8List bytes;
late CompilationUnit resolvedUnit;
for (var unitResult in results) {
var unitBytes =
_serializeResolvedUnit(unitResult.unit, unitResult.errors);
@ -1356,16 +1356,21 @@ class AnalysisDriver implements AnalysisDriverGeneric {
String unitKey = _getResolvedUnitKey(unitSignature);
_byteStore.putGet(unitKey, unitBytes);
if (unitResult.file == file) {
fileResult = unitResult;
bytes = unitBytes;
resolvedUnit = unitResult.unit;
}
}
// Return the result, full or partial.
_logger.writeln('Computed new analysis result.');
var result = _getAnalysisResultFromBytes(file, signature, bytes,
content: withUnit ? file.content : null,
resolvedUnit: withUnit ? resolvedUnit : null);
var result = _getAnalysisResultFromBytes(
file,
signature,
bytes,
content: withUnit ? file.content : null,
resolvedUnit: withUnit ? fileResult.unit : null,
errors: fileResult.errors,
);
if (withUnit && _priorityFiles.contains(path)) {
_priorityResults[path] = result.unitResult!;
}
@ -1588,10 +1593,15 @@ class AnalysisDriver implements AnalysisDriverGeneric {
/// Load the [AnalysisResult] for the given [file] from the [bytes]. Set
/// optional [content] and [resolvedUnit].
AnalysisResult _getAnalysisResultFromBytes(
FileState file, String signature, Uint8List bytes,
{String? content, CompilationUnit? resolvedUnit}) {
FileState file,
String signature,
Uint8List bytes, {
String? content,
CompilationUnit? resolvedUnit,
List<AnalysisError>? errors,
}) {
var unit = AnalysisDriverResolvedUnit.fromBuffer(bytes);
List<AnalysisError> errors = _getErrorsFromSerialized(file, unit.errors);
errors ??= _getErrorsFromSerialized(file, unit.errors);
_updateHasErrorOrWarningFlag(file, errors);
var index = unit.index!;
if (content != null && resolvedUnit != null) {

View file

@ -840,19 +840,24 @@ class ConstantVerifier extends RecursiveAstVisitor<void> {
errorToken,
);
} else if (error is NonExhaustiveError && reportNonExhaustive) {
// TODO(paulberry): instead of using SimpleDartBuffer, use an
// analyzer-specific class that implements DartBuffer and captures the
// information needed for quick assists.
var errorBuffer = SimpleDartBuffer();
error.witness.toDart(errorBuffer, forCorrection: false);
var correctionBuffer = SimpleDartBuffer();
error.witness.toDart(correctionBuffer, forCorrection: true);
var correctionTextBuffer = SimpleDartBuffer();
var correctionDataBuffer = AnalyzerDartTemplateBuffer();
error.witness.toDart(correctionTextBuffer, forCorrection: true);
error.witness.toDart(correctionDataBuffer, forCorrection: true);
_errorReporter.reportErrorForToken(
isSwitchExpression
? CompileTimeErrorCode.NON_EXHAUSTIVE_SWITCH_EXPRESSION
: CompileTimeErrorCode.NON_EXHAUSTIVE_SWITCH_STATEMENT,
switchKeyword,
[scrutineeType, errorBuffer.toString(), correctionBuffer.toString()],
[
scrutineeType,
errorBuffer.toString(),
correctionTextBuffer.toString(),
],
[],
correctionDataBuffer.isComplete ? correctionDataBuffer.parts : null,
);
}
}

View file

@ -2,6 +2,7 @@
// 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:_fe_analyzer_shared/src/exhaustiveness/dart_template_buffer.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/key.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/path.dart';
@ -23,6 +24,64 @@ import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/dart/resolver/variance.dart';
import 'package:analyzer/src/generated/constant.dart';
/// The buffer that accumulates types and elements as is, so that they
/// can be written latter into Dart code that considers imports. It also
/// accumulates fragments of text, such as syntax `(`, or names of properties.
class AnalyzerDartTemplateBuffer
implements DartTemplateBuffer<DartObject, FieldElement, DartType> {
final List<MissingPatternPart> parts = [];
bool isComplete = true;
@override
void write(String text) {
parts.add(
MissingPatternTextPart(text),
);
}
@override
void writeBoolValue(bool value) {
parts.add(
MissingPatternTextPart('$value'),
);
}
@override
void writeCoreType(String name) {
parts.add(
MissingPatternTextPart(name),
);
}
@override
void writeEnumValue(FieldElement value, String name) {
final enumElement = value.enclosingElement;
if (enumElement is! EnumElement) {
isComplete = false;
return;
}
parts.add(
MissingPatternEnumValuePart(
enumElement: enumElement,
value: value,
),
);
}
@override
void writeGeneralConstantValue(DartObject value, String name) {
isComplete = false;
}
@override
void writeGeneralType(DartType type, String name) {
parts.add(
MissingPatternTypePart(type),
);
}
}
class AnalyzerEnumOperations
implements EnumOperations<DartType, EnumElement, FieldElement, DartObject> {
const AnalyzerEnumOperations();
@ -367,6 +426,41 @@ class ExhaustivenessDataForTesting {
ExhaustivenessDataForTesting(this.objectFieldLookup);
}
class MissingPatternEnumValuePart extends MissingPatternPart {
final EnumElement enumElement;
final FieldElement value;
MissingPatternEnumValuePart({
required this.enumElement,
required this.value,
});
@override
String toString() => value.name;
}
abstract class MissingPatternPart {}
class MissingPatternTextPart extends MissingPatternPart {
final String text;
MissingPatternTextPart(this.text);
@override
String toString() => text;
}
class MissingPatternTypePart extends MissingPatternPart {
final DartType type;
MissingPatternTypePart(this.type);
@override
String toString() {
return type.getDisplayString(withNullability: true);
}
}
class PatternConverter with SpaceCreator<DartPattern, DartType> {
final FeatureSet featureSet;
final AnalyzerExhaustivenessCache cache;

View file

@ -84,14 +84,24 @@ class CollectingReporter extends ErrorReporter {
}
@override
void reportErrorForNode(ErrorCode errorCode, AstNode node,
[List<Object?>? arguments, List<DiagnosticMessage>? messages]) {
void reportErrorForNode(
ErrorCode errorCode,
AstNode node, [
List<Object?>? arguments,
List<DiagnosticMessage>? messages,
Object? data,
]) {
code = errorCode;
}
@override
void reportErrorForToken(ErrorCode errorCode, Token token,
[List<Object?>? arguments, List<DiagnosticMessage>? messages]) {
void reportErrorForToken(
ErrorCode errorCode,
Token token, [
List<Object?>? arguments,
List<DiagnosticMessage>? messages,
Object? data,
]) {
code = errorCode;
}
}

View file

@ -37,6 +37,9 @@ class MockAnalysisError implements AnalysisError {
@override
String? get correctionMessage => null;
@override
Object? get data => throw UnimplementedError();
@override
DiagnosticMessage get problemMessage => DiagnosticMessageImpl(
filePath: source.fullName,