blob: f2132b266693496fe660ef0aa8445c615007516c [file] [log] [blame]
// 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*');
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, '');
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',
],
'--',
];
args.addAll(tidyArgs.split(' '));
return WorkerJob(
<String>[tidyPath, ...args],
workingDirectory: directory,
name: 'clang-tidy on $filePath',
printOutput: options.verbose,
);
}
}