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

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

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

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

34 35
  /// The parsed argument results for execution.
  final ArgResults argResults;
36 37 38 39 40 41 42 43 44 45 46 47 48
  @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
49
  final Terminal terminal;
50
  @protected
51
  final Artifacts artifacts;
52 53

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

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

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

83 84
  bool get isFlutterRepo => argResults['flutter-repo'] as bool;
  String get sdkPath => argResults['dart-sdk'] as String ?? artifacts.getArtifactPath(Artifact.engineDartSdkPath);
85
  bool get isBenchmarking => argResults['benchmark'] as bool;
86 87 88 89 90 91 92
  bool get isDartDocs => argResults['dartdocs'] as bool;

  static int countMissingDartDocs(List<AnalysisError> errors) {
    return errors.where((AnalysisError error) {
      return error.code == 'public_member_api_docs';
    }).length;
  }
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110

  static String generateDartDocMessage(int undocumentedMembers) {
    String dartDocMessage;

    assert(undocumentedMembers >= 0);
    switch (undocumentedMembers) {
      case 0:
        dartDocMessage = 'all public member have documentation';
        break;
      case 1:
        dartDocMessage = 'one public member lacks documentation';
        break;
      default:
        dartDocMessage = '$undocumentedMembers public members lack documentation';
    }

    return dartDocMessage;
  }
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141

  /// Generate an analysis summary for both [AnalyzeOnce], [AnalyzeContinuously].
  static String generateErrorsMessage({
    @required int issueCount,
    int issueDiff,
    int files,
    @required String seconds,
    int undocumentedMembers = 0,
    String dartDocMessage = '',
  }) {
    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)}');
    }

    if (undocumentedMembers > 0) {
      errorsMessage.write(' (ran in ${seconds}s; $dartDocMessage)');
    } else {
142
      errorsMessage.write(' (ran in ${seconds}s)');
143 144 145
    }
    return errorsMessage.toString();
  }
146 147
}

148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
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 {
163
    assert(globals.fs.path.isAbsolute(Cache.flutterRoot));
164 165
    for (final List<String> targetSources in values.values) {
      for (final String source in targetSources) {
166 167
        assert(globals.fs.path.isAbsolute(source));
        if (globals.fs.path.isWithin(Cache.flutterRoot, source)) {
168
          return true;
169
        }
170 171 172 173 174 175 176 177
      }
    }
    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));
178
    for (final String target in targets) {
179 180 181
      final int count = values[target].length;
      result.writeln('  $count ${count == 1 ? 'source wants' : 'sources want'} "$target":');
      bool canonical = false;
182
      for (final String source in values[target]) {
183
        result.writeln('    $source');
184
        if (source == canonicalSource) {
185
          canonical = true;
186
        }
187 188 189 190 191 192 193 194 195 196 197
      }
      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.
198
  static const List<String> _vendedSdkPackages = <String>['analyzer', 'front_end', 'kernel'];
199 200 201 202 203 204

  // 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) {
205
    return packages.putIfAbsent(packageName, () => PackageDependency());
206 207 208 209
  }

  /// Read the .packages file in [directory] and add referenced packages to [dependencies].
  void addDependenciesFromPackagesFileIn(Directory directory) {
210 211
    final String dotPackagesPath = globals.fs.path.join(directory.path, '.packages');
    final File dotPackages = globals.fs.file(dotPackagesPath);
212 213
    if (dotPackages.existsSync()) {
      // this directory has opinions about what we should be using
214
      final Iterable<String> lines = dotPackages
215 216
        .readAsStringSync()
        .split('\n')
217
        .where((String line) => !line.startsWith(RegExp(r'^ *#')));
218
      for (final String line in lines) {
219 220 221
        final int colon = line.indexOf(':');
        if (colon > 0) {
          final String packageName = line.substring(0, colon);
222
          final String packagePath = globals.fs.path.fromUri(line.substring(colon+1));
223
          // Ensure that we only add `analyzer` and dependent packages defined in the vended SDK (and referred to with a local
224
          // globals.fs.path. directive). Analyzer package versions reached via transitive dependencies (e.g., via `test`) are ignored
225
          // since they would produce spurious conflicts.
226
          if (!_vendedSdkPackages.contains(packageName) || packagePath.startsWith('..')) {
227
            add(packageName, globals.fs.path.normalize(globals.fs.path.absolute(directory.path, packagePath)), dotPackagesPath);
228
          }
229 230
        }
      }
231 232 233 234 235 236 237 238 239 240 241 242
    }
  }

  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) {
243
    for (final Directory directory in pubSpecDirectories) {
244 245
      final String pubSpecYamlPath = globals.fs.path.join(directory.path, 'pubspec.yaml');
      final File pubSpecYamlFile = globals.fs.file(pubSpecYamlPath);
246 247 248 249
      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.
250
        final dynamic pubSpecYaml = yaml.loadYaml(globals.fs.file(pubSpecYamlPath).readAsStringSync());
251 252 253
        if (pubSpecYaml is yaml.YamlMap) {
          final dynamic packageName = pubSpecYaml['name'];
          if (packageName is String) {
254
            final String packagePath = globals.fs.path.normalize(globals.fs.path.absolute(globals.fs.path.join(directory.path, 'lib')));
255 256 257 258 259 260 261
            dependencies.addCanonicalCase(packageName, packagePath, pubSpecYamlPath);
          } else {
            throwToolExit('pubspec.yaml is malformed. The name should be a String.');
          }
        } else {
          throwToolExit('pubspec.yaml is malformed.');
        }
262 263 264 265 266 267
      }
      dependencies.addDependenciesFromPackagesFileIn(directory);
    }

    // prepare a union of all the .packages files
    if (dependencies.hasConflicts) {
268
      final StringBuffer message = StringBuffer();
269 270 271 272
      message.writeln(dependencies.generateConflictReport());
      message.writeln('Make sure you have run "pub upgrade" in all the directories mentioned above.');
      if (dependencies.hasConflictsAffectingFlutterRepo) {
        message.writeln(
273 274 275 276
          '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.)'
        );
277 278
      }
      message.write(
279 280 281
        '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.'
      );
282 283 284 285 286 287 288 289 290 291 292 293 294 295
      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);
296
    final StringBuffer result = StringBuffer();
297
    for (final String package in packages.keys.where((String package) => packages[package].hasConflict)) {
298 299 300 301 302 303 304 305
      result.writeln('Package "$package" has conflicts:');
      packages[package].describeConflict(result);
    }
    return result.toString();
  }

  Map<String, String> asPackageMap() {
    final Map<String, String> result = <String, String>{};
306
    for (final String package in packages.keys) {
307
      result[package] = packages[package].target;
308
    }
309 310 311
    return result;
  }
}