93f4ac0a83
The CFEs FormattedMessage always had two getters to get the text inside one that would supposedly give an ansi formated version of the message and one that would supposedly give a plaintext formated version of the message. They both returned the same string, though, which would either be with ansi escape codes or plain text depending on the environment at compile time. This CL fixes that by having both messages, and letting the reporting (i.e. whenever the message is read) decide which to use. That way we can - for instance - report errors with color if the terminal supports it correctly when reusing a dill (and reissuing problems, but where the terminal support changes) and if printing the problem to an html <pre> field (like observatory does). It also cleans up two different implementations of whether we think the terminal supports colors or not, by deleting one of them. This is the second try. Patchset #1 is the original. Patchset #2(and possibly beyond) is the changes. TEST=Existing test suites. Change-Id: I8e483049ce81ce1bd8e5396b588a31e0ad3a8630 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/187402 Commit-Queue: Jens Johansen <jensj@google.com> Reviewed-by: Johnni Winther <johnniwinther@google.com> Reviewed-by: Alexander Markov <alexmarkov@google.com>
316 lines
11 KiB
Dart
316 lines
11 KiB
Dart
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE.md file.
|
|
|
|
// @dart = 2.9
|
|
|
|
import 'dart:convert' show utf8;
|
|
|
|
import 'dart:io' show File;
|
|
|
|
import 'package:_fe_analyzer_shared/src/messages/severity.dart' show Severity;
|
|
|
|
import 'package:_fe_analyzer_shared/src/scanner/io.dart' show readBytesFromFile;
|
|
|
|
import 'package:_fe_analyzer_shared/src/scanner/scanner.dart'
|
|
show ScannerResult, Token, scan;
|
|
|
|
import 'package:_fe_analyzer_shared/src/scanner/token.dart' as analyzer
|
|
show Token;
|
|
|
|
import 'package:front_end/src/base/instrumentation.dart'
|
|
show Instrumentation, InstrumentationValue;
|
|
|
|
import 'package:front_end/src/fasta/compiler_context.dart' show CompilerContext;
|
|
|
|
import 'package:front_end/src/fasta/messages.dart'
|
|
show noLength, templateUnspecified;
|
|
|
|
/// Implementation of [Instrumentation] which checks property/value pairs
|
|
/// against expectations encoded in source files using "/*@...*/" comments.
|
|
class ValidatingInstrumentation implements Instrumentation {
|
|
static final RegExp _ESCAPE_SEQUENCE = new RegExp(r'\\(.)');
|
|
|
|
/// Map from feature names to the property names they are short for.
|
|
static const Map<String, List<String>> _FEATURES = const {
|
|
'inference': const [
|
|
'typeArg',
|
|
'typeArgs',
|
|
'promotedType',
|
|
'type',
|
|
'returnType',
|
|
'target',
|
|
],
|
|
'checks': const [
|
|
'checkGetterReturn',
|
|
'checkReturn',
|
|
'genericContravariant',
|
|
],
|
|
};
|
|
|
|
/// Map from file URI to the as-yet unsatisfied expectations from that file,
|
|
/// organized by file offset.
|
|
final _unsatisfiedExpectations = <Uri, Map<int, List<_Expectation>>>{};
|
|
|
|
/// Information about "testedFeatures" annotations, organized by file URI and
|
|
/// file offset. The inner map is guaranteed to be in ascending order of
|
|
/// file offset.
|
|
final _testedFeaturesState = <Uri, Map<int, Set<String>>>{};
|
|
|
|
/// Map from file URI to a sorted list of token offsets for that file.
|
|
final _tokenOffsets = <Uri, List<int>>{};
|
|
|
|
/// String descriptions of the expectation mismatches found so far.
|
|
final _problems = <String>[];
|
|
|
|
/// Fixes that would need to be performed on source files in order for all
|
|
/// expectations to be met, organized by file URI. The inner map is not
|
|
/// guaranteed to be in ascending order of file offset.
|
|
final _fixes = <Uri, List<_Fix>>{};
|
|
|
|
/// Indicates whether any expectation mismatches were found.
|
|
///
|
|
/// Should be called after [finish].
|
|
bool get hasProblems => _problems.isNotEmpty;
|
|
|
|
/// Gets a description of all expectation mismatches that were found, in a
|
|
/// form suitable for printing to the console.
|
|
///
|
|
/// Should be called after [finish].
|
|
String get problemsAsString => _problems.join('\n');
|
|
|
|
/// Checks whether the property/value pairs passed to [record] match the
|
|
/// expectations loaded by [loadExpectations].
|
|
void finish() {
|
|
_unsatisfiedExpectations.forEach((uri, expectationsForUri) {
|
|
expectationsForUri.forEach((offset, expectationsAtOffset) {
|
|
for (_Expectation expectation in expectationsAtOffset) {
|
|
_problem(
|
|
uri,
|
|
offset,
|
|
'expected ${expectation.property}=${expectation.value}, '
|
|
'got nothing',
|
|
new _Fix(
|
|
expectation.commentOffset, expectation.commentLength, ''));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/// Updates the source file at [uri] based on the actual property/value
|
|
/// pairs that were observed.
|
|
Future<Null> fixSource(Uri uri, bool offsetsCountCharacters) async {
|
|
uri = Uri.base.resolveUri(uri);
|
|
List<_Fix> fixes = _fixes[uri];
|
|
if (fixes == null) return;
|
|
File file = new File.fromUri(uri);
|
|
List<int> bytes = (await file.readAsBytes()).toList();
|
|
int convertOffset(int offset) {
|
|
if (offsetsCountCharacters) {
|
|
return utf8.encode(utf8.decode(bytes).substring(0, offset)).length;
|
|
} else {
|
|
return offset;
|
|
}
|
|
}
|
|
|
|
// Apply the fixes in reverse order so that offsets don't need to be
|
|
// adjusted after each fix.
|
|
fixes.sort((a, b) => a.offset.compareTo(b.offset));
|
|
for (_Fix fix in fixes.reversed) {
|
|
bytes.replaceRange(convertOffset(fix.offset),
|
|
convertOffset(fix.offset + fix.length), utf8.encode(fix.replacement));
|
|
}
|
|
await file.writeAsBytes(bytes);
|
|
}
|
|
|
|
/// Loads expectations from the source file located at [uri].
|
|
///
|
|
/// Should be called before [finish].
|
|
Future<Null> loadExpectations(Uri uri) async {
|
|
uri = Uri.base.resolveUri(uri);
|
|
List<int> bytes = await readBytesFromFile(uri);
|
|
Map<int, List<_Expectation>> expectations =
|
|
_unsatisfiedExpectations.putIfAbsent(uri, () => {});
|
|
Map<int, Set<String>> testedFeaturesState =
|
|
_testedFeaturesState.putIfAbsent(uri, () => {});
|
|
List<int> tokenOffsets = _tokenOffsets.putIfAbsent(uri, () => []);
|
|
ScannerResult result = scan(bytes, includeComments: true);
|
|
for (Token token = result.tokens; !token.isEof; token = token.next) {
|
|
tokenOffsets.add(token.offset);
|
|
for (analyzer.Token commentToken = token.precedingComments;
|
|
commentToken != null;
|
|
commentToken = commentToken.next) {
|
|
String lexeme = commentToken.lexeme;
|
|
if (lexeme.startsWith('/*@') && lexeme.endsWith('*/')) {
|
|
String expectation = lexeme.substring(3, lexeme.length - 2);
|
|
int equals = expectation.indexOf('=');
|
|
String property;
|
|
String value;
|
|
if (equals == -1) {
|
|
property = expectation;
|
|
value = '';
|
|
} else {
|
|
property = expectation.substring(0, equals);
|
|
value = expectation
|
|
.substring(equals + 1)
|
|
.replaceAllMapped(_ESCAPE_SEQUENCE, (m) => m.group(1));
|
|
}
|
|
property = property.trim();
|
|
value = value.trim();
|
|
if (property == 'testedFeatures') {
|
|
Set<String> state = new Set<String>();
|
|
for (String feature in value.split(',')) {
|
|
feature = feature.trim();
|
|
// If an unrecognized feature name is found, it is assumed to be
|
|
// just a property name.
|
|
state.addAll(_FEATURES[feature] ?? [feature]);
|
|
}
|
|
testedFeaturesState[commentToken.offset] = state;
|
|
} else {
|
|
int offset = token.charOffset;
|
|
List<_Expectation> expectationsAtOffset =
|
|
expectations.putIfAbsent(offset, () => []);
|
|
expectationsAtOffset.add(new _Expectation(
|
|
property, value, commentToken.offset, commentToken.length));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void record(
|
|
Uri uri, int offset, String property, InstrumentationValue value) {
|
|
uri = Uri.base.resolveUri(uri);
|
|
if (offset == -1) {
|
|
throw _formatProblem(uri, 0, 'No offset for $property=$value', null);
|
|
}
|
|
Map<int, List<_Expectation>> expectationsForUri =
|
|
_unsatisfiedExpectations[uri];
|
|
if (expectationsForUri == null) return;
|
|
offset = _normalizeOffset(offset, _tokenOffsets[uri]);
|
|
List<_Expectation> expectationsAtOffset = expectationsForUri[offset];
|
|
if (expectationsAtOffset != null) {
|
|
for (int i = 0; i < expectationsAtOffset.length; i++) {
|
|
_Expectation expectation = expectationsAtOffset[i];
|
|
if (expectation.property == property) {
|
|
if (!value.matches(expectation.value)) {
|
|
_problem(
|
|
uri,
|
|
offset,
|
|
'expected $property=${expectation.value}, got '
|
|
'$property=${value.toString()}',
|
|
new _Fix(expectation.commentOffset, expectation.commentLength,
|
|
_makeExpectationComment(property, value)));
|
|
}
|
|
expectationsAtOffset.removeAt(i);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// Unexpected property/value pair. See if we should report.
|
|
if (_shouldCheck(property, uri, offset)) {
|
|
_problem(
|
|
uri,
|
|
offset,
|
|
'expected nothing, got $property=${value.toString()}',
|
|
new _Fix(offset, 0, _makeExpectationComment(property, value)));
|
|
}
|
|
}
|
|
|
|
String _escape(String s) {
|
|
s = s.replaceAll(r'\', r'\\');
|
|
if (s.endsWith('/')) {
|
|
s = '$s ';
|
|
}
|
|
return s.replaceAll('/*', r'/\*').replaceAll('*/', r'*\/');
|
|
}
|
|
|
|
String _formatProblem(
|
|
Uri uri, int offset, String desc, StackTrace stackTrace) {
|
|
return CompilerContext.current
|
|
.format(
|
|
templateUnspecified
|
|
.withArguments(
|
|
'$desc${stackTrace == null ? '' : '\n$stackTrace'}')
|
|
.withLocation(uri, offset, noLength),
|
|
Severity.internalProblem)
|
|
.plain;
|
|
}
|
|
|
|
String _makeExpectationComment(String property, InstrumentationValue value) {
|
|
return '/*@$property=${_escape(value.toString())}*/';
|
|
}
|
|
|
|
/// If [offset] is not one of the token offsets in [tokenOffsets], increase it
|
|
/// until it is.
|
|
///
|
|
/// Exception: if [offset] is past the last token offset in [tokenOffsets],
|
|
/// leave it alone.
|
|
int _normalizeOffset(int offset, List<int> tokenOffsets) {
|
|
int i = -1;
|
|
int j = tokenOffsets.length;
|
|
// Invariant: (i == -1 || tokenOffsets[i] < offset) &&
|
|
// (j == tokenOffsets.length || offset <= tokenOffsets[j])
|
|
while (j - i > 1) {
|
|
int k = (i + j) ~/ 2;
|
|
if (tokenOffsets[k] < offset) {
|
|
i = k;
|
|
} else {
|
|
j = k;
|
|
}
|
|
}
|
|
// Now i == j - 1
|
|
if (j < tokenOffsets.length) {
|
|
// tokenOffsets[j-1] < offset <= tokenOffsets[j]
|
|
// Therefore the comment belongs with token j.
|
|
return tokenOffsets[j];
|
|
} else {
|
|
// j-1 is the last token, and tokenOffsets[j-1] < offset. Since there's
|
|
// no later token, we can't normalize the offset.
|
|
return offset;
|
|
}
|
|
}
|
|
|
|
void _problem(Uri uri, int offset, String desc, _Fix fix) {
|
|
_problems.add(_formatProblem(uri, offset, desc, null));
|
|
_fixes.putIfAbsent(uri, () => []).add(fix);
|
|
}
|
|
|
|
/// ignore: unused_element
|
|
void _problemWithStack(Uri uri, int offset, String desc, _Fix fix) {
|
|
_problems.add(_formatProblem(uri, offset, desc, StackTrace.current));
|
|
_fixes.putIfAbsent(uri, () => []).add(fix);
|
|
}
|
|
|
|
bool _shouldCheck(String property, Uri uri, int offset) {
|
|
bool state = false;
|
|
Map<int, Set<String>> testedFeaturesStateForUri = _testedFeaturesState[uri];
|
|
if (testedFeaturesStateForUri == null) return false;
|
|
for (int i in testedFeaturesStateForUri.keys) {
|
|
if (i > offset) break;
|
|
Set<String> testedFeaturesStateAtOffset = testedFeaturesStateForUri[i];
|
|
state = testedFeaturesStateAtOffset.contains(property);
|
|
}
|
|
return state;
|
|
}
|
|
}
|
|
|
|
class _Expectation {
|
|
final String property;
|
|
final String value;
|
|
final int commentOffset;
|
|
final int commentLength;
|
|
|
|
_Expectation(
|
|
this.property, this.value, this.commentOffset, this.commentLength);
|
|
}
|
|
|
|
class _Fix {
|
|
final int offset;
|
|
final int length;
|
|
final String replacement;
|
|
|
|
_Fix(this.offset, this.length, this.replacement);
|
|
}
|