report errors in included options files

Example error messages include:
/analysis_options.yaml(10..27): The include file other_options.yaml in /analysis_options.yaml cannot be found.
/analysis_options.yaml(10..27): Bad options file format (expected String scope key, got YamlScalar) in /other_options.yaml(0..0)
/analysis_options.yaml(10..27): Warning in the included options file /other_options.yaml(47..49): The option 'ftw' isn't supported by 'errors'.

Since the included options file may not be part of the project,
the error is reported on the initial include statement
and includes the needed location information for the user to track down the problem.

R=brianwilkerson@google.com

Review URL: https://codereview.chromium.org/2502233004 .
This commit is contained in:
Dan Rubel 2016-11-17 14:43:05 -05:00
parent f43b48e34d
commit 0d15f1d892
4 changed files with 191 additions and 10 deletions

View file

@ -10,6 +10,7 @@ import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/source/source_resource.dart';
import 'package:analyzer/src/task/options.dart';
import 'package:analyzer/src/util/yaml.dart';
import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';
@ -25,6 +26,8 @@ class AnalysisOptionsProvider {
/// Provide the options found in either
/// [root]/[AnalysisEngine.ANALYSIS_OPTIONS_FILE] or
/// [root]/[AnalysisEngine.ANALYSIS_OPTIONS_YAML_FILE].
/// Recursively merge options referenced by an include directive
/// and remove the include directive from the resulting options map.
/// Return an empty options map if the file does not exist.
Map<String, YamlNode> getOptions(Folder root, {bool crawlUp: false}) {
Resource resource;
@ -42,17 +45,21 @@ class AnalysisOptionsProvider {
}
/// Provide the options found in [file].
/// Recursively merge options referenced by an include directive
/// and remove the include directive from the resulting options map.
/// Return an empty options map if the file does not exist.
Map<String, YamlNode> getOptionsFromFile(File file) {
return getOptionsFromSource(new FileSource(file));
}
/// Provide the options found in [source].
/// Recursively merge options referenced by an include directive
/// and remove the include directive from the resulting options map.
/// Return an empty options map if the file does not exist.
Map<String, YamlNode> getOptionsFromSource(Source source) {
Map<String, YamlNode> options =
getOptionsFromString(_readAnalysisOptions(source));
YamlNode node = options.remove('include');
YamlNode node = options.remove(AnalyzerOptions.include);
if (sourceFactory != null && node is YamlScalar) {
var path = node.value;
if (path is String) {
@ -64,6 +71,8 @@ class AnalysisOptionsProvider {
}
/// Provide the options found in [optionsSource].
/// An include directive, if present, will be left as-is,
/// and the referenced options will NOT be merged into the result.
/// Return an empty options map if the source is null.
Map<String, YamlNode> getOptionsFromString(String optionsSource) {
Map<String, YamlNode> options = <String, YamlNode>{};

View file

@ -22,6 +22,19 @@ class AnalysisOptionsErrorCode extends ErrorCode {
static const AnalysisOptionsErrorCode PARSE_ERROR =
const AnalysisOptionsErrorCode('PARSE_ERROR', '{0}');
/**
* An error code indicating that there is a syntactic error
* in the included file.
*
* Parameters:
* 0: the path of the file containing the error
* 1: the starting offset of the text in the file that contains the error
* 2: the ending offset of the text in the file that contains the error
* 3: the error message
*/
static const INCLUDED_FILE_PARSE_ERROR = const AnalysisOptionsErrorCode(
'INCLUDED_FILE_PARSE_ERROR', '{3} in {0}({1}..{2})');
/**
* Initialize a newly created error code to have the given [name].
*/
@ -43,6 +56,30 @@ class AnalysisOptionsErrorCode extends ErrorCode {
* wrong and, when appropriate, how the problem can be corrected.
*/
class AnalysisOptionsWarningCode extends ErrorCode {
/**
* An error code indicating a specified include file could not be found.
*
* Parameters:
* 0: the uri of the file to be included
* 1: the path of the file containing the include directive
*/
static const AnalysisOptionsWarningCode INCLUDE_FILE_NOT_FOUND =
const AnalysisOptionsWarningCode('INCLUDE_FILE_NOT_FOUND',
"The include file {0} in {1} cannot be found.");
/**
* An error code indicating a specified include file has a warning.
*
* Parameters:
* 0: the path of the file containing the warnings
* 1: the starting offset of the text in the file that contains the warning
* 2: the ending offset of the text in the file that contains the warning
* 3: the warning message
*/
static const AnalysisOptionsWarningCode INCLUDED_FILE_WARNING =
const AnalysisOptionsWarningCode('INCLUDED_FILE_WARNING',
"Warning in the included options file {0}({1}..{2}): {3}");
/**
* An error code indicating that a plugin is being configured with an
* unsupported option and legal options are provided.

View file

@ -63,6 +63,7 @@ class AnalyzerOptions {
static const String errors = 'errors';
static const String exclude = 'exclude';
static const String include = 'include';
static const String language = 'language';
static const String plugins = 'plugins';
static const String strong_mode = 'strong-mode';
@ -228,10 +229,12 @@ class GenerateOptionsErrorsTask extends SourceBasedAnalysisTask {
<ResultDescriptor>[ANALYSIS_OPTIONS_ERRORS, LINE_INFO],
suitabilityFor: suitabilityFor);
final AnalysisOptionsProvider optionsProvider = new AnalysisOptionsProvider();
AnalysisOptionsProvider optionsProvider;
GenerateOptionsErrorsTask(AnalysisContext context, AnalysisTarget target)
: super(context, target);
: super(context, target) {
optionsProvider = new AnalysisOptionsProvider(context?.sourceFactory);
}
@override
TaskDescriptor get descriptor => DESCRIPTOR;
@ -243,16 +246,80 @@ class GenerateOptionsErrorsTask extends SourceBasedAnalysisTask {
String content = getRequiredInput(CONTENT_INPUT_NAME);
List<AnalysisError> errors = <AnalysisError>[];
Source initialSource = source;
SourceSpan initialIncludeSpan;
// Validate the specified options and any included option files
void validate(Source source, Map<String, YamlNode> options) {
List<AnalysisError> validationErrors =
new OptionsFileValidator(source).validate(options);
if (initialIncludeSpan != null && validationErrors.isNotEmpty) {
for (AnalysisError error in validationErrors) {
var args = [
source.fullName,
error.offset.toString(),
(error.offset + error.length - 1).toString(),
error.message,
];
errors.add(new AnalysisError(
initialSource,
initialIncludeSpan.start.column + 1,
initialIncludeSpan.length,
AnalysisOptionsWarningCode.INCLUDED_FILE_WARNING,
args));
}
} else {
errors.addAll(validationErrors);
}
YamlNode node = options[AnalyzerOptions.include];
if (node == null) {
return;
}
SourceSpan span = node.span;
initialIncludeSpan ??= span;
String includeUri = span.text;
Source includedSource =
context.sourceFactory.resolveUri(source, includeUri);
if (!includedSource.exists()) {
errors.add(new AnalysisError(
initialSource,
initialIncludeSpan.start.column + 1,
initialIncludeSpan.length,
AnalysisOptionsWarningCode.INCLUDE_FILE_NOT_FOUND,
[includeUri, source.fullName]));
return;
}
try {
Map<String, YamlNode> options =
optionsProvider.getOptionsFromString(includedSource.contents.data);
validate(includedSource, options);
} on OptionsFormatException catch (e) {
var args = [
includedSource.fullName,
e.span.start.offset.toString(),
e.span.end.offset.toString(),
e.message,
];
// Report errors for included option files
// on the include directive located in the initial options file.
errors.add(new AnalysisError(
initialSource,
initialIncludeSpan.start.column + 1,
initialIncludeSpan.length,
AnalysisOptionsErrorCode.INCLUDED_FILE_PARSE_ERROR,
args));
}
}
try {
Map<String, YamlNode> options =
optionsProvider.getOptionsFromString(content);
errors.addAll(_validate(options));
validate(source, options);
} on OptionsFormatException catch (e) {
SourceSpan span = e.span;
var error = new AnalysisError(source, span.start.column + 1, span.length,
AnalysisOptionsErrorCode.PARSE_ERROR, [e.message]);
errors.add(error);
errors.add(new AnalysisError(source, span.start.column + 1, span.length,
AnalysisOptionsErrorCode.PARSE_ERROR, [e.message]));
}
//
@ -262,9 +329,6 @@ class GenerateOptionsErrorsTask extends SourceBasedAnalysisTask {
outputs[LINE_INFO] = computeLineInfo(content);
}
List<AnalysisError> _validate(Map<String, YamlNode> options) =>
new OptionsFileValidator(source).validate(options);
/// Return a map from the names of the inputs of this kind of task to the
/// task input descriptors describing those inputs for a task with the
/// given [target].

View file

@ -226,6 +226,77 @@ abstract class GenerateOptionsErrorsTaskTest extends AbstractContextTest {
expect(errors[0].errorCode, AnalysisOptionsErrorCode.PARSE_ERROR);
}
test_perform_include() {
newSource('/other_options.yaml', '');
String code = r'''
include: other_options.yaml
''';
AnalysisTarget target = newSource(optionsFilePath, code);
computeResult(target, ANALYSIS_OPTIONS_ERRORS);
expect(task, isGenerateOptionsErrorsTask);
List<AnalysisError> errors =
outputs[ANALYSIS_OPTIONS_ERRORS] as List<AnalysisError>;
expect(errors, hasLength(0));
}
test_perform_include_bad_value() {
newSource('/other_options.yaml', '''
analyzer:
errors:
unused_local_variable: ftw
''');
String code = r'''
include: other_options.yaml
''';
AnalysisTarget target = newSource(optionsFilePath, code);
computeResult(target, ANALYSIS_OPTIONS_ERRORS);
expect(task, isGenerateOptionsErrorsTask);
List<AnalysisError> errors =
outputs[ANALYSIS_OPTIONS_ERRORS] as List<AnalysisError>;
expect(errors, hasLength(1));
AnalysisError error = errors[0];
expect(error.errorCode, AnalysisOptionsWarningCode.INCLUDED_FILE_WARNING);
expect(error.source, target.source);
expect(error.offset, 10);
expect(error.length, 18);
expect(error.message, contains('other_options.yaml(47..49)'));
}
test_perform_include_bad_yaml() {
newSource('/other_options.yaml', ':');
String code = r'''
include: other_options.yaml
''';
AnalysisTarget target = newSource(optionsFilePath, code);
computeResult(target, ANALYSIS_OPTIONS_ERRORS);
expect(task, isGenerateOptionsErrorsTask);
List<AnalysisError> errors =
outputs[ANALYSIS_OPTIONS_ERRORS] as List<AnalysisError>;
expect(errors, hasLength(1));
AnalysisError error = errors[0];
expect(error.errorCode, AnalysisOptionsErrorCode.INCLUDED_FILE_PARSE_ERROR);
expect(error.source, target.source);
expect(error.offset, 10);
expect(error.length, 18);
expect(error.message, contains('other_options.yaml(0..0)'));
}
test_perform_include_missing() {
String code = r'''
include: other_options.yaml
''';
AnalysisTarget target = newSource(optionsFilePath, code);
computeResult(target, ANALYSIS_OPTIONS_ERRORS);
expect(task, isGenerateOptionsErrorsTask);
List<AnalysisError> errors =
outputs[ANALYSIS_OPTIONS_ERRORS] as List<AnalysisError>;
expect(errors, hasLength(1));
AnalysisError error = errors[0];
expect(error.errorCode, AnalysisOptionsWarningCode.INCLUDE_FILE_NOT_FOUND);
expect(error.offset, 10);
expect(error.length, 18);
}
test_perform_OK() {
String code = r'''
analyzer: