scripts/ had ~80 flat files. Entry points (ci_*.sh, codegen.sh, run_tests.sh, format_native.sh, setup_hooks.sh, upload-symbols.*) and the shared pubspec_version.py stay at the root; checkers, generators, maestro tooling and release tooling move into subdirectories with their tests. Updated every reference: workflow steps, guard-test glob, Docker COPY paths and .dockerignore whitelist, website audit path, dart test imports, and regenerated the five outputs whose headers embed generator paths.
195 lines
5.8 KiB
Dart
195 lines
5.8 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
const _analysisTimeout = Duration(minutes: 3);
|
|
|
|
final _allowedDiagnostics = <AnalyzerDiagnostic>{
|
|
const AnalyzerDiagnostic(
|
|
severity: 'INFO',
|
|
type: 'LINT',
|
|
code: 'UNAWAITED_FUTURES',
|
|
path: 'test/navigation/profile_navigation_scope_test.dart',
|
|
line: 21,
|
|
column: 28,
|
|
length: 4,
|
|
message: "Missing an 'await' for the 'Future' computed by this expression.",
|
|
),
|
|
};
|
|
|
|
Future<void> main() async {
|
|
final scriptDirectory = File.fromUri(Platform.script).parent;
|
|
final root = scriptDirectory.parent.parent.path;
|
|
late final Process process;
|
|
|
|
try {
|
|
process = await Process.start(
|
|
Platform.resolvedExecutable,
|
|
const ['analyze', '--format', 'machine', '--fatal-infos'],
|
|
workingDirectory: root,
|
|
runInShell: false,
|
|
);
|
|
} on ProcessException catch (error) {
|
|
stderr.writeln('Failed to start the analyzer: $error');
|
|
exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
final analyzerStdoutFuture = process.stdout.transform(utf8.decoder).join();
|
|
final analyzerStderrFuture = process.stderr.transform(utf8.decoder).join();
|
|
|
|
int analyzerExitCode;
|
|
try {
|
|
analyzerExitCode = await process.exitCode.timeout(_analysisTimeout);
|
|
} on TimeoutException {
|
|
process.kill(ProcessSignal.sigkill);
|
|
await Future.wait([analyzerStdoutFuture, analyzerStderrFuture]);
|
|
stderr.writeln('Analyzer timed out after ${_analysisTimeout.inMinutes} minutes.');
|
|
exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
final analyzerStdout = await analyzerStdoutFuture;
|
|
final analyzerStderr = await analyzerStderrFuture;
|
|
stdout.write(analyzerStdout);
|
|
stderr.write(analyzerStderr);
|
|
|
|
final failure = validateAnalyzerResult(
|
|
analyzerExitCode: analyzerExitCode,
|
|
analyzerStdout: analyzerStdout,
|
|
analyzerStderr: analyzerStderr,
|
|
rootPath: root,
|
|
);
|
|
if (failure != null) {
|
|
stderr.writeln('Analyzer check failed: $failure');
|
|
exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
final diagnosticCount = analyzerStdout.trim().isEmpty ? 0 : const LineSplitter().convert(analyzerStdout).length;
|
|
stdout.writeln(
|
|
'Analyzer passed with $diagnosticCount explicitly allowed info '
|
|
'diagnostic(s).',
|
|
);
|
|
}
|
|
|
|
String? validateAnalyzerResult({
|
|
required int analyzerExitCode,
|
|
required String analyzerStdout,
|
|
required String analyzerStderr,
|
|
required String rootPath,
|
|
}) {
|
|
if (analyzerStderr.trim().isNotEmpty) {
|
|
return 'unexpected stderr output (exit $analyzerExitCode)';
|
|
}
|
|
|
|
final lines = const LineSplitter().convert(analyzerStdout);
|
|
final diagnostics = <AnalyzerDiagnostic>[];
|
|
for (final line in lines) {
|
|
if (line.isEmpty) {
|
|
return 'unexpected blank line in machine output';
|
|
}
|
|
final diagnostic = AnalyzerDiagnostic.tryParse(line, rootPath: rootPath);
|
|
if (diagnostic == null) {
|
|
return 'malformed machine output: $line';
|
|
}
|
|
diagnostics.add(diagnostic);
|
|
}
|
|
|
|
final seen = <AnalyzerDiagnostic>{};
|
|
for (final diagnostic in diagnostics) {
|
|
if (diagnostic.severity != 'INFO') {
|
|
return 'unexpected ${diagnostic.severity} diagnostic: ${diagnostic.code}';
|
|
}
|
|
if (!_allowedDiagnostics.contains(diagnostic)) {
|
|
return 'info diagnostic is not explicitly allowed: '
|
|
'${diagnostic.code} at ${diagnostic.path}:${diagnostic.line}';
|
|
}
|
|
if (!seen.add(diagnostic)) {
|
|
return 'duplicate diagnostic: '
|
|
'${diagnostic.code} at ${diagnostic.path}:${diagnostic.line}';
|
|
}
|
|
}
|
|
|
|
if (analyzerExitCode == 0) {
|
|
return diagnostics.isEmpty ? null : 'analyzer returned success despite --fatal-infos diagnostics';
|
|
}
|
|
if (analyzerExitCode == 1 && diagnostics.isNotEmpty) {
|
|
return null;
|
|
}
|
|
return 'analyzer exited $analyzerExitCode without only allowed info diagnostics';
|
|
}
|
|
|
|
class AnalyzerDiagnostic {
|
|
const AnalyzerDiagnostic({
|
|
required this.severity,
|
|
required this.type,
|
|
required this.code,
|
|
required this.path,
|
|
required this.line,
|
|
required this.column,
|
|
required this.length,
|
|
required this.message,
|
|
});
|
|
|
|
factory AnalyzerDiagnostic._fromParts(List<String> parts, String rootPath) {
|
|
return AnalyzerDiagnostic(
|
|
severity: parts[0],
|
|
type: parts[1],
|
|
code: parts[2],
|
|
path: _relativePath(parts[3], rootPath),
|
|
line: int.parse(parts[4]),
|
|
column: int.parse(parts[5]),
|
|
length: int.parse(parts[6]),
|
|
message: parts.sublist(7).join('|'),
|
|
);
|
|
}
|
|
|
|
static AnalyzerDiagnostic? tryParse(String line, {required String rootPath}) {
|
|
final parts = line.split('|');
|
|
if (parts.length < 8 || parts.take(8).any((part) => part.isEmpty)) {
|
|
return null;
|
|
}
|
|
if (int.tryParse(parts[4]) == null || int.tryParse(parts[5]) == null || int.tryParse(parts[6]) == null) {
|
|
return null;
|
|
}
|
|
return AnalyzerDiagnostic._fromParts(parts, rootPath);
|
|
}
|
|
|
|
static String _relativePath(String path, String rootPath) {
|
|
final normalizedPath = _normalizePath(path);
|
|
final normalizedRoot = _normalizePath(rootPath);
|
|
final rootPrefix = '$normalizedRoot/';
|
|
if (normalizedPath.toLowerCase().startsWith(rootPrefix.toLowerCase())) {
|
|
return normalizedPath.substring(rootPrefix.length);
|
|
}
|
|
return normalizedPath;
|
|
}
|
|
|
|
static String _normalizePath(String path) => path.replaceAll(r'\\', '/').replaceAll(r'\', '/');
|
|
|
|
final String severity;
|
|
final String type;
|
|
final String code;
|
|
final String path;
|
|
final int line;
|
|
final int column;
|
|
final int length;
|
|
final String message;
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
other is AnalyzerDiagnostic &&
|
|
severity == other.severity &&
|
|
type == other.type &&
|
|
code == other.code &&
|
|
path == other.path &&
|
|
line == other.line &&
|
|
column == other.column &&
|
|
length == other.length &&
|
|
message == other.message;
|
|
|
|
@override
|
|
int get hashCode => Object.hash(severity, type, code, path, line, column, length, message);
|
|
}
|