blob: 1c1af83ddd750b0e98b8a58aa544350a87d5f7f7 [file] [log] [blame]
// 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);
}