analyze_base.dart 10.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7
import 'package:args/args.dart';
8
import 'package:meta/meta.dart';
9
import 'package:process/process.dart';
10
import 'package:yaml/yaml.dart' as yaml;
11

12
import '../artifacts.dart';
13
import '../base/common.dart';
14
import '../base/file_system.dart';
15
import '../base/logger.dart';
16
import '../base/platform.dart';
17
import '../base/terminal.dart';
18 19
import '../base/utils.dart';
import '../cache.dart';
20
import '../globals_null_migrated.dart' as globals;
21 22 23

/// Common behavior for `flutter analyze` and `flutter analyze --watch`
abstract class AnalyzeBase {
24 25 26 27 28 29 30 31
  AnalyzeBase(this.argResults, {
    @required this.repoRoots,
    @required this.repoPackages,
    @required this.fileSystem,
    @required this.logger,
    @required this.platform,
    @required this.processManager,
    @required this.terminal,
32
    @required this.artifacts,
33
  });
34

35 36
  /// The parsed argument results for execution.
  final ArgResults argResults;
37 38 39 40 41 42 43 44 45 46 47 48 49
  @protected
  final List<String> repoRoots;
  @protected
  final List<Directory> repoPackages;
  @protected
  final FileSystem fileSystem;
  @protected
  final Logger logger;
  @protected
  final ProcessManager processManager;
  @protected
  final Platform platform;
  @protected
50
  final Terminal terminal;
51
  @protected
52
  final Artifacts artifacts;
53 54

  /// Called by [AnalyzeCommand] to start the analysis process.
55
  Future<void> analyze();
56 57 58 59

  void dumpErrors(Iterable<String> errors) {
    if (argResults['write'] != null) {
      try {
60
        final RandomAccessFile resultsFile = fileSystem.file(argResults['write']).openSync(mode: FileMode.write);
61 62 63 64 65 66
        try {
          resultsFile.lockSync();
          resultsFile.writeStringSync(errors.join('\n'));
        } finally {
          resultsFile.close();
        }
67
      } on Exception catch (e) {
68
        logger.printError('Failed to save output to "${argResults['write']}": $e');
69 70 71 72
      }
    }
  }

73
  void writeBenchmark(Stopwatch stopwatch, int errorCount) {
74
    const String benchmarkOut = 'analysis_benchmark.json';
75
    final Map<String, dynamic> data = <String, dynamic>{
76
      'time': stopwatch.elapsedMilliseconds / 1000.0,
77 78
      'issues': errorCount,
    };
79 80
    fileSystem.file(benchmarkOut).writeAsStringSync(toPrettyJson(data));
    logger.printStatus('Analysis benchmark written to $benchmarkOut ($data).');
81 82
  }

83
  bool get isFlutterRepo => argResults['flutter-repo'] as bool;
84
  String get sdkPath => argResults['dart-sdk'] as String ?? artifacts.getHostArtifact(HostArtifact.engineDartSdkPath).path;
85
  bool get isBenchmarking => argResults['benchmark'] as bool;
86
  String get protocolTrafficLog => argResults['protocol-traffic-log'] as String;
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111

  /// Generate an analysis summary for both [AnalyzeOnce], [AnalyzeContinuously].
  static String generateErrorsMessage({
    @required int issueCount,
    int issueDiff,
    int files,
    @required String seconds,
  }) {
    final StringBuffer errorsMessage = StringBuffer(issueCount > 0
      ? '$issueCount ${pluralize('issue', issueCount)} found.'
      : 'No issues found!');

    // Only [AnalyzeContinuously] has issueDiff message.
    if (issueDiff != null) {
      if (issueDiff > 0) {
        errorsMessage.write(' ($issueDiff new)');
      } else if (issueDiff < 0) {
        errorsMessage.write(' (${-issueDiff} fixed)');
      }
    }

    // Only [AnalyzeContinuously] has files message.
    if (files != null) {
      errorsMessage.write(' • analyzed $files ${pluralize('file', files)}');
    }
112
    errorsMessage.write(' (ran in ${seconds}s)');
113 114
    return errorsMessage.toString();
  }
115 116
}

117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
class PackageDependency {
  // This is a map from dependency targets (lib directories) to a list
  // of places that ask for that target (.packages or pubspec.yaml files)
  Map<String, List<String>> values = <String, List<String>>{};
  String canonicalSource;
  void addCanonicalCase(String packagePath, String pubSpecYamlPath) {
    assert(canonicalSource == null);
    add(packagePath, pubSpecYamlPath);
    canonicalSource = pubSpecYamlPath;
  }
  void add(String packagePath, String sourcePath) {
    values.putIfAbsent(packagePath, () => <String>[]).add(sourcePath);
  }
  bool get hasConflict => values.length > 1;
  bool get hasConflictAffectingFlutterRepo {
132
    assert(globals.fs.path.isAbsolute(Cache.flutterRoot));
133 134
    for (final List<String> targetSources in values.values) {
      for (final String source in targetSources) {
135 136
        assert(globals.fs.path.isAbsolute(source));
        if (globals.fs.path.isWithin(Cache.flutterRoot, source)) {
137
          return true;
138
        }
139 140 141 142 143 144 145 146
      }
    }
    return false;
  }
  void describeConflict(StringBuffer result) {
    assert(hasConflict);
    final List<String> targets = values.keys.toList();
    targets.sort((String a, String b) => values[b].length.compareTo(values[a].length));
147
    for (final String target in targets) {
148 149 150
      final int count = values[target].length;
      result.writeln('  $count ${count == 1 ? 'source wants' : 'sources want'} "$target":');
      bool canonical = false;
151
      for (final String source in values[target]) {
152
        result.writeln('    $source');
153
        if (source == canonicalSource) {
154
          canonical = true;
155
        }
156 157 158 159 160 161 162 163 164 165 166
      }
      if (canonical) {
        result.writeln('    (This is the actual package definition, so it is considered the canonical "right answer".)');
      }
    }
  }
  String get target => values.keys.single;
}

class PackageDependencyTracker {
  /// Packages whose source is defined in the vended SDK.
167
  static const List<String> _vendedSdkPackages = <String>['analyzer', 'front_end', 'kernel'];
168 169 170 171 172 173

  // This is a map from package names to objects that track the paths
  // involved (sources and targets).
  Map<String, PackageDependency> packages = <String, PackageDependency>{};

  PackageDependency getPackageDependency(String packageName) {
174
    return packages.putIfAbsent(packageName, () => PackageDependency());
175 176 177 178
  }

  /// Read the .packages file in [directory] and add referenced packages to [dependencies].
  void addDependenciesFromPackagesFileIn(Directory directory) {
179 180
    final String dotPackagesPath = globals.fs.path.join(directory.path, '.packages');
    final File dotPackages = globals.fs.file(dotPackagesPath);
181 182
    if (dotPackages.existsSync()) {
      // this directory has opinions about what we should be using
183
      final Iterable<String> lines = dotPackages
184 185
        .readAsStringSync()
        .split('\n')
186
        .where((String line) => !line.startsWith(RegExp(r'^ *#')));
187
      for (final String line in lines) {
188 189 190
        final int colon = line.indexOf(':');
        if (colon > 0) {
          final String packageName = line.substring(0, colon);
191
          final String packagePath = globals.fs.path.fromUri(line.substring(colon+1));
192
          // Ensure that we only add `analyzer` and dependent packages defined in the vended SDK (and referred to with a local
193
          // globals.fs.path. directive). Analyzer package versions reached via transitive dependencies (e.g., via `test`) are ignored
194
          // since they would produce spurious conflicts.
195
          if (!_vendedSdkPackages.contains(packageName) || packagePath.startsWith('..')) {
196
            add(packageName, globals.fs.path.normalize(globals.fs.path.absolute(directory.path, packagePath)), dotPackagesPath);
197
          }
198 199
        }
      }
200 201 202 203 204 205 206 207 208 209 210 211
    }
  }

