diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart index 7323a5bd76..2876b8cca2 100644 --- a/dev/bots/analyze.dart +++ b/dev/bots/analyze.dart @@ -8,6 +8,11 @@ import 'dart:core' hide print; import 'dart:io' hide exit; import 'dart:typed_data'; +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; import 'package:crypto/crypto.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; @@ -283,25 +288,66 @@ Future _verifyNoMissingLicenseForExtension(String workingDirectory, String } } -final RegExp _skipTestCommentPattern = RegExp(r'\bskip:.*?//(.*)'); +class _TestSkip { + _TestSkip(this.line, this.content); + + final int line; + final String content; +} + +Iterable<_TestSkip> _getTestSkips(File file) { + final ParseStringResult parseResult = parseFile( + featureSet: FeatureSet.latestLanguageVersion(), + path: file.absolute.path, + ); + final _TestSkipLinesVisitor visitor = _TestSkipLinesVisitor(parseResult); + visitor.visitCompilationUnit(parseResult.unit); + return visitor.skips; +} + +class _TestSkipLinesVisitor extends RecursiveAstVisitor { + _TestSkipLinesVisitor(this.parseResult) : skips = <_TestSkip>{}; + + final ParseStringResult parseResult; + final Set<_TestSkip> skips; + + static bool isTestMethod(String name) { + return name.startsWith('test') || name == 'group' || name == 'expect'; + } + + @override + T? visitMethodInvocation(MethodInvocation node) { + if (isTestMethod(node.methodName.toString())) { + for (final Expression argument in node.argumentList.arguments) { + if (argument is NamedExpression && argument.name.label.name == 'skip') { + final int lineNumber = parseResult.lineInfo.getLocation(argument.beginToken.charOffset).lineNumber; + final String content = parseResult.content.substring(parseResult.lineInfo.getOffsetOfLine(lineNumber - 1), + parseResult.lineInfo.getOffsetOfLine(lineNumber) - 1); + skips.add(_TestSkip(lineNumber, content)); + } + } + } + return super.visitMethodInvocation(node); + } +} + +final RegExp _skipTestCommentPattern = RegExp(r'//(.*)$'); const Pattern _skipTestIntentionalPattern = '[intended]'; final Pattern _skipTestTrackingBugPattern = RegExp(r'https+?://github.com/.*/issues/[0-9]+'); Future verifySkipTestComments(String workingDirectory) async { final List errors = []; - final Stream testFiles = _allFiles(workingDirectory, 'dart', minimumMatches: 1500) + final Stream testFiles =_allFiles(workingDirectory, 'dart', minimumMatches: 1500) .where((File f) => f.path.endsWith('_test.dart')); await for (final File file in testFiles) { - final List lines = file.readAsLinesSync(); - for (int index = 0; index < lines.length; index++) { - final Match? match = _skipTestCommentPattern.firstMatch(lines[index]); + for (final _TestSkip skip in _getTestSkips(file)) { + final Match? match = _skipTestCommentPattern.firstMatch(skip.content); final String? skipComment = match?.group(1); - if (skipComment != null - && !skipComment.contains(_skipTestIntentionalPattern) - && !skipComment.contains(_skipTestTrackingBugPattern)) { - final int sourceLine = index + 1; - errors.add('${file.path}:$sourceLine}: skip test without a justification comment.'); + if (skipComment == null || + !(skipComment.contains(_skipTestIntentionalPattern) || + skipComment.contains(_skipTestTrackingBugPattern))) { + errors.add('${file.path}:${skip.line}: skip test without a justification comment.'); } } }