blob: 6ac0487738cb4de80c631e3c81176c007d9ac721 [file] [edit]
// Copyright (c) 2026, 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/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/services/correction/fix_internal.dart';
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
import 'package:linter/src/diagnostic.dart' as diag;
import 'package:linter/src/rules.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../tool/lsp_spec/matchers.dart';
import 'server_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(GetFixesTest);
});
}
@reflectiveTest
class GetFixesTest extends AbstractLspAnalysisServerTest {
@override
void setUp() {
super.setUp();
registerLintRules();
registerBuiltInFixGenerators();
// Enable this so we can verify we get annotations back (which would allow
// grouping, etc but also show up in our edit verifier strings below).
setChangeAnnotationSupport();
}
Future<void> test_diagnosticCodes_enabled() async {
newFile(analysisOptionsPath, '''
linter:
rules:
- always_specify_types # fix
- prefer_single_quotes # don't fix
- prefer_is_empty # fix
''');
newFile(mainFilePath, '''
var a = '';
String b = "";
bool c = ''.length == 0;
''');
await initialize();
var params = DartGetWorkspaceFixesParams(
diagnosticCodes: [
diag.alwaysSpecifyTypesReplaceKeyword.lowerCaseName,
diag.preferIsEmptyUseIsEmpty.lowerCaseName,
],
);
var result = await getWorkspaceFixes(params);
// Expect fixes for always_specify_types and prefer_is_empty
// but not for prefer_single_quotes.
verifyResult(
result,
'''
>>>>>>>>>> lib/main.dart
>>>>>>>>>> Add type annotation: line 1
>>>>>>>>>> Replace with 'isEmpty': line 3
String a = '';
String b = "";
bool c = ''.isEmpty;
''',
'''
lib/main.dart:
always_specify_types: 1
prefer_is_empty: 1
''',
);
}
Future<void> test_diagnosticCodes_invalid() async {
newFile(mainFilePath, '''
var a = '';
''');
await initialize();
var params = DartGetWorkspaceFixesParams(
diagnosticCodes: ['not_a_valid_code'],
);
await expectLater(
getWorkspaceFixes(params),
throwsA(
isResponseError(
.RequestFailed,
message: "The diagnostic 'not_a_valid_code' is not defined by the analyzer.",
),
),
);
}
Future<void> test_diagnosticCodes_notEnabled() async {
newFile(analysisOptionsPath, '''
linter:
rules:
# The two lints we will fix are not enabled
- prefer_single_quotes # don't fix
''');
newFile(mainFilePath, '''
var a = '';
String b = "";
bool c = ''.length == 0;
''');
await initialize();
var params = DartGetWorkspaceFixesParams(
diagnosticCodes: [
diag.alwaysSpecifyTypesReplaceKeyword.lowerCaseName,
diag.preferIsEmptyUseIsEmpty.lowerCaseName,
],
);
var result = await getWorkspaceFixes(params);
// Expect fixes for always_specify_types and prefer_is_empty
// but not for prefer_single_quotes.
verifyResult(
result,
'''
>>>>>>>>>> lib/main.dart
>>>>>>>>>> Add type annotation: line 1
>>>>>>>>>> Replace with 'isEmpty': line 3
String a = '';
String b = "";
bool c = ''.isEmpty;
''',
'''
lib/main.dart:
always_specify_types: 1
prefer_is_empty: 1
''',
);
}
Future<void> test_iterative() async {
// Use lints that will fire on different iterations. var -> final -> const.
newFile(analysisOptionsPath, '''
linter:
rules:
- prefer_final_locals
- prefer_const_declarations
''');
newFile(mainFilePath, '''
void f() {
var a = 'test';
var b = 'test';
}
''');
await initialize();
var result = await getWorkspaceFixes();
// Expect two fixes from two different passes, merged together.
verifyResult(
result,
'''
>>>>>>>>>> lib/main.dart
>>>>>>>>>> Make final, Replace 'final' with 'const': line 2, line 3
void f() {
const a = 'test';
const b = 'test';
}
''',
'''
lib/main.dart:
prefer_final_locals: 2
prefer_const_declarations: 2
''',
);
}
Future<void> test_iterativeDartAndPubspec_ifNoCodes() async {
failTestOnErrorDiagnostic = false;
newFile(analysisOptionsPath, '''
linter:
rules:
- prefer_final_locals
- prefer_const_declarations
''');
newPubspecYamlFile(projectFolderPath, '''
name: x
''');
newFile(mainFilePath, '''
import 'package:path/path.dart' as path;
void f() {
var a = 'test';
}
''');
await initialize();
var result = await getWorkspaceFixes();
verifyResult(
result,
'''
>>>>>>>>>> lib/main.dart
>>>>>>>>>> Make final, Replace 'final' with 'const': line 4
import 'package:path/path.dart' as path;
void f() {
const a = 'test';
}
>>>>>>>>>> pubspec.yaml
>>>>>>>>>> Update pubspec with the missing dependencies: line 2
name: x
dependencies:
path: any
''',
'''
lib/main.dart:
prefer_final_locals: 1
prefer_const_declarations: 1
pubspec.yaml:
missing_dependency: 1
''',
);
}
Future<void> test_iterativeDartAndPubspec_ifSpecificCodes() async {
failTestOnErrorDiagnostic = false;
newFile(analysisOptionsPath, '''
linter:
rules:
- prefer_final_locals
- prefer_const_declarations
''');
newPubspecYamlFile(projectFolderPath, '''
name: x
''');
newFile(mainFilePath, '''
import 'package:path/path.dart' as path;
void f() {
var a = 'test';
}
''');
await initialize();
var result = await getWorkspaceFixes(
DartGetWorkspaceFixesParams(
diagnosticCodes: [
diag.preferFinalLocals.lowerCaseName,
diag.preferConstDeclarations.lowerCaseName,
diag.missingDependency.lowerCaseName,
],
),
);
verifyResult(
result,
'''
>>>>>>>>>> lib/main.dart
>>>>>>>>>> Make final, Replace 'final' with 'const': line 4
import 'package:path/path.dart' as path;
void f() {
const a = 'test';
}
>>>>>>>>>> pubspec.yaml
>>>>>>>>>> Update pubspec with the missing dependencies: line 2
name: x
dependencies:
path: any
''',
'''
lib/main.dart:
prefer_final_locals: 1
prefer_const_declarations: 1
pubspec.yaml:
missing_dependency: 1
''',
);
}
Future<void> test_multiple() async {
newFile(analysisOptionsPath, '''
linter:
rules:
- always_specify_types
- prefer_single_quotes
- prefer_is_empty
''');
newFile(mainFilePath, '''
var a = '';
String b = "";
bool c = ''.length == 0;
''');
await initialize();
var result = await getWorkspaceFixes();
// Expect fixes for all three diagnostics.
verifyResult(
result,
'''
>>>>>>>>>> lib/main.dart
>>>>>>>>>> Add type annotation: line 1
>>>>>>>>>> Convert to single quoted string: line 2, line 2
>>>>>>>>>> Replace with 'isEmpty': line 3
String a = '';
String b = '';
bool c = ''.isEmpty;
''',
'''
lib/main.dart:
always_specify_types: 1
prefer_single_quotes: 1
prefer_is_empty: 1
''',
);
}
Future<void> test_pubspec_excluded_ifNonPubspecCodes() async {
failTestOnErrorDiagnostic = false;
newPubspecYamlFile(projectFolderPath, '''
name: x
''');
newFile(mainFilePath, '''
import 'package:path/path.dart' as path;
''');
await initialize();
var params = DartGetWorkspaceFixesParams(
// Filtered to another code means we shouldn't get the pubspec fix.
diagnosticCodes: [diag.preferSingleQuotes.lowerCaseName],
);
var result = await getWorkspaceFixes(params);
expect(result.edit, isNull);
}
Future<void> test_pubspec_included_ifCode_migrateDesignWidgets() async {
failTestOnErrorDiagnostic = false;
newPubspecYamlFile(projectFolderPath, '''
name: x
''');
newFile(mainFilePath, '''
import 'package:path/path.dart' as path;
''');
await initialize();
var params = DartGetWorkspaceFixesParams(
diagnosticCodes: [diag.migrateDesignWidgets.lowerCaseName],
);
var result = await getWorkspaceFixes(params);
verifyResult(
result,
'''
>>>>>>>>>> pubspec.yaml
>>>>>>>>>> Update pubspec with the missing dependencies: line 2
name: x
dependencies:
path: any
''',
'''
pubspec.yaml:
missing_dependency: 1
''',
);
}
Future<void> test_pubspec_included_ifCode_missingDependency() async {
failTestOnErrorDiagnostic = false;
newPubspecYamlFile(projectFolderPath, '''
name: x
''');
newFile(mainFilePath, '''
import 'package:path/path.dart' as path;
''');
await initialize();
var params = DartGetWorkspaceFixesParams(
diagnosticCodes: [diag.missingDependency.lowerCaseName],
);
var result = await getWorkspaceFixes(params);
verifyResult(
result,
'''
>>>>>>>>>> pubspec.yaml
>>>>>>>>>> Update pubspec with the missing dependencies: line 2
name: x
dependencies:
path: any
''',
'''
pubspec.yaml:
missing_dependency: 1
''',
);
}
Future<void> test_pubspec_included_ifNoCodes() async {
failTestOnErrorDiagnostic = false;
newPubspecYamlFile(projectFolderPath, '''
name: x
''');
newFile(mainFilePath, '''
import 'package:args/args.dart' as args;
import 'package:path/path.dart' as path;
''');
await initialize();
var result = await getWorkspaceFixes();
verifyResult(
result,
'''
>>>>>>>>>> pubspec.yaml
>>>>>>>>>> Update pubspec with the missing dependencies: line 2
name: x
dependencies:
args: any
path: any
''',
'''
pubspec.yaml:
missing_dependency: 1
''',
);
}
void verifyResult(
DartGetWorkspaceFixesResult result,
String expectedEdits,
String expectedDetails,
) {
verifyEdit(result.edit!, expectedEdits);
var detailsString = StringBuffer();
for (var detail in result.details) {
detailsString.writeln('${relativePath(fromUri(detail.uri))}:');
for (var fix in detail.fixes) {
detailsString.writeln(' ${fix.code}: ${fix.occurrences}');
}
}
expect(detailsString.toString(), expectedDetails);
}
}