  void addCanonicalCase(String packageName, String packagePath, String pubSpecYamlPath) {
    getPackageDependency(packageName).addCanonicalCase(packagePath, pubSpecYamlPath);
  }

  void add(String packageName, String packagePath, String dotPackagesPath) {
    getPackageDependency(packageName).add(packagePath, dotPackagesPath);
  }

  void checkForConflictingDependencies(Iterable<Directory> pubSpecDirectories, PackageDependencyTracker dependencies) {
212
    for (final Directory directory in pubSpecDirectories) {
213 214
      final String pubSpecYamlPath = globals.fs.path.join(directory.path, 'pubspec.yaml');
      final File pubSpecYamlFile = globals.fs.file(pubSpecYamlPath);
215 216 217 218
      if (pubSpecYamlFile.existsSync()) {
        // we are analyzing the actual canonical source for this package;
        // make sure we remember that, in case all the packages are actually
        // pointing elsewhere somehow.
219
        final dynamic pubSpecYaml = yaml.loadYaml(globals.fs.file(pubSpecYamlPath).readAsStringSync());
220 221 222
        if (pubSpecYaml is yaml.YamlMap) {
          final dynamic packageName = pubSpecYaml['name'];
          if (packageName is String) {
223
            final String packagePath = globals.fs.path.normalize(globals.fs.path.absolute(globals.fs.path.join(directory.path, 'lib')));
224 225 226 227 228 229 230
            dependencies.addCanonicalCase(packageName, packagePath, pubSpecYamlPath);
          } else {
            throwToolExit('pubspec.yaml is malformed. The name should be a String.');
          }
        } else {
          throwToolExit('pubspec.yaml is malformed.');
        }
231 232 233 234 235 236
      }
      dependencies.addDependenciesFromPackagesFileIn(directory);
    }

    // prepare a union of all the .packages files
    if (dependencies.hasConflicts) {
237
      final StringBuffer message = StringBuffer();
238 239 240 241
      message.writeln(dependencies.generateConflictReport());
      message.writeln('Make sure you have run "pub upgrade" in all the directories mentioned above.');
      if (dependencies.hasConflictsAffectingFlutterRepo) {
        message.writeln(
242 243 244 245
          'For packages in the flutter repository, try using "flutter update-packages" to do all of them at once.\n'
          'If you need to actually upgrade them, consider "flutter update-packages --force-upgrade". '
          '(This will update your pubspec.yaml files as well, so you may wish to do this on a separate branch.)'
        );
246 247
      }
      message.write(
248 249 250
        'If this does not help, to track down the conflict you can use '
        '"pub deps --style=list" and "pub upgrade --verbosity=solver" in the affected directories.'
      );
251 252 253 254 255 256 257 258 259 260 261 262 263 264
      throwToolExit(message.toString());
    }
  }

  bool get hasConflicts {
    return packages.values.any((PackageDependency dependency) => dependency.hasConflict);
  }

  bool get hasConflictsAffectingFlutterRepo {
    return packages.values.any((PackageDependency dependency) => dependency.hasConflictAffectingFlutterRepo);
  }

  String generateConflictReport() {
    assert(hasConflicts);
265
    final StringBuffer result = StringBuffer();
266
    for (final String package in packages.keys.where((String package) => packages[package].hasConflict)) {
267 268 269 270 271 272 273 274
      result.writeln('Package "$package" has conflicts:');
      packages[package].describeConflict(result);
    }
    return result.toString();
  }

  Map<String, String> asPackageMap() {
    final Map<String, String> result = <String, String>{};
275
    for (final String package in packages.keys) {
276
      result[package] = packages[package].target;
277
    }
278 279 280
    return result;
  }
}