| // Copyright 2014 The Flutter Authors. 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:flutter_tools/src/base/file_system.dart'; |
| import 'package:flutter_tools/src/base/io.dart'; |
| import '../src/common.dart'; |
| import 'test_utils.dart'; |
| |
| final String analyzerSeparator = platform.isWindows ? '-' : '•'; |
| |
| void main() { |
| late Directory tempDir; |
| late String projectPath; |
| late File libMain; |
| late File errorFile; |
| |
| Future<void> runCommand({ |
| List<String> arguments = const <String>[], |
| List<String> statusTextContains = const <String>[], |
| List<String> errorTextContains = const <String>[], |
| String exitMessageContains = '', |
| int exitCode = 0, |
| }) async { |
| final ProcessResult result = await processManager.run(<String>[ |
| fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter'), |
| '--no-color', |
| ...arguments, |
| ], workingDirectory: projectPath); |
| printOnFailure('Output of flutter ${arguments.join(" ")}'); |
| printOnFailure(result.stdout.toString()); |
| printOnFailure(result.stderr.toString()); |
| expect(result.exitCode, exitCode, reason: 'Expected to exit with non-zero exit code.'); |
| assertContains(result.stdout.toString(), statusTextContains); |
| assertContains(result.stdout.toString(), errorTextContains); |
| expect(result.stderr, contains(exitMessageContains)); |
| } |
| |
| void createDotPackages(String projectPath, [bool nullSafe = false]) { |
| final StringBuffer flutterRootUri = StringBuffer('file://'); |
| final String canonicalizedFlutterRootPath = fileSystem.path.canonicalize(getFlutterRoot()); |
| if (platform.isWindows) { |
| flutterRootUri |
| ..write('/') |
| ..write(canonicalizedFlutterRootPath.replaceAll(r'\', '/')); |
| } else { |
| flutterRootUri.write(canonicalizedFlutterRootPath); |
| } |
| final String dotPackagesSrc = ''' |
| { |
| "configVersion": 2, |
| "packages": [ |
| { |
| "name": "flutter", |
| "rootUri": "$flutterRootUri/packages/flutter", |
| "packageUri": "lib/", |
| "languageVersion": "2.12" |
| }, |
| { |
| "name": "sky_engine", |
| "rootUri": "$flutterRootUri/bin/cache/pkg/sky_engine", |
| "packageUri": "lib/", |
| "languageVersion": "2.12" |
| }, |
| { |
| "name": "flutter_project", |
| "rootUri": "../", |
| "packageUri": "lib/", |
| "languageVersion": "${nullSafe ? "2.12" : "2.7"}" |
| } |
| ] |
| } |
| '''; |
| |
| fileSystem.file(fileSystem.path.join(projectPath, '.dart_tool', 'package_config.json')) |
| ..createSync(recursive: true) |
| ..writeAsStringSync(dotPackagesSrc); |
| } |
| |
| setUp(() { |
| tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_analyze_once_test_1.').absolute; |
| projectPath = fileSystem.path.join(tempDir.path, 'flutter_project'); |
| final String projectWithErrors = fileSystem.path.join(tempDir.path, 'flutter_project_errors'); |
| fileSystem.file(fileSystem.path.join(projectPath, 'pubspec.yaml')) |
| ..createSync(recursive: true) |
| ..writeAsStringSync(pubspecYamlSrc); |
| createDotPackages(projectPath); |
| libMain = fileSystem.file(fileSystem.path.join(projectPath, 'lib', 'main.dart')) |
| ..createSync(recursive: true) |
| ..writeAsStringSync(mainDartSrc); |
| errorFile = fileSystem.file(fileSystem.path.join(projectWithErrors, 'other', 'error.dart')) |
| ..createSync(recursive: true) |
| ..writeAsStringSync(r"""import 'package:flutter/material.dart"""); |
| }); |
| |
| tearDown(() { |
| tryToDelete(tempDir); |
| }); |
| |
| // Analyze in the current directory - no arguments |
| testWithoutContext('working directory', () async { |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub'], |
| statusTextContains: <String>['No issues found!'], |
| ); |
| }); |
| |
| testWithoutContext('passing one file works', () async { |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub', libMain.path], |
| statusTextContains: <String>['No issues found!'] |
| ); |
| }); |
| |
| testWithoutContext('passing one file with errors are detected', () async { |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub', errorFile.path], |
| statusTextContains: <String>[ |
| 'Analyzing error.dart', |
| "error $analyzerSeparator Target of URI doesn't exist", |
| "error $analyzerSeparator Expected to find ';'", |
| 'error $analyzerSeparator Unterminated string literal', |
| ], |
| exitMessageContains: '3 issues found', |
| exitCode: 1 |
| ); |
| }); |
| |
| testWithoutContext('passing more than one file with errors', () async { |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub', libMain.path, errorFile.path], |
| statusTextContains: <String>[ |
| 'Analyzing 2 items', |
| "error $analyzerSeparator Target of URI doesn't exist", |
| "error $analyzerSeparator Expected to find ';'", |
| 'error $analyzerSeparator Unterminated string literal', |
| ], |
| exitMessageContains: '3 issues found', |
| exitCode: 1 |
| ); |
| }); |
| |
| testWithoutContext('passing more than one file success', () async { |
| final File secondFile = fileSystem.file(fileSystem.path.join(projectPath, 'lib', 'second.dart')) |
| ..createSync(recursive: true) |
| ..writeAsStringSync(''); |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub', libMain.path, secondFile.path], |
| statusTextContains: <String>['No issues found!'] |
| ); |
| }); |
| |
| testWithoutContext('mixing directory and files success', () async { |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub', libMain.path, projectPath], |
| statusTextContains: <String>['No issues found!'] |
| ); |
| }); |
| |
| testWithoutContext('file not found', () async { |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub', 'not_found.abc'], |
| exitMessageContains: "not_found.abc', however it does not exist on disk", |
| exitCode: 1 |
| ); |
| }); |
| |
| // Analyze in the current directory - no arguments |
| testWithoutContext('working directory with errors', () async { |
| // Break the code to produce the "Avoid empty else" hint |
| // that is upgraded to a warning in package:flutter/analysis_options_user.yaml |
| // to assert that we are using the default Flutter analysis options. |
| // Also insert a statement that should not trigger a lint here |
| // but will trigger a lint later on when an analysis_options.yaml is added. |
| String source = await libMain.readAsString(); |
| source = source.replaceFirst( |
| 'return MaterialApp(', |
| 'if (debugPrintRebuildDirtyWidgets) {} else ; return MaterialApp(', |
| ); |
| source = source.replaceFirst( |
| 'onPressed: _incrementCounter,', |
| '// onPressed: _incrementCounter,', |
| ); |
| source = source.replaceFirst( |
| '_counter++;', |
| '_counter++; throw "an error message";', |
| ); |
| libMain.writeAsStringSync(source); |
| |
| // Analyze in the current directory - no arguments |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub'], |
| statusTextContains: <String>[ |
| 'Analyzing', |
| 'avoid_empty_else', |
| 'empty_statements', |
| 'unused_element', |
| 'missing_required_param', |
| ], |
| exitMessageContains: '4 issues found.', |
| exitCode: 1, |
| ); |
| }); |
| |
| // Analyze in the current directory - no arguments |
| testWithoutContext('working directory with local options', () async { |
| // Insert an analysis_options.yaml file in the project |
| // which will trigger a lint for broken code that was inserted earlier |
| final File optionsFile = fileSystem.file(fileSystem.path.join(projectPath, 'analysis_options.yaml')); |
| optionsFile.writeAsStringSync(''' |
| include: package:flutter/analysis_options_user.yaml |
| linter: |
| rules: |
| - only_throw_errors |
| '''); |
| String source = libMain.readAsStringSync(); |
| source = source.replaceFirst( |
| 'onPressed: _incrementCounter,', |
| '// onPressed: _incrementCounter,', |
| ); |
| source = source.replaceFirst( |
| '_counter++;', |
| '_counter++; throw "an error message";', |
| ); |
| libMain.writeAsStringSync(source); |
| |
| // Analyze in the current directory - no arguments |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub'], |
| statusTextContains: <String>[ |
| 'Analyzing', |
| 'unused_element', |
| 'only_throw_errors', |
| 'missing_required_param', |
| ], |
| exitMessageContains: '3 issues found.', |
| exitCode: 1, |
| ); |
| }); |
| |
| testWithoutContext('analyze once no duplicate issues', () async { |
| final File foo = fileSystem.file(fileSystem.path.join(projectPath, 'foo.dart')); |
| foo.writeAsStringSync(''' |
| import 'bar.dart'; |
| |
| void foo() => bar(); |
| '''); |
| |
| final File bar = fileSystem.file(fileSystem.path.join(projectPath, 'bar.dart')); |
| bar.writeAsStringSync(''' |
| import 'dart:async'; // unused |
| |
| void bar() { |
| } |
| '''); |
| |
| // Analyze in the current directory - no arguments |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub'], |
| statusTextContains: <String>[ |
| 'Analyzing', |
| ], |
| exitMessageContains: '1 issue found.', |
| exitCode: 1 |
| ); |
| }); |
| |
| testWithoutContext('analyze once returns no issues when source is error-free', () async { |
| const String contents = ''' |
| StringBuffer bar = StringBuffer('baz'); |
| '''; |
| |
| fileSystem.directory(projectPath).childFile('main.dart').writeAsStringSync(contents); |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub'], |
| statusTextContains: <String>['No issues found!'], |
| ); |
| }); |
| |
| testWithoutContext('analyze once returns no issues for todo comments', () async { |
| const String contents = ''' |
| // TODO(foobar): |
| StringBuffer bar = StringBuffer('baz'); |
| '''; |
| |
| fileSystem.directory(projectPath).childFile('main.dart').writeAsStringSync(contents); |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub'], |
| statusTextContains: <String>['No issues found!'], |
| ); |
| }); |
| |
| testWithoutContext('analyze once with default options has info issue finally exit code 1.', () async { |
| const String infoSourceCode = ''' |
| int analyze() {} |
| '''; |
| |
| fileSystem.directory(projectPath).childFile('main.dart').writeAsStringSync(infoSourceCode); |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub'], |
| statusTextContains: <String>[ |
| 'info', |
| 'missing_return', |
| ], |
| exitMessageContains: '1 issue found.', |
| exitCode: 1, |
| ); |
| }); |
| |
| testWithoutContext('analyze once with no-fatal-infos has info issue finally exit code 0.', () async { |
| const String infoSourceCode = ''' |
| int analyze() {} |
| '''; |
| |
| fileSystem.directory(projectPath).childFile('main.dart').writeAsStringSync(infoSourceCode); |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub', '--no-fatal-infos'], |
| statusTextContains: <String>[ |
| 'info', |
| 'missing_return', |
| ], |
| exitMessageContains: '1 issue found.', |
| ); |
| }); |
| |
| testWithoutContext('analyze once only fatal-warnings has info issue finally exit code 0.', () async { |
| const String infoSourceCode = ''' |
| int analyze() {} |
| '''; |
| |
| fileSystem.directory(projectPath).childFile('main.dart').writeAsStringSync(infoSourceCode); |
| await runCommand( |
| arguments: <String>['analyze', '--no-pub', '--fatal-warnings', '--no-fatal-infos'], |
| statusTextContains: <String>[ |
| 'info', |
| 'missing_return', |
| ], |
| exitMessageContains: '1 issue found.', |
| ); |
| }); |
| |
| testWithoutContext('analyze once only fatal-infos has warning issue finally exit code 0.', () async { |
| const String warningSourceCode = ''' |
| int analyze() {} |
| '''; |
| |
| final File optionsFile = fileSystem.file(fileSystem.path.join(projectPath, 'analysis_options.yaml')); |
| optionsFile.writeAsStringSync(''' |
| analyzer: |
| errors: |
| missing_return: warning |
| '''); |
| |
| fileSystem.directory(projectPath).childFile('main.dart').writeAsStringSync(warningSourceCode); |
| await runCommand( |
| arguments: <String>['analyze','--no-pub', '--fatal-infos', '--no-fatal-warnings'], |
| statusTextContains: <String>[ |
| 'warning', |
| 'missing_return', |
| ], |
| exitMessageContains: '1 issue found.', |
| ); |
| }); |
| |
| |
| testWithoutContext('analyze once only fatal-warnings has warning issue finally exit code 1.', () async { |
| const String warningSourceCode = ''' |
| int analyze() {} |
| '''; |
| |
| final File optionsFile = fileSystem.file(fileSystem.path.join(projectPath, 'analysis_options.yaml')); |
| optionsFile.writeAsStringSync(''' |
| analyzer: |
| errors: |
| missing_return: warning |
| '''); |
| |
| fileSystem.directory(projectPath).childFile('main.dart').writeAsStringSync(warningSourceCode); |
| await runCommand( |
| arguments: <String>['analyze','--no-pub', '--no-fatal-infos', '--fatal-warnings'], |
| statusTextContains: <String>[ |
| 'warning', |
| 'missing_return', |
| ], |
| exitMessageContains: '1 issue found.', |
| exitCode: 1, |
| ); |
| }); |
| } |
| |
| void assertContains(String text, List<String> patterns) { |
| for (final String pattern in patterns) { |
| expect(text, contains(pattern)); |
| } |
| } |
| |
| const String mainDartSrc = r''' |
| import 'package:flutter/material.dart'; |
| |
| void main() => runApp(MyApp()); |
| |
| class MyApp extends StatelessWidget { |
| @override |
| Widget build(BuildContext context) { |
| return MaterialApp( |
| title: 'Flutter Demo', |
| theme: ThemeData( |
| primarySwatch: Colors.blue, |
| ), |
| home: MyHomePage(title: 'Flutter Demo Home Page'), |
| ); |
| } |
| } |
| |
| class MyHomePage extends StatefulWidget { |
| MyHomePage({Key key, this.title}) : super(key: key); |
| |
| final String title; |
| |
| @override |
| _MyHomePageState createState() => _MyHomePageState(); |
| } |
| |
| class _MyHomePageState extends State<MyHomePage> { |
| int _counter = 0; |
| |
| void _incrementCounter() { |
| setState(() { |
| _counter++; |
| }); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return Scaffold( |
| appBar: AppBar( |
| title: Text(widget.title), |
| ), |
| body: Center( |
| child: Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: <Widget>[ |
| Text( |
| 'You have pushed the button this many times:', |
| ), |
| Text( |
| '$_counter', |
| style: Theme.of(context).textTheme.headlineMedium, |
| ), |
| ], |
| ), |
| ), |
| floatingActionButton: FloatingActionButton( |
| onPressed: _incrementCounter, |
| tooltip: 'Increment', |
| child: Icon(Icons.add), |
| ), |
| ); |
| } |
| } |
| '''; |
| |
| const String pubspecYamlSrc = r''' |
| name: flutter_project |
| environment: |
| sdk: ">=2.1.0 <4.0.0" |
| |
| dependencies: |
| flutter: |
| sdk: flutter |
| '''; |