blob: 9ef2b2e4b50a91747a1594b5cb581cf693d74b6f [file] [log] [blame]
// Copyright (c) 2024, 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/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/element2.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/dart/element/element.dart'; // ignore: implementation_imports
import 'package:analyzer/src/dart/element/type_visitor.dart'; // ignore: implementation_imports
import '../analyzer.dart';
const _desc = r'Use new element model in opted-in files.';
bool _isOldModelElement(Element2? element) {
if (element == null) {
return false;
}
// Skip synthetic formal parameters.
if (element is FormalParameterElement && element.enclosingElement2 == null) {
return false;
}
var firstFragment = element.firstFragment;
if (firstFragment == null) {
return false;
}
var libraryFragment = firstFragment.libraryFragment;
var uriStr = libraryFragment.source.uri.toString();
if (element is InstanceElement2) {
if (uriStr == 'package:analyzer/dart/element/element.dart') {
// Skip classes that don't required migration.
if (const {
'DirectiveUri',
'DirectiveUriWithRelativeUri',
'DirectiveUriWithRelativeUriString',
'DirectiveUriWithSource',
'ElementAnnotation',
'ElementKind',
'ElementLocation',
'LibraryLanguageVersion',
}.contains(firstFragment.name2)) {
return false;
}
return true;
}
}
if (element is GetterElement) {
if (uriStr == 'package:analyzer/src/dart/ast/ast.dart') {
return element.name3 == 'declaredElement';
}
}
return false;
}
bool _isOldModelType(DartType? type) {
var visitor = _TypeVisitor();
type?.accept(visitor);
return visitor.result;
}
/// The lint must be enabled for a Pub package.
///
/// The lint rule reads once the file `analyzer_use_new_elements.txt`
/// from the root of the package (next to `pubspec.yaml`). Then it uses
/// the content of the file until DAS is restarted.
///
/// The file should have path prefixes from the root of the package.
/// For example `lib/src/services/correction/dart/add_async.dart` as a line.
/// It could be also the whole directory: `lib/src/services/correction/dart/`.
/// These are just path prefixes, not regular expressions.
///
/// When you start migrating a Dart file, add it to the opt-in file, restart
/// DAS, and open the file in your IDE.
class AnalyzerUseNewElements extends LintRule {
static const LintCode code = LintCode(
'analyzer_use_new_elements',
'This code uses the old analyzer element model.',
correctionMessage: 'Try using the new elements.',
);
/// Whether to use or bypass the opt-in file.
bool useOptInFile;
AnalyzerUseNewElements({
this.useOptInFile = true,
}) : super(
name: code.name,
description: _desc,
state: State.internal(),
);
@override
LintCode get lintCode => code;
@override
void registerNodeProcessors(
NodeLintRegistry registry, LinterContext context) {
if (!_isEnabledForFile(context)) {
return;
}
var visitor = _Visitor(this);
registry.addMethodInvocation(this, visitor);
registry.addNamedType(this, visitor);
registry.addSimpleIdentifier(this, visitor);
}
bool _isEnabledForFile(LinterContext context) {
if (!useOptInFile) {
return true;
}
if (context.package case PubPackage pubPackage) {
if (_FilesRegistry.get(pubPackage) case var filesRegistry?) {
var file = context.definingUnit.file;
return filesRegistry.isEnabled(file);
}
}
return false;
}
static void resetCaches() {
_FilesRegistry._registry.clear();
}
}
class _FilesRegistry {
static final Map<Folder, _FilesRegistry?> _registry = {};
final Folder rootFolder;
final List<String> prefixes;
final Map<File, bool> _fileResults = {};
_FilesRegistry({
required this.rootFolder,
required this.prefixes,
});
bool isEnabled(File file) => _fileResults[file] ??= _computeEnabled(file);
bool _computeEnabled(File file) {
var rootPath = rootFolder.path;
if (!file.path.startsWith(rootPath)) {
return false;
}
var relativePath = file.path.substring(rootPath.length + 1);
return _fileResults[file] ??=
prefixes.any((prefix) => relativePath.startsWith(prefix));
}
/// Note, we cache statically, to reload restart the server.
static _FilesRegistry? get(PubPackage pubPackage) {
var rootFolder = pubPackage.pubspecFile.parent;
var separator = rootFolder.provider.pathContext.separator;
if (_registry.containsKey(rootFolder)) {
return _registry[rootFolder];
}
try {
var lines = rootFolder
.getChildAssumingFile('analyzer_use_new_elements.txt')
.readAsStringSync()
.trim()
.split('\n')
// The file always uses forward slashes, so convert to the correct
// slash for this platform.
.map((line) => line.trim().replaceAll('/', separator))
.toList();
var result = _FilesRegistry(
rootFolder: rootFolder,
prefixes: lines,
);
return _registry[rootFolder] = result;
} on FileSystemException {
return null;
}
}
}
class _TypeVisitor extends RecursiveTypeVisitor {
bool result = false;
@override
bool visitInterfaceType(InterfaceType type) {
result |= _isOldModelElement(type.element3);
return super.visitInterfaceType(type);
}
}
class _Visitor extends SimpleAstVisitor<void> {
final LintRule rule;
_Visitor(this.rule);
@override
visitMethodInvocation(MethodInvocation node) {
if (_isOldModelType(node.staticType)) {
rule.reportLint(node.methodName);
}
}
@override
visitNamedType(NamedType node) {
if (_isOldModelElement(node.element2)) {
rule.reportLintForToken(node.name2);
}
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
if (node.parent case MethodInvocation invocation) {
if (invocation.methodName == node) {
return;
}
}
if (_isOldModelElement(node.element)) {
rule.reportLint(node);
}
if (_isOldModelType(node.staticType)) {
rule.reportLint(node);
}
}
}
extension on Element2 {
Fragment? get firstFragment {
switch (this) {
case FragmentedElementMixin fragmented:
return fragmented.firstFragment;
case AugmentedInstanceElement fragmented:
return fragmented.firstFragment as Fragment;
}
return null;
}
}