11import 'dart:io' ;
22
33import 'package:args/args.dart' ;
4- import 'package:path/path.dart' as p;
54
6- import 'package:flutterguard_cli/src/config_loader.dart' ;
7- import 'package:flutterguard_cli/src/file_collector.dart' ;
85import 'package:flutterguard_cli/src/report_generator.dart' ;
9- import 'package:flutterguard_cli/src/rules/circular_dependency.dart' ;
10- import 'package:flutterguard_cli/src/rules/large_units.dart' ;
11- import 'package:flutterguard_cli/src/rules/layer_violation.dart' ;
12- import 'package:flutterguard_cli/src/rules/lifecycle_resource.dart' ;
13- import 'package:flutterguard_cli/src/rules/missing_const_constructor.dart' ;
14- import 'package:flutterguard_cli/src/rules/module_violation.dart' ;
15- import 'package:flutterguard_cli/src/static_issue.dart' ;
6+ import 'package:flutterguard_cli/src/scanner.dart' ;
167
178const _version = '0.1.0' ;
189
1910void main (List <String > args) {
20- if (args.contains ('--version' ) || args.contains ('-V' )) {
21- stdout.writeln ('flutterguard $_version ' );
22- exit (0 );
23- }
24-
2511 final scanParser = ArgParser ()
2612 ..addOption ('path' ,
2713 abbr: 'p' , defaultsTo: '.' , help: 'Project path to scan' )
@@ -46,23 +32,39 @@ void main(List<String> args) {
4632 defaultsTo: 'none' ,
4733 allowed: ['none' , 'high' , 'medium' , 'low' ],
4834 help: 'Fail threshold for CI gate' )
49- ..addOption ('min-score' , help: 'Minimum score threshold (0-100)' );
35+ ..addOption ('min-score' , help: 'Minimum score threshold (0-100)' )
36+ ..addFlag ('help' , abbr: 'h' , help: 'Show scan usage' , negatable: false );
5037
5138 final parser = ArgParser ()
5239 ..addCommand ('scan' , scanParser)
53- ..addFlag ('help' , abbr: 'h' , help: 'Show usage' , negatable: false );
40+ ..addFlag ('help' , abbr: 'h' , help: 'Show usage' , negatable: false )
41+ ..addFlag ('version' , abbr: 'V' , help: 'Show version' , negatable: false );
5442
5543 try {
5644 final results = parser.parse (args);
5745
46+ if (results['version' ] == true ) {
47+ stdout.writeln ('flutterguard $_version ' );
48+ exit (0 );
49+ }
50+
5851 if (results['help' ] == true || args.isEmpty || args.first == 'help' ) {
5952 _printUsage (parser);
6053 exit (0 );
6154 }
6255
6356 final command = results.command;
64- if (command? .name == 'scan' ) {
65- _handleScan (command! );
57+ if (command == null ) {
58+ _printUsage (parser);
59+ exit (0 );
60+ }
61+
62+ if (command.name == 'scan' ) {
63+ if (command['help' ] == true ) {
64+ _printScanUsage (scanParser);
65+ exit (0 );
66+ }
67+ _handleScan (command);
6668 } else {
6769 _printUsage (parser);
6870 exit (0 );
@@ -76,106 +78,63 @@ void main(List<String> args) {
7678}
7779
7880void _handleScan (ArgResults args) {
79- final projectPath = p.normalize (p.absolute (args['path' ] as String ));
80- final configPath = args['config' ] as String ;
81- final outputDir = args['output' ] as String ;
8281 final format = args['format' ] as String ;
8382 final verbose = args['verbose' ] as bool ;
8483 final failOn = args['fail-on' ] as String ;
8584 final minScoreStr = args['min-score' ] as String ? ;
85+ final minScore = _parseMinScore (minScoreStr);
8686
87- if (! Directory (projectPath).existsSync ()) {
88- stderr.writeln ('Error: Project path "$projectPath " does not exist.' );
87+ late final ScanResult result;
88+ try {
89+ result = FlutterGuardScanner .scan (
90+ projectPath: args['path' ] as String ,
91+ configPath: args['config' ] as String ,
92+ outputDir: args['output' ] as String ,
93+ writeJson: format == 'json' ,
94+ );
95+ } on ScanException catch (e) {
96+ stderr.writeln ('Error: ${e .message }' );
97+ exit (2 );
98+ } on FormatException catch (e) {
99+ stderr.writeln ('Error: ${e .message }' );
89100 exit (2 );
90101 }
91102
92- final config = ScanConfig .fromFile (p.join (projectPath, configPath));
93-
94- final files = FileCollector .collect (projectPath, config);
95-
96- if (files.isEmpty) {
103+ if (result.files.isEmpty) {
97104 stderr.writeln ('No Dart files found. Check your include/exclude patterns.' );
98105 exit (0 );
99106 }
100107
101- final allIssues = < StaticIssue > [];
102-
103- allIssues.addAll (LargeUnitsRule (
104- largeFileConfig: config.rules.largeFile,
105- largeClassConfig: config.rules.largeClass,
106- largeBuildMethodConfig: config.rules.largeBuildMethod,
107- ).analyze (files));
108- allIssues.addAll (
109- LifecycleResourceRule (config.rules.lifecycleResource).analyze (files));
110- if (config.architecture.layerViolationEnabled) {
111- allIssues.addAll (LayerViolationRule (
112- config.architecture.layers,
113- projectPath: projectPath,
114- ).analyze (files));
115- }
116- if (config.architecture.moduleViolationEnabled) {
117- allIssues.addAll (ModuleViolationRule (
118- config.architecture.modules,
119- projectPath: projectPath,
120- ).analyze (files));
121- }
122- allIssues.addAll (MissingConstConstructorRule (
123- config.rules.missingConstConstructor,
124- ).analyze (files));
125- allIssues.addAll (CircularDependencyRule (
126- enabled: config.architecture.detectCycles,
127- projectPath: projectPath,
128- ).analyze (files));
129-
130- allIssues.sort ((a, b) {
131- final levelOrder = {
132- RiskLevel .high: 0 ,
133- RiskLevel .medium: 1 ,
134- RiskLevel .low: 2 ,
135- };
136- final cmp = levelOrder[a.level]! .compareTo (levelOrder[b.level]! );
137- if (cmp != 0 ) return cmp;
138- return a.file.compareTo (b.file);
139- });
140-
141- final reportDir =
142- p.isAbsolute (outputDir) ? outputDir : p.join (projectPath, outputDir);
143- Directory (reportDir).createSync (recursive: true );
144-
145- if (format == 'json' ) {
146- final json = ReportGenerator .generateJson (
147- projectPath: projectPath,
148- issues: allIssues,
149- );
150- File (p.join (reportDir, 'report.json' )).writeAsStringSync (json);
151- }
152-
153108 final stdoutOutput = ReportGenerator .generateStdout (
154- projectPath: projectPath,
155- issues: allIssues,
109+ projectPath: result.projectPath,
110+ issues: result.issues,
111+ scannedFileCount: result.files.length,
156112 verbose: verbose,
157113 );
158114 stdout.writeln (stdoutOutput);
159115
160116 if (failOn != 'none' ) {
161- if (ReportGenerator .shouldFail (allIssues , failOn)) {
117+ if (ReportGenerator .shouldFail (result.issues , failOn)) {
162118 stderr
163119 .writeln ('CI gate failed: Issues found at or above "$failOn " level.' );
164120 exit (1 );
165121 }
166122 }
167123
168- if (minScoreStr != null ) {
169- final minScore = int .tryParse (minScoreStr);
170- if (minScore != null ) {
171- final score = ReportGenerator .calculateScore (allIssues);
172- if (score < minScore) {
173- stderr.writeln (
174- 'CI gate failed: Score $score is below minimum $minScore .' );
175- exit (1 );
176- }
177- }
124+ if (minScore != null && result.score < minScore) {
125+ stderr.writeln (
126+ 'CI gate failed: Score ${result .score } is below minimum $minScore .' );
127+ exit (1 );
128+ }
129+ }
130+
131+ int ? _parseMinScore (String ? value) {
132+ if (value == null ) return null ;
133+ final score = int .tryParse (value);
134+ if (score == null || score < 0 || score > 100 ) {
135+ throw const FormatException ('Expected --min-score to be an integer 0-100.' );
178136 }
137+ return score;
179138}
180139
181140void _printUsage (ArgParser parser) {
@@ -206,3 +165,11 @@ void _printUsage(ArgParser parser) {
206165 stdout.writeln (' flutterguard scan -p ./my_flutter_app' );
207166 stdout.writeln (' flutterguard scan -p . --format json --fail-on high' );
208167}
168+
169+ void _printScanUsage (ArgParser scanParser) {
170+ stdout.writeln ('FlutterGuard — scan command' );
171+ stdout.writeln ();
172+ stdout.writeln ('Usage: flutterguard scan [options]' );
173+ stdout.writeln ();
174+ stdout.writeln (scanParser.usage);
175+ }
0 commit comments