| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:convert' show LineSplitter, utf8; |
| import 'dart:io' as io; |
| |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:process_runner/process_runner.dart'; |
| |
| import 'options.dart'; |
| |
| /// The url prefix for issues that must be attached to the directive in files |
| /// that disables linting. |
| const String issueUrlPrefix = 'https://github.com/flutter/flutter/issues'; |
| |
| /// Lint actions to apply to a file. |
| enum LintAction { |
| /// Run the linter over the file. |
| lint, |
| |
| /// Ignore files under third_party/. |
| skipThirdParty, |
| |
| /// Ignore due to a well-formed FLUTTER_NOLINT comment. |
| skipNoLint, |
| |
| /// Fail due to a malformed FLUTTER_NOLINT comment. |
| failMalformedNoLint, |
| |
| /// Ignore because the file doesn't exist locally. |
| skipMissing, |
| } |
| |
| /// A compilation command and methods to generate the lint command and job for |
| /// it. |
| class Command { |
| /// Generate a [Command] from a [Map]. |
| Command.fromMap(Map<String, dynamic> map) : |
| directory = io.Directory(map['directory'] as String).absolute, |
| command = map['command'] as String { |
| filePath = path.normalize(path.join( |
| directory.path, |
| map['file'] as String, |
| )); |
| } |
| |
| /// The working directory of the command. |
| final io.Directory directory; |
| |
| /// The compilation command. |
| final String command ; |
| |
| /// The file on which the command operates. |
| late final String filePath; |
| |
| static final RegExp _pathRegex = RegExp(r'\S*clang/bin/clang'); |
| static final RegExp _argRegex = RegExp(r'-MF \S*'); |
| |
| // Filter out any extra commands that were appended to the compile command. |
| static final RegExp _extraCommandRegex = RegExp(r'&&.*$'); |
| |
| String? _tidyArgs; |
| |
| /// The command line arguments of the command. |
| String get tidyArgs { |
| return _tidyArgs ??= (() { |
| String result = command; |
| result = result.replaceAll(_pathRegex, ''); |
| result = result.replaceAll(_argRegex, ''); |
| result = result.replaceAll(_extraCommandRegex, ''); |
| return result; |
| })(); |
| } |
| |
| String? _tidyPath; |
| |
| /// The command but with clang-tidy instead of clang. |
| String get tidyPath { |
| return _tidyPath ??= _pathRegex.stringMatch(command)?.replaceAll( |
| 'clang/bin/clang', |
| 'clang/bin/clang-tidy', |
| ) ?? ''; |
| } |
| |
| /// Whether this command operates on any of the files in `queries`. |
| bool containsAny(List<io.File> queries) { |
| return queries.indexWhere( |
| (io.File query) => path.equals(query.path, filePath), |
| ) != -1; |
| } |
| |
| static final RegExp _nolintRegex = RegExp( |
| r'//\s*FLUTTER_NOLINT(: https://github.com/flutter/flutter/issues/\d+)?', |
| ); |
| |
| /// The type of lint that is appropriate for this command. |
| late final Future<LintAction> lintAction = getLintAction(filePath); |
| |
| /// Determine the lint action for the file at `path`. |
| @visibleForTesting |
| static Future<LintAction> getLintAction(String filePath) async { |
| if (path.split(filePath).contains('third_party')) { |
| return LintAction.skipThirdParty; |
| } |
| |
| final io.File file = io.File(filePath); |
| if (!file.existsSync()) { |
| return LintAction.skipMissing; |
| } |
| final Stream<String> lines = file.openRead() |
| .transform(utf8.decoder) |
| .transform(const LineSplitter()); |
| return lintActionFromContents(lines); |
| } |
| |
| /// Determine the lint action for the file with contents `lines`. |
| @visibleForTesting |
| static Future<LintAction> lintActionFromContents(Stream<String> lines) async { |
| // Check for FlUTTER_NOLINT at top of file. |
| await for (final String line in lines) { |
| final RegExpMatch? match = _nolintRegex.firstMatch(line); |
| if (match != null) { |
| return match.group(1) != null |
| ? LintAction.skipNoLint |
| : LintAction.failMalformedNoLint; |
| } else if (line.isNotEmpty && line[0] != '\n' && line[0] != '/') { |
| // Quick out once we find a line that isn't empty or a comment. The |
| // FLUTTER_NOLINT must show up before the first real code. |
| return LintAction.lint; |
| } |
| } |
| return LintAction.lint; |
| } |
| |
| /// The job for the process runner for the lint needed for this command. |
| WorkerJob createLintJob(Options options) { |
| final List<String> args = <String>[ |
| filePath, |
| '--warnings-as-errors=${options.warningsAsErrors ?? '*'}', |
| if (options.checks != null) |
| options.checks!, |
| if (options.fix) ...<String>[ |
| '--fix', |
| '--format-style=file', |
| ], |
| if (options.enableCheckProfile) |
| '--enable-check-profile', |
| '--', |
| ]; |
| args.addAll(tidyArgs.split(' ')); |
| final String clangTidyPath = options.clangTidyPath?.path ?? tidyPath; |
| return WorkerJob( |
| <String>[clangTidyPath, ...args], |
| workingDirectory: directory, |
| name: 'clang-tidy on $filePath', |
| printOutput: options.verbose, |
| ); |
| } |
| } |