| // Copyright (c) 2019, 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 file. |
| |
| import 'package:analyzer/dart/analysis/features.dart'; |
| import 'package:analyzer/diagnostic/diagnostic.dart'; |
| import 'package:analyzer/error/error.dart'; |
| import 'package:analyzer/src/dart/analysis/experiments.dart'; |
| import 'package:analyzer_testing/package_config_file_builder.dart'; |
| import 'package:analyzer_testing/utilities/utilities.dart'; |
| import 'package:analyzer_utilities/analyzer_messages.dart'; |
| import 'package:analyzer_utilities/lint_messages.dart'; |
| import 'package:analyzer_utilities/messages.dart'; |
| import 'package:dart_style/dart_style.dart'; |
| import 'package:linter/src/lint_names.dart'; |
| import 'package:pub_semver/pub_semver.dart'; |
| import 'package:test/test.dart'; |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; |
| |
| import '../tool/messages/error_code_documentation_info.dart'; |
| import 'src/dart/resolution/context_collection_resolution.dart'; |
| |
| main() { |
| defineReflectiveSuite(() { |
| defineReflectiveTests(VerifyDiagnosticsTest); |
| }); |
| } |
| |
| /// A class used to validate diagnostic documentation. |
| class DocumentationValidator { |
| /// The sequence used to mark the start of an error range. |
| static const String errorRangeStart = '[!'; |
| |
| /// The sequence used to mark the end of an error range. |
| static const String errorRangeEnd = '!]'; |
| |
| /// Whether the formatting of the snippets should be validated. |
| /// |
| /// This should always be `true` except when rolling in a new version of |
| /// `dart_style`. To ease the process, this flag can be set to `false` prior |
| /// to the roll, and then reverted to `true` in a follow-on CL in which all |
| /// of the impacted doc snippets are updated to conform. |
| static const bool validateFormatting = true; |
| |
| /// A list of the diagnostic codes that are not being verified. These should |
| /// ony include docs that can't be verified because of missing support in the |
| /// verifier. |
| static const List<String> unverifiedDocs = [ |
| // The following diagnostics can't be verified because the examples aren't |
| // Dart code. The verifier needs to add the ability to verify YAML snippets |
| // and to use a YAML snippet as the example. If we can do this based on the |
| // class of the diagnostic, then there will be less chance of a false |
| // positive. |
| 'analysis_option_deprecated', |
| 'asset_does_not_exist', |
| 'asset_directory_does_not_exist', |
| 'asset_field_not_list', |
| 'asset_missing_path', |
| 'asset_not_string', |
| 'asset_not_string_or_map', |
| 'asset_path_not_string', |
| 'dependencies_field_not_map', |
| 'deprecated_field', |
| 'deprecated_lint', |
| 'duplicate_rule', |
| 'flutter_field_not_map', |
| 'included_file_parse_error', |
| 'included_file_warning', |
| 'include_file_not_found', |
| 'incompatible_lint', |
| 'invalid_dependency', |
| 'invalid_option', |
| 'invalid_platforms_field', |
| 'invalid_section_format', |
| 'missing_name', |
| 'missing_dependency', |
| 'name_not_string', |
| 'package_names', |
| 'path_does_not_exist', |
| 'path_not_posix', |
| 'path_pubspec_does_not_exist', |
| 'parse_error', |
| 'platform_value_disallowed', |
| 'plugins_in_inner_options', |
| 'recursive_include_file', |
| 'removed_lint', |
| 'removed_lint_use', |
| 'secure_pubspec_urls', |
| 'sort_pub_dependencies', |
| 'unknown_platform', |
| 'undefined_lint', |
| 'unnecessary_dev_dependency', |
| 'unrecognized_error_code', |
| 'unsupported_option_with_legal_value', |
| 'unsupported_value', |
| 'workspace_field_not_list', |
| 'workspace_value_not_string', |
| 'workspace_value_not_subdirectory', |
| |
| // The following diagnostics can't be verified because they necessarily |
| // produce more than one diagnostic. This is typically because of a conflict |
| // between two or more declarations, neither of which is obviously the |
| // better declaration to flag, and we have chosen to create a diagnostic for |
| // all of them. |
| 'ambiguous_import', |
| 'extension_type_implements_itself', |
| 'extension_type_representation_depends_on_itself', |
| 'recursive_compile_time_constant', |
| 'recursive_constructor_redirect', |
| 'recursive_interface_inheritance', |
| 'text_direction_code_point_in_comment', |
| 'text_direction_code_point_in_literal', |
| 'top_level_cycle', |
| 'type_alias_cannot_reference_itself', |
| 'type_parameter_supertype_of_its_bound', |
| |
| // const_constructor_param_type_mismatch (analyzer) |
| // - Expected an error with code const_constructor_param_type_mismatch, |
| // found const_eval_throws_exception (example 0). |
| // |
| // Based on the TODO comment below, it appears that this diagnostic is never |
| // reported, and it should be marked as removed. |
| // |
| // TODO(kallentu): This is always reported with |
| // `argument_type_not_assignable` or is reported as |
| // `const_eval_throws_exception` in const constructor evaluation. |
| 'const_constructor_param_type_mismatch', |
| |
| // invalid_implementation_override (analyzer) |
| // - No error range in example |
| // - Expected no errors but found 1 (example 1): |
| // undefined_class (7, 1) Undefined class 'B'. |
| // |
| // Has code in the example section that needs to be skipped (because it's |
| // part of the explanatory text not part of the example), but there's |
| // currently no way to do that. We could try to rewrite the text so that all |
| // of the code is in a single snippet, or we could introduce a way to skip |
| // some code blocks. |
| 'invalid_implementation_override', |
| |
| // invalid_uri (analyzer) |
| // - Expected an error with code invalid_uri, found uri_does_not_exist |
| // (example 0). |
| // |
| // It's possible that this diagnostic is no longer reported. |
| 'invalid_uri', |
| |
| // yield_each_in_non_generator (analyzer) |
| // - No error range in example |
| // - Expected no errors but found 2 (example 0): |
| // - undefined_identifier (29, 5) Undefined name 'yield'. |
| // - body_might_complete_normally (18, 6) The body might complete |
| // normally, causing 'null' to be returned, but the return type, |
| // 'Iterable<int>', is a potentially non-nullable type. |
| 'yield_each_in_non_generator', |
| |
| // deprecated_colon_for_default_value (analyzer) |
| // - Expected an error with code deprecated_colon_for_default_value, found |
| // obsolete_colon_for_default_value (example 0). |
| // |
| // This no longer works in 3.0 and should be marked as removed. |
| 'deprecated_colon_for_default_value', |
| |
| // deprecated_member_use (analyzer) |
| // - Expected an error with code deprecated_member_use, found |
| // undefined_class (example 0). |
| // |
| // The example needs to have a definition of `C` that is marked as |
| // deprecated. |
| 'deprecated_member_use', |
| |
| // avoid_relative_lib_imports (linter) |
| // - Expected one error but found 2 (example 0): |
| // - uri_does_not_exist (7, 15) Target of URI doesn't exist: '../lib/a.dart'. |
| // - avoid_relative_lib_imports (7, 15) Can't use a relative path to import a library in 'lib'. |
| // - Expected no errors but found 1 (fixes 0): |
| // - unused_import (7, 8) Unused import: 'a.dart'. |
| // |
| // Missing support for example files outside of `lib`. |
| 'avoid_relative_lib_imports', |
| |
| // avoid_web_libraries_in_flutter (linter) |
| // - Expected one error but found none (example 0). |
| // |
| // The example isn't being recognized as a flutter app. We might need to |
| // build a pubspec.yaml when analyzing flutter code. |
| 'avoid_web_libraries_in_flutter', |
| |
| // depend_on_referenced_packages (linter) |
| // - Expected one error but found none (example 0). |
| // |
| // The example doesn't generate the documented diagnostic. |
| 'depend_on_referenced_packages', |
| |
| // file_names (linter) |
| // - No example. |
| // |
| // There's no interesting file content to use as an example. We could have |
| // some placeholder content to get rid of the failure, but the documentation |
| // wouldn't be improved. |
| 'file_names', |
| |
| // prefer_inlined_adds_single (linter) |
| // - Expected one error but found none (example 1). |
| // |
| // Doesn't produce a lint for the second example, even though the analyzer |
| // does when the example is pasted into a file. |
| 'prefer_inlined_adds_single', |
| |
| // library_annotations (linter) |
| // - Expected an error with code library_annotations, found |
| // undefined_annotation (example 0). |
| // - Expected no errors but found 1 (fixes 0): |
| // - undefined_annotation (0, 18) Undefined name 'TestOn' used as an |
| // annotation. |
| // |
| // No mock 'test' package, no good library annotations in 'meta'. |
| 'library_annotations', |
| |
| // package_prefixed_library_names (linter) |
| // - Expected one error but found none (example 0). |
| // |
| // The lint does nothing, so no diagnostic is produced. I needs to be marked |
| // as 'removed'. |
| 'package_prefixed_library_names', |
| |
| // prefer_relative_imports (linter) |
| // No error range in example |
| // - Expected no errors but found 1 (example 0): |
| // - uri_does_not_exist (7, 29) Target of URI doesn't exist: |
| // 'package:my_package/bar.dart'. |
| // - Expected no errors but found 1 (fixes 0): |
| // - uri_does_not_exist (7, 10) Target of URI doesn't exist: 'bar.dart'. |
| // |
| // Need a way to specify the existance of files whose content is irrelevant. |
| // Either that or the example needs to include a minial file to refer to. |
| 'prefer_relative_imports', |
| |
| // public_member_api_docs (linter) |
| // - Expected one error but found none (example 0). |
| // |
| // The test file is in a basic workspace, so it can't have public API. I |
| // think we'd need to add a `pubspec.yaml` file to the example. |
| 'public_member_api_docs', |
| |
| // recursive_getters (linter) |
| // - Expected an error at 39, found 48 (example 0). |
| // |
| // The lint fires when the example is pasted into an empty file. |
| 'recursive_getters', |
| |
| // Missing a mock of `Expando` in `dart:core`. |
| 'extension_declares_instance_field', |
| |
| // deprecated_extends_function (analyzer) |
| // - Expected an error with code deprecated_subtype_of_function, found |
| // final_class_extended_outside_of_library (example 0). |
| // |
| // Probably needs a language override comment, but I don't know which |
| // version. |
| 'deprecated_extends_function', |
| ]; |
| |
| /// The buffer to which validation errors are written. |
| final StringBuffer buffer = StringBuffer(); |
| |
| /// The name of the package containing the variables currently being verified. |
| late String packageName; |
| |
| /// The name of the variable currently being verified. |
| late String variableName; |
| |
| /// The name of the error code currently being verified. |
| late String codeName; |
| |
| /// A flag indicating whether the [variableName] has already been written to |
| /// the buffer. |
| bool hasWrittenVariableName = false; |
| |
| /// Initialize a newly created documentation validator. |
| DocumentationValidator(); |
| |
| /// Validate the documentation. |
| Future<void> validate() async { |
| packageName = '_fe_analyzer_shared'; |
| await _validateMessages(feAnalyzerSharedMessages); |
| packageName = 'analyzer'; |
| await _validateMessages(analyzerMessages); |
| packageName = 'analysis_server'; |
| await _validateMessages(analysisServerMessages); |
| packageName = 'linter'; |
| await _validateMessages(lintMessages); |
| if (buffer.isNotEmpty) { |
| fail(buffer.toString()); |
| } |
| } |
| |
| _SnippetData _extractSnippetData({ |
| required String snippet, |
| required int index, |
| required bool errorRequired, |
| required bool onlyValidateFormatting, |
| required Map<String, String> auxiliaryFiles, |
| required List<String> experiments, |
| required List<String> ignores, |
| required String? languageVersion, |
| }) { |
| // TODO(brianwilkerson): This doesn't check to ensure that there are no |
| // ranges specified in fixes (when `errorRequired` is `false`), but it |
| // probably should. |
| var section = errorRequired ? _Section.examples : _Section.fixes; |
| |
| var experimentalFeatures = <Feature>[]; |
| for (var experiment in experiments) { |
| var feature = ExperimentStatus.knownFeatures[experiment]; |
| if (feature != null) { |
| experimentalFeatures.add(feature); |
| } else if (!onlyValidateFormatting) { |
| _reportProblem("Unknown experiment '$experiment' in $section $index"); |
| } |
| } |
| |
| int rangeStart = snippet.indexOf(errorRangeStart); |
| if (rangeStart < 0) { |
| if (errorRequired && !onlyValidateFormatting) { |
| _reportProblem('No error range in $section $index'); |
| } |
| return _SnippetData( |
| snippet, |
| -1, |
| 0, |
| auxiliaryFiles, |
| experimentalFeatures, |
| ignores, |
| languageVersion, |
| ); |
| } |
| int rangeEnd = snippet.indexOf(errorRangeEnd, rangeStart + 1); |
| if (rangeEnd < 0) { |
| if (!onlyValidateFormatting) { |
| _reportProblem('No end of error range in $section $index'); |
| } |
| return _SnippetData( |
| snippet, |
| -1, |
| 0, |
| auxiliaryFiles, |
| experimentalFeatures, |
| ignores, |
| languageVersion, |
| ); |
| } else if (snippet.indexOf(errorRangeStart, rangeEnd) > 0) { |
| if (!onlyValidateFormatting) { |
| _reportProblem('More than one error range in $section $index'); |
| } |
| } |
| String content; |
| try { |
| content = |
| snippet.substring(0, rangeStart) + |
| snippet.substring(rangeStart + errorRangeStart.length, rangeEnd) + |
| snippet.substring(rangeEnd + errorRangeEnd.length); |
| } on RangeError catch (exception) { |
| _reportProblem('${exception.message} in $section $index'); |
| content = ''; |
| } |
| return _SnippetData( |
| content, |
| rangeStart, |
| rangeEnd - rangeStart - 2, |
| auxiliaryFiles, |
| experimentalFeatures, |
| ignores, |
| languageVersion, |
| ); |
| } |
| |
| /// Extract the snippets of Dart code from [documentationParts] that are |
| /// tagged as belonging to the given [blockSection]. |
| List<_SnippetData> _extractSnippets( |
| List<ErrorCodeDocumentationPart> documentationParts, |
| BlockSection blockSection, |
| bool onlyValidateFormatting, |
| ) { |
| var snippets = <_SnippetData>[]; |
| var auxiliaryFiles = <String, String>{}; |
| for (var documentationPart in documentationParts) { |
| if (documentationPart is ErrorCodeDocumentationBlock) { |
| if (documentationPart.containingSection != blockSection) { |
| continue; |
| } |
| var uri = documentationPart.uri; |
| if (uri != null) { |
| auxiliaryFiles[uri] = documentationPart.text; |
| } else { |
| if (documentationPart.fileType == 'dart') { |
| snippets.add( |
| _extractSnippetData( |
| snippet: documentationPart.text, |
| index: snippets.length, |
| errorRequired: blockSection == BlockSection.examples, |
| onlyValidateFormatting: onlyValidateFormatting, |
| auxiliaryFiles: auxiliaryFiles, |
| experiments: documentationPart.experiments, |
| ignores: documentationPart.ignores, |
| languageVersion: documentationPart.languageVersion, |
| ), |
| ); |
| } |
| auxiliaryFiles = <String, String>{}; |
| } |
| } |
| } |
| return snippets; |
| } |
| |
| /// Report a problem with the current error code. |
| void _reportProblem( |
| String problem, { |
| List<Diagnostic> diagnostics = const [], |
| }) { |
| if (!hasWrittenVariableName) { |
| buffer.writeln(' $variableName ($packageName)'); |
| hasWrittenVariableName = true; |
| } |
| buffer.writeln(' $problem'); |
| for (Diagnostic diagnostic in diagnostics) { |
| buffer.write(' '); |
| buffer.write(diagnostic.diagnosticCode.lowerCaseName); |
| buffer.write(' ('); |
| buffer.write(diagnostic.offset); |
| buffer.write(', '); |
| buffer.write(diagnostic.length); |
| buffer.write(') '); |
| buffer.writeln(diagnostic.message); |
| } |
| } |
| |
| /// Validates that the [snippet] is formatted correctly. |
| /// |
| /// The snippet is identified for reporting purposes by the [section] and |
| /// [index]. |
| void _validateFormatting(_SnippetData snippet, _Section section, int index) { |
| if (!validateFormatting) return; |
| var ignoreFormatting = snippet.ignores.contains('formatting'); |
| String? formattedContent; |
| try { |
| var formatter = DartFormatter(languageVersion: Version(3, 13, 0)); |
| formattedContent = formatter.format(snippet.content).trimRight(); |
| if (formattedContent != snippet.content) { |
| if (!ignoreFormatting) { |
| _reportProblem(''' |
| Snippet is not formatted ($section $index). Content is: |
| ${snippet.content} |
| |
| Formatted content is: |
| $formattedContent'''); |
| } |
| } else if (ignoreFormatting) { |
| _reportProblem('Formatting is unnecessarily ignored ($section $index)'); |
| } |
| } catch (e) { |
| if (section != _Section.examples) { |
| // The assumption is that the examples will only fail to format when the |
| // diagnostic is an expected symtactic error, in which case we don't |
| // need to report the problem. |
| _reportProblem(''' |
| Exception while formatting ($section $index): $e |
| Content is: |
| ${snippet.content} |
| |
| Formatted content is: |
| $formattedContent'''); |
| } |
| } |
| } |
| |
| /// Extract documentation from the given [messages]. |
| Future<void> _validateMessages(List<MessageWithAnalyzerCode> messages) async { |
| for (var message in messages) { |
| // If the diagnostic is no longer generated, |
| // the corresponding code snippets won't report it. |
| if (message.isRemoved) { |
| continue; |
| } |
| var docs = parseErrorCodeDocumentation( |
| message.analyzerCode.toString(), |
| message.documentation, |
| ); |
| if (docs != null) { |
| codeName = (message.sharedName ?? message.analyzerCode).snakeCaseName; |
| // Even if the message has been exempted from other verification, |
| // the formatting of the code will still be verified. |
| var onlyValidateFormatting = unverifiedDocs.contains( |
| message.analyzerCode.snakeCaseName, |
| ); |
| if (onlyValidateFormatting && !validateFormatting) { |
| continue; |
| } |
| variableName = message.analyzerCode.camelCaseName; |
| hasWrittenVariableName = false; |
| |
| List<_SnippetData> exampleSnippets = _extractSnippets( |
| docs, |
| BlockSection.examples, |
| onlyValidateFormatting, |
| ); |
| _SnippetData? firstExample; |
| if (exampleSnippets.isEmpty) { |
| if (!onlyValidateFormatting) { |
| _reportProblem('No example.'); |
| } |
| } else { |
| firstExample = exampleSnippets[0]; |
| } |
| for (int i = 0; i < exampleSnippets.length; i++) { |
| _SnippetData snippet = exampleSnippets[i]; |
| if (message.type == AnalyzerDiagnosticType.lint) { |
| snippet.lintCode = codeName; |
| } |
| _validateFormatting(snippet, _Section.examples, i); |
| if (!onlyValidateFormatting) { |
| await _validateSnippet(snippet, _Section.examples, i); |
| } |
| } |
| |
| List<_SnippetData> fixesSnippets = _extractSnippets( |
| docs, |
| BlockSection.commonFixes, |
| onlyValidateFormatting, |
| ); |
| for (int i = 0; i < fixesSnippets.length; i++) { |
| _SnippetData snippet = fixesSnippets[i]; |
| if (firstExample != null) { |
| snippet.auxiliaryFiles.addAll(firstExample.auxiliaryFiles); |
| } |
| if (message.type == AnalyzerDiagnosticType.lint) { |
| snippet.lintCode = codeName; |
| } |
| _validateFormatting(snippet, _Section.fixes, i); |
| if (!onlyValidateFormatting) { |
| await _validateSnippet(snippet, _Section.fixes, i); |
| } |
| } |
| } |
| } |
| } |
| |
| /// Resolve the [snippet]. If the snippet's offset is less than zero, then |
| /// verify that no diagnostics are reported. If the offset is greater than or |
| /// equal to zero, verify that one error whose name matches the current code |
| /// is reported at that offset with the expected length. |
| Future<void> _validateSnippet( |
| _SnippetData snippet, |
| _Section section, |
| int index, |
| ) async { |
| var test = _SnippetTest(snippet); |
| test.setUp(); |
| var result = await test.resolveTestFile(); |
| var diagnostics = result.diagnostics; |
| var filteredDiagnostics = <Diagnostic>[]; |
| var errorCount = 0; |
| var unneededIgnores = snippet.ignores.toList()..remove('formatting'); |
| for (var diagnostic in diagnostics) { |
| var diagnosticName = diagnostic.diagnosticCode.lowerCaseName; |
| if (snippet.ignores.contains(diagnosticName)) { |
| unneededIgnores.remove(diagnosticName); |
| } else { |
| errorCount++; |
| filteredDiagnostics.add(diagnostic); |
| } |
| } |
| |
| if (snippet.offset < 0) { |
| if (errorCount > 0) { |
| _reportProblem( |
| 'Expected no errors but found $errorCount ($section $index):', |
| diagnostics: diagnostics, |
| ); |
| } |
| } else { |
| if (errorCount == 0) { |
| _reportProblem('Expected one error but found none ($section $index).'); |
| } else if (errorCount == 1) { |
| var diagnostic = filteredDiagnostics[0]; |
| if (diagnostic.diagnosticCode.lowerCaseName != codeName) { |
| _reportProblem( |
| 'Expected an error with code $codeName, ' |
| 'found ${diagnostic.diagnosticCode.lowerCaseName} ($section $index).', |
| ); |
| } |
| if (diagnostic.offset != snippet.offset) { |
| _reportProblem( |
| 'Expected an error at ${snippet.offset}, ' |
| 'found ${diagnostic.offset} ($section $index).', |
| ); |
| } |
| if (diagnostic.length != snippet.length) { |
| _reportProblem( |
| 'Expected an error of length ${snippet.length}, ' |
| 'found ${diagnostic.length} ($section $index).', |
| ); |
| } |
| } else { |
| _reportProblem( |
| 'Expected one error but found $errorCount ($section $index):', |
| diagnostics: diagnostics, |
| ); |
| } |
| } |
| if (unneededIgnores.isNotEmpty) { |
| var list = unneededIgnores.join(', '); |
| _reportProblem('Unneeded ignores: $list ($section $index).'); |
| } |
| } |
| } |
| |
| /// Validate the documentation associated with the declarations of the error |
| /// codes. |
| @reflectiveTest |
| class VerifyDiagnosticsTest { |
| @TestTimeout(Timeout.factor(4)) |
| test_diagnostics() async { |
| // |
| // Validate that the input to the generator is correct. |
| // |
| DocumentationValidator validator = DocumentationValidator(); |
| await validator.validate(); |
| } |
| |
| test_published() { |
| // Verify that if _any_ error code is marked as having published docs then |
| // _all_ codes with the same name are also marked that way. |
| var nameToCodeMap = <String, List<DiagnosticCode>>{}; |
| var nameToPublishedMap = <String, bool>{}; |
| for (var code in diagnosticCodeValues) { |
| var name = code.lowerCaseName; |
| nameToCodeMap.putIfAbsent(name, () => []).add(code); |
| nameToPublishedMap[name] = |
| (nameToPublishedMap[name] ?? false) || code.hasPublishedDocs; |
| } |
| var unpublished = <DiagnosticCode>[]; |
| for (var entry in nameToCodeMap.entries) { |
| var name = entry.key; |
| if (nameToPublishedMap[name]!) { |
| for (var code in entry.value) { |
| if (!code.hasPublishedDocs) { |
| unpublished.add(code); |
| } |
| } |
| } |
| } |
| if (unpublished.isNotEmpty) { |
| var buffer = StringBuffer(); |
| buffer.write( |
| "The following error codes have published docs but aren't " |
| "marked as such:", |
| ); |
| for (var code in unpublished) { |
| buffer.writeln(); |
| buffer.write('- ${code.runtimeType}.${code.lowerCaseUniqueName}'); |
| } |
| fail(buffer.toString()); |
| } |
| } |
| } |
| |
| enum _Section { |
| examples, |
| fixes; |
| |
| @override |
| String toString() => name; |
| } |
| |
| /// A data holder used to return multiple values when extracting an error range |
| /// from a snippet. |
| class _SnippetData { |
| final String content; |
| final int offset; |
| final int length; |
| final Map<String, String> auxiliaryFiles; |
| final List<Feature> experimentalFeatures; |
| final List<String> ignores; |
| final String? languageVersion; |
| String? lintCode; |
| |
| _SnippetData( |
| this.content, |
| this.offset, |
| this.length, |
| this.auxiliaryFiles, |
| this.experimentalFeatures, |
| this.ignores, |
| this.languageVersion, |
| ); |
| } |
| |
| /// A test class that creates an environment suitable for analyzing the |
| /// snippets. |
| class _SnippetTest extends PubPackageResolutionTest { |
| /// The lints that are used to find code that needs to be updated to conform |
| /// with the currently recommended style. |
| /// |
| /// The lints that are commented out are lints that currently cause the test |
| /// to break. There are two common reasons. |
| /// |
| /// - There is are examples that generate multiple diagnostics when they |
| /// should only generate a single diagnostic. Most of the time this will be |
| /// handled by adding an `%ignore` directive and filing an issue to reduce |
| /// the number of diagnostics being produced. |
| /// |
| /// - There is are examples that need to be updated in order to conform. The |
| /// examples will be updated and the lint uncommented. |
| // |
| // TODO(brianwilkerson): We should consider reading the list of 'core' and |
| // 'recommended' lints from `sdk/third_party/pkg/core/pkgs/lints/lib`. Doing |
| // so would have the advantage that the test would always use the most |
| // current set of lints. It would have the disadvantage that it might be more |
| // difficult to roll in a new version of these files. |
| // |
| // If we do that, we'll need to have a list of rules that we exclude so that |
| // lints that are just too hard to accomodate can be ignored, and so that |
| // there's a way to simplify rolls. |
| // |
| // If we don't do that, then this to-do should be removed and the comment |
| // updated to indicate why we chose not to. |
| // |
| // I'm not sure how we could get the flutter lints, so they might have to |
| // remain an explicitly duplicated list. |
| static final List<String> lints = [ |
| // The lints from the 'core' lint set. |
| LintNames.avoid_empty_else, |
| LintNames.avoid_relative_lib_imports, |
| LintNames.avoid_shadowing_type_parameters, |
| // LintNames.avoid_types_as_parameter_names, |
| LintNames.await_only_futures, |
| // LintNames.camel_case_extensions, |
| // LintNames.camel_case_types, |
| LintNames.collection_methods_unrelated_type, |
| // LintNames.curly_braces_in_flow_control_structures, |
| LintNames.dangling_library_doc_comments, |
| LintNames.depend_on_referenced_packages, |
| LintNames.empty_catches, |
| LintNames.file_names, |
| // LintNames.hash_and_equals, |
| LintNames.implicit_call_tearoffs, |
| LintNames.library_annotations, |
| LintNames.no_duplicate_case_values, |
| LintNames.no_wildcard_variable_uses, |
| // LintNames.non_constant_identifier_names, |
| LintNames.null_check_on_nullable_type_parameter, |
| LintNames.prefer_generic_function_type_aliases, |
| LintNames.prefer_is_empty, |
| LintNames.prefer_is_not_empty, |
| LintNames.prefer_iterable_wheretype, |
| // LintNames.prefer_typing_uninitialized_variables, |
| LintNames.provide_deprecation_message, |
| LintNames.secure_pubspec_urls, |
| // LintNames.strict_top_level_inference, |
| LintNames.type_literal_in_constant_pattern, |
| LintNames.unintended_html_in_doc_comment, |
| // LintNames.unnecessary_overrides, |
| LintNames.unrelated_type_equality_checks, |
| LintNames.use_string_in_part_of_directives, |
| LintNames.valid_regexps, |
| LintNames.void_checks, |
| |
| // The lints from the 'recommended' lint set. |
| LintNames.annotate_overrides, |
| // LintNames.avoid_function_literals_in_foreach_calls, |
| LintNames.avoid_init_to_null, |
| LintNames.avoid_renaming_method_parameters, |
| // LintNames.avoid_return_types_on_setters, |
| LintNames.avoid_returning_null_for_void, |
| // LintNames.avoid_single_cascade_in_expression_statements, |
| LintNames.constant_identifier_names, |
| LintNames.control_flow_in_finally, |
| // LintNames.empty_constructor_bodies, |
| // LintNames.empty_statements, |
| LintNames.exhaustive_cases, |
| // LintNames.implementation_imports, |
| LintNames.invalid_runtime_check_with_js_interop_types, |
| LintNames.library_prefixes, |
| LintNames.library_private_types_in_public_api, |
| LintNames.no_leading_underscores_for_library_prefixes, |
| LintNames.no_leading_underscores_for_local_identifiers, |
| LintNames.null_closures, |
| LintNames.overridden_fields, |
| LintNames.package_names, |
| // LintNames.prefer_adjacent_string_concatenation, |
| LintNames.prefer_collection_literals, |
| LintNames.prefer_conditional_assignment, |
| LintNames.prefer_contains, |
| LintNames.prefer_final_fields, |
| LintNames.prefer_for_elements_to_map_fromiterable, |
| // LintNames.prefer_function_declarations_over_variables, |
| LintNames.prefer_if_null_operators, |
| // LintNames.prefer_initializing_formals, |
| LintNames.prefer_inlined_adds, |
| LintNames.prefer_interpolation_to_compose_strings, |
| LintNames.prefer_is_not_operator, |
| LintNames.prefer_null_aware_operators, |
| LintNames.prefer_spread_collections, |
| LintNames.recursive_getters, |
| LintNames.slash_for_doc_comments, |
| LintNames.type_init_formals, |
| LintNames.unnecessary_brace_in_string_interps, |
| LintNames.unnecessary_const, |
| LintNames.unnecessary_constructor_name, |
| LintNames.unnecessary_getters_setters, |
| LintNames.unnecessary_late, |
| // LintNames.unnecessary_library_name, |
| // LintNames.unnecessary_new, |
| LintNames.unnecessary_null_aware_assignments, |
| LintNames.unnecessary_null_in_if_null_operators, |
| LintNames.unnecessary_nullable_for_final_variable_declarations, |
| LintNames.unnecessary_string_escapes, |
| LintNames.unnecessary_string_interpolations, |
| LintNames.unnecessary_this, |
| LintNames.unnecessary_to_list_in_spreads, |
| LintNames.unnecessary_underscores, |
| // LintNames.use_function_type_syntax_for_parameters, |
| LintNames.use_null_aware_elements, |
| LintNames.use_rethrow_when_possible, |
| // LintNames.use_super_parameters, |
| |
| // The lints from the 'flutter' lint set. |
| // LintNames.avoid_print, |
| LintNames.avoid_unnecessary_containers, |
| LintNames.avoid_web_libraries_in_flutter, |
| LintNames.no_logic_in_create_state, |
| // LintNames.prefer_const_constructors_in_immutables, |
| LintNames.sized_box_for_whitespace, |
| LintNames.sort_child_properties_last, |
| LintNames.use_build_context_synchronously, |
| LintNames.use_full_hex_values_for_flutter_colors, |
| // LintNames.use_key_in_widget_constructors, |
| |
| // Additional lints. |
| // |
| // At this point these are purely speculative. The goal of any additional |
| // lints should be to conform to common standards or to catch errors, but |
| // there is no process at this point for deciding which lints to add. |
| // |
| // LintNames.empty_container_bodies, |
| // LintNames.prefer_single_quotes, |
| // LintNames.unnecessary_const_in_enum_constructor, |
| LintNames.unnecessary_constructor_name, |
| LintNames.unnecessary_primary_constructor_body, |
| // LintNames.unnecessary_type_name_in_constructor, |
| LintNames.var_with_no_type_annotation, |
| ]; |
| |
| /// The snippet being tested. |
| final _SnippetData snippet; |
| |
| /// Initialize a newly created test to test the given [snippet]. |
| _SnippetTest(this.snippet) { |
| writeTestPackageAnalysisOptionsFile( |
| analysisOptionsContent( |
| experimentalFeatures: snippet.experimentalFeatures, |
| ), |
| ); |
| } |
| |
| @override |
| String? get testPackageLanguageVersion { |
| return snippet.languageVersion; |
| } |
| |
| @override |
| String get testPackageRootPath => '$workspaceRootPath/docTest'; |
| |
| @override |
| void setUp() { |
| super.setUp(); |
| _createAnalysisOptionsFile(); |
| _createAuxiliaryFiles(snippet.auxiliaryFiles); |
| addTestFile(snippet.content); |
| } |
| |
| void _createAnalysisOptionsFile() { |
| var lintCode = snippet.lintCode; |
| var rules = [?lintCode, ...lints]; |
| var experimentalFeatures = snippet.experimentalFeatures; |
| if (rules.isNotEmpty || experimentalFeatures.isNotEmpty) { |
| writeTestPackageAnalysisOptionsFile( |
| analysisOptionsContent( |
| rules: rules, |
| experimentalFeatures: experimentalFeatures, |
| ), |
| ); |
| } |
| } |
| |
| void _createAuxiliaryFiles(Map<String, String> auxiliaryFiles) { |
| var packageConfigBuilder = PackageConfigFileBuilder(); |
| for (String uriStr in auxiliaryFiles.keys) { |
| if (uriStr.startsWith('package:')) { |
| Uri uri = Uri.parse(uriStr); |
| |
| String packageName = uri.pathSegments[0]; |
| String packageRootPath = '/packages/$packageName'; |
| packageConfigBuilder.add( |
| name: packageName, |
| rootFolder: getFolder(packageRootPath), |
| ); |
| |
| String pathInLib = uri.pathSegments.skip(1).join('/'); |
| newFile('$packageRootPath/lib/$pathInLib', auxiliaryFiles[uriStr]!); |
| } else { |
| newFile('$testPackageRootPath/$uriStr', auxiliaryFiles[uriStr]!); |
| } |
| } |
| writeTestPackageConfig( |
| packageConfigBuilder, |
| angularMeta: true, |
| ffi: true, |
| flutter: true, |
| meta: true, |
| ); |
| } |
| } |