analyze_base.dart 11.7 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.experiments,
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 52
  @protected
  final List<String> experiments;
53 54
  @protected
  final Artifacts artifacts;
55 56

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

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

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

86 87
  bool get isFlutterRepo => argResults['flutter-repo'] as bool;
  String get sdkPath => argResults['dart-sdk'] as String ?? artifacts.getArtifactPath(Artifact.engineDartSdkPath);
88
  bool get isBenchmarking => argResults['benchmark'] as bool;
89 90 91 92 93 94 95
  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;
  }
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113

  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;
  }
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 142 143 144

  /// 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 {
145
      errorsMessage.write(' (ran in ${seconds}s)');
146 147 148
    }
    return errorsMessage.toString();
  }
149 150
}

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

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

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

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

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

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