Skip to content
This repository has been archived by the owner on Jul 16, 2023. It is now read-only.

Commit

Permalink
fix: support excludes and conditional imports for check-unused-code (#…
Browse files Browse the repository at this point in the history
…654)

* fix: support excludes and conditional imports for check-unused-code

* chore: update changelog

* fix: track usage even for excluded files
  • Loading branch information
incendial authored Jan 19, 2022
1 parent 12e34a6 commit c9d0eb2
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 146 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

* fix: support excludes and conditional imports for `check-unused-code` command.

## 4.10.0-dev.1

* feat: add check unused code command.
Expand Down
1 change: 0 additions & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ analyzer:
- test/resources/*
- test/resources/unused_files_analyzer/**
- test/resources/unused_l10n_analyzer/**
- test/resources/unused_code_analyzer/**
- test/**/examples/**
language:
strict-inference: true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import 'package:analyzer/dart/element/element.dart';

import 'prefix_element_usage.dart';

/// A container with information about used imports prefixes and used imported
/// elements.
class FileElementsUsage {
/// The map of referenced prefix elements and the elements that they prefix.
final Map<PrefixElement, List<Element>> prefixMap = {};
final Map<PrefixElement, PrefixElementUsage> prefixMap = {};

/// The set of referenced top-level elements.
final Set<Element> elements = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'package:analyzer/dart/element/element.dart';

/// A container with information about used imports prefixes.
class PrefixElementUsage {
/// The paths to imported files.
/// Used for conditional imports to track all conditional paths.
final Iterable<String> paths;

/// The set of referenced elements.
final Set<Element> elements;

const PrefixElementUsage(this.paths, this.elements);

void add(Element element) {
elements.add(element);
}
}
95 changes: 47 additions & 48 deletions lib/src/analyzers/unused_code_analyzer/unused_code_analyzer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/element/element.dart';
// ignore: implementation_imports
import 'package:analyzer/src/dart/element/element.dart';
import 'package:glob/glob.dart';
import 'package:path/path.dart';
import 'package:source_span/source_span.dart';

Expand Down Expand Up @@ -57,28 +58,34 @@ class UnusedCodeAnalyzer {
final unusedCodeAnalysisConfig =
await _getAnalysisConfig(context, rootFolder, config);

final filePaths =
_getFilePaths(folders, context, rootFolder, unusedCodeAnalysisConfig);
final excludes = unusedCodeAnalysisConfig.globalExcludes
.followedBy(unusedCodeAnalysisConfig.analyzerExcludedPatterns);
final filePaths = _getFilePaths(folders, context, rootFolder, excludes);

final analyzedFiles =
filePaths.intersection(context.contextRoot.analyzedFiles().toSet());
await _analyseFiles(
analyzedFiles,
context.currentSession.getResolvedUnit,
codeUsages,
publicCode,
);
for (final filePath in analyzedFiles) {
final unit = await context.currentSession.getResolvedUnit(filePath);

final codeUsage = _analyzeFileCodeUsages(unit);
if (codeUsage != null) {
codeUsages.merge(codeUsage);
}

publicCode[filePath] = _analyzeFilePublicCode(unit);
}

final notAnalyzedFiles = filePaths.difference(analyzedFiles);
await _analyseFiles(
notAnalyzedFiles,
(filePath) => resolveFile2(path: filePath),
codeUsages,
publicCode,
shouldAnalyse: (filePath) => unusedCodeAnalysisConfig
.analyzerExcludedPatterns
.any((pattern) => pattern.matches(filePath)),
);
for (final filePath in notAnalyzedFiles) {
if (excludes.any((pattern) => pattern.matches(filePath))) {
final unit = await resolveFile2(path: filePath);

final codeUsage = _analyzeFileCodeUsages(unit);
if (codeUsage != null) {
codeUsages.merge(codeUsage);
}
}
}
}

codeUsages.exports.forEach(publicCode.remove);
Expand All @@ -105,18 +112,14 @@ class UnusedCodeAnalyzer {
Iterable<String> folders,
AnalysisContext context,
String rootFolder,
UnusedCodeAnalysisConfig unusedCodeAnalysisConfig,
Iterable<Glob> excludes,
) {
final contextFolders = folders
.where((path) => normalize(join(rootFolder, path))
.startsWith(context.contextRoot.root.path))
.toList();

return extractDartFilesFromFolders(
contextFolders,
rootFolder,
unusedCodeAnalysisConfig.globalExcludes,
);
return extractDartFilesFromFolders(contextFolders, rootFolder, excludes);
}

FileElementsUsage? _analyzeFileCodeUsages(SomeResolvedUnitResult unit) {
Expand All @@ -141,27 +144,6 @@ class UnusedCodeAnalyzer {
return {};
}

Future<void> _analyseFiles(
Set<String> files,
Future<SomeResolvedUnitResult> Function(String) unitExtractor,
FileElementsUsage codeUsages,
Map<String, Set<Element>> publicCode, {
bool Function(String)? shouldAnalyse,
}) async {
for (final filePath in files) {
if (shouldAnalyse == null || shouldAnalyse(filePath)) {
final unit = await unitExtractor(filePath);

final codeUsage = _analyzeFileCodeUsages(unit);
if (codeUsage != null) {
codeUsages.merge(codeUsage);
}

publicCode[filePath] = _analyzeFilePublicCode(unit);
}
}
}

Iterable<UnusedCodeFileReport> _getReports(
FileElementsUsage codeUsages,
Map<String, Set<Element>> publicCodeElements,
Expand All @@ -173,10 +155,7 @@ class UnusedCodeAnalyzer {
final issues = <UnusedCodeIssue>[];

for (final element in elements) {
if (!codeUsages.elements
.any((usedElement) => _isUsed(usedElement, element)) &&
!codeUsages.usedExtensions
.any((usedElement) => _isUsed(usedElement, element))) {
if (_isUnused(codeUsages, path, element)) {
final unit = element.thisOrAncestorOfType<CompilationUnitElement>();
if (unit != null) {
issues.add(_createUnusedCodeIssue(element as ElementImpl, unit));
Expand All @@ -202,6 +181,26 @@ class UnusedCodeAnalyzer {
element == usedElement ||
element is PropertyInducingElement && element.getter == usedElement;

bool _isUnused(
FileElementsUsage codeUsages,
String path,
Element element,
) =>
!codeUsages.elements.any(
(usedElement) => _isUsed(usedElement, element),
) &&
!codeUsages.usedExtensions.any(
(usedElement) => _isUsed(usedElement, element),
) &&
!codeUsages.prefixMap.values.any(
(usage) =>
usage.paths.contains(path) &&
usage.elements.any((usedElement) =>
_isUsed(usedElement, element) ||
(usedElement.name == element.name &&
usedElement.kind == element.kind)),
);

UnusedCodeIssue _createUnusedCodeIssue(
ElementImpl element,
CompilationUnitElement unit,
Expand Down
34 changes: 31 additions & 3 deletions lib/src/analyzers/unused_code_analyzer/used_code_visitor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';

import 'models/file_elements_usage.dart';
import 'models/prefix_element_usage.dart';

// Copied from https://github.com/dart-lang/sdk/blob/main/pkg/analyzer/lib/src/error/imports_verifier.dart#L15

Expand Down Expand Up @@ -100,8 +102,10 @@ class UsedCodeVisitor extends RecursiveAstVisitor<void> {
if (target is SimpleIdentifier) {
final targetElement = target.staticElement;
if (targetElement is PrefixElement) {
(fileElementsUsage.prefixMap
.putIfAbsent(targetElement, () => <Element>[])).add(element);
(fileElementsUsage.prefixMap.putIfAbsent(
targetElement,
() => PrefixElementUsage(_getPrefixUsagePaths(target), {}),
)).add(element);

return true;
}
Expand Down Expand Up @@ -162,7 +166,10 @@ class UsedCodeVisitor extends RecursiveAstVisitor<void> {

return;
} else if (element is PrefixElement) {
fileElementsUsage.prefixMap.putIfAbsent(element, () => <Element>[]);
fileElementsUsage.prefixMap.putIfAbsent(
element,
() => PrefixElementUsage(_getPrefixUsagePaths(identifier), {}),
);
} else if (element is MultiplyDefinedElement) {
// If the element is multiply defined then call this method recursively
// for each of the conflicting elements.
Expand All @@ -174,4 +181,25 @@ class UsedCodeVisitor extends RecursiveAstVisitor<void> {
}
}
}

Iterable<String> _getPrefixUsagePaths(SimpleIdentifier target) {
final root = target.root;

if (root is! CompilationUnit) {
return [];
}

return root.directives.fold<List<String>>([], (previousValue, directive) {
if (directive is ImportDirective &&
directive.prefix?.name == target.name) {
previousValue.add(directive.uriSource.toString());

for (final config in directive.configurations) {
previousValue.add(config.uriSource.toString());
}
}

return previousValue;
});
}
}
6 changes: 6 additions & 0 deletions test/resources/unused_code_analyzer/conditional_file.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// ignore: prefer_expression_function_bodies
String calculateResults() {
return 'conditional results';
}

bool hello() => false; // LINT
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
void freeze() {
print('brr');
}
1 change: 1 addition & 0 deletions test/resources/unused_code_analyzer/not_used.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ class NotUsed {} // LINT

const int value = 15; // LINT

// ignore: no-empty-block
void someFunction() {} // LINT
6 changes: 4 additions & 2 deletions test/resources/unused_code_analyzer/public_members.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// ignore_for_file: avoid_positional_boolean_parameters, library_private_types_in_public_api, override_on_non_overriding_member, unused_local_variable, no-empty-block

// LINT
void printInteger(int aNumber) {
print('The number is $aNumber.'); // Print to console.
Expand Down Expand Up @@ -37,7 +39,7 @@ class _MyWidgetState extends BaseState<MyWidget> {

void myMethod() {
setState(() {
myString = "Hello";
myString = 'Hello';
});
}

Expand Down Expand Up @@ -152,7 +154,7 @@ enum SomeOtherEnum {
world,
}

class Mixin {}
mixin Mixin {}

typedef Hello = String;

Expand Down
4 changes: 4 additions & 0 deletions test/resources/unused_code_analyzer/unconditional_file.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// ignore: prefer_expression_function_bodies
String calculateResults() {
return 'unconditional results';
}
7 changes: 7 additions & 0 deletions test/resources/unused_code_analyzer/unused_code_example.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
// ignore_for_file: unnecessary_statements, cascade_invocations, prefer_const_declarations, omit_local_variable_types

import 'public_members.dart';
import 'unconditional_file.dart'
if (dart.library.html) 'conditional_file.dart'
if (dart.library.io) 'conditional_file.dart' as config;

void main() {
final widget = MyWidget('hello');
Expand All @@ -17,4 +22,6 @@ void main() {
str.doNothing();

SomeEnum.hello;

config.calculateResults();
}
Loading

0 comments on commit c9d0eb2

Please sign in to comment.