@@ -8,6 +8,7 @@ import 'package:analyzer/file_system/physical_file_system.dart';
88import 'package:glob/glob.dart' ;
99import 'package:glob/list_local_fs.dart' ;
1010import 'package:path/path.dart' as path;
11+ import 'package:source_gen/source_gen.dart' show InvalidGenerationSourceError;
1112
1213import '../analyzer/entity_analyzer.dart' ;
1314import '../analyzer/enum_analyzer.dart' ;
@@ -75,11 +76,44 @@ class AnalyzeResult {
7576 unions.isNotEmpty;
7677}
7778
79+ /// An annotation an analyzer rejected, with the file it was found in.
80+ class InvalidAnnotation {
81+ /// Absolute path of the Dart file declaring the annotation.
82+ final String filePath;
83+
84+ /// The analyzer's error.
85+ final InvalidGenerationSourceError error;
86+
87+ const InvalidAnnotation ({required this .filePath, required this .error});
88+
89+ @override
90+ String toString () => '$filePath : $error ' ;
91+ }
92+
93+ /// Thrown by [analyzeSourceFiles] when one or more annotations are invalid.
94+ ///
95+ /// Every file is still scanned first, so all invalid specs are reported in one
96+ /// run rather than one per invocation.
97+ class InvalidAnnotationsException implements Exception {
98+ /// The rejected annotations, in scan order.
99+ final List <InvalidAnnotation > annotations;
100+
101+ const InvalidAnnotationsException (this .annotations);
102+
103+ @override
104+ String toString () =>
105+ 'Found ${annotations .length } invalid annotation(s):\n '
106+ '${annotations .map ((a ) => ' $a ' ).join ('\n ' )}' ;
107+ }
108+
78109/// Scans and analyzes Dart source files for @IntentSpec, @EntitySpec,
79110/// @EnumSpec, and @AppShortcutsProvider annotations.
80111///
81112/// [inputDir] is the directory to scan (absolute or relative to cwd).
82113/// Returns an [AnalyzeResult] with all found annotations.
114+ ///
115+ /// Throws [InvalidAnnotationsException] if any analyzer rejects an annotation.
116+ /// A file that fails to resolve is skipped with a warning instead.
83117Future <AnalyzeResult > analyzeSourceFiles (String inputDir) async {
84118 final currentDir = Directory .current.path;
85119 final absoluteInputDir = path.isAbsolute (inputDir)
@@ -143,80 +177,96 @@ Future<AnalyzeResult> analyzeSourceFiles(String inputDir) async {
143177 final unionAnalyzer = const UnionAnalyzer ();
144178 final allShortcuts = < AppShortcutInfo > [];
145179
180+ final invalidAnnotations = < InvalidAnnotation > [];
181+
146182 for (final filePath in dartFiles) {
183+ // Only resolution is recoverable: a file the analyzer cannot resolve is
184+ // skipped with a warning, as before.
185+ final ResolvedLibraryResult resolved;
147186 try {
148187 final context = collection.contextFor (filePath);
149188 final result = await context.currentSession.getResolvedLibrary (filePath);
189+ if (result is ! ResolvedLibraryResult ) continue ;
190+ resolved = result;
191+ } catch (e) {
192+ stderr.writeln (' Warning: Could not analyze $filePath : $e ' );
193+ continue ;
194+ }
150195
151- if (result is ResolvedLibraryResult ) {
152- final library = result.element;
153-
154- for (final element in library.classes) {
155- // Check for @IntentSpec
156- if (intentAnalyzer.hasIntentSpecAnnotation (element)) {
157- final info = intentAnalyzer.analyze (element);
158- if (info != null && ! intentsMap.containsKey (info.identifier)) {
159- intentsMap[info.identifier] = info;
160- stdout.writeln (' Found intent: ${info .className }' );
161- }
196+ // An analyzer rejecting an annotation is not. Swallowing it here used to
197+ // drop the spec from the output while the CLI still exited 0.
198+ try {
199+ final library = resolved.element;
200+
201+ for (final element in library.classes) {
202+ // Check for @IntentSpec
203+ if (intentAnalyzer.hasIntentSpecAnnotation (element)) {
204+ final info = intentAnalyzer.analyze (element);
205+ if (info != null && ! intentsMap.containsKey (info.identifier)) {
206+ intentsMap[info.identifier] = info;
207+ stdout.writeln (' Found intent: ${info .className }' );
162208 }
209+ }
163210
164- // Check for @EntitySpec
165- if (entityAnalyzer.hasEntitySpecAnnotation (element)) {
166- final info = entityAnalyzer.analyze (element);
167- if (info != null && ! entitiesMap.containsKey (info.identifier)) {
168- entitiesMap[info.identifier] = info;
169- stdout.writeln (' Found entity: ${info .className }' );
170- }
211+ // Check for @EntitySpec
212+ if (entityAnalyzer.hasEntitySpecAnnotation (element)) {
213+ final info = entityAnalyzer.analyze (element);
214+ if (info != null && ! entitiesMap.containsKey (info.identifier)) {
215+ entitiesMap[info.identifier] = info;
216+ stdout.writeln (' Found entity: ${info .className }' );
171217 }
218+ }
172219
173- // Check for @WidgetConfigurationSpec
174- if (widgetConfigurationAnalyzer.hasWidgetConfigurationSpecAnnotation (
175- element,
176- )) {
177- final info = widgetConfigurationAnalyzer.analyze (element);
178- if (info != null &&
179- ! widgetConfigurationsMap.containsKey (info.identifier)) {
180- widgetConfigurationsMap[info.identifier] = info;
181- stdout.writeln (' Found widget configuration: ${info .className }' );
182- }
220+ // Check for @WidgetConfigurationSpec
221+ if (widgetConfigurationAnalyzer.hasWidgetConfigurationSpecAnnotation (
222+ element,
223+ )) {
224+ final info = widgetConfigurationAnalyzer.analyze (element);
225+ if (info != null &&
226+ ! widgetConfigurationsMap.containsKey (info.identifier)) {
227+ widgetConfigurationsMap[info.identifier] = info;
228+ stdout.writeln (' Found widget configuration: ${info .className }' );
183229 }
230+ }
184231
185- // Check for @UnionValueSpec
186- if (unionAnalyzer.hasUnionValueSpecAnnotation (element)) {
187- final info = unionAnalyzer.analyze (element);
188- if (info != null && ! unionsMap.containsKey (info.identifier)) {
189- unionsMap[info.identifier] = info;
190- stdout.writeln (' Found union: ${info .className }' );
191- }
232+ // Check for @UnionValueSpec
233+ if (unionAnalyzer.hasUnionValueSpecAnnotation (element)) {
234+ final info = unionAnalyzer.analyze (element);
235+ if (info != null && ! unionsMap.containsKey (info.identifier)) {
236+ unionsMap[info.identifier] = info;
237+ stdout.writeln (' Found union: ${info .className }' );
192238 }
239+ }
193240
194- // Check for @AppShortcutsProvider
195- if (shortcutAnalyzer.hasAppShortcutsProviderAnnotation (element)) {
196- final shortcuts = shortcutAnalyzer.analyze (element);
197- for (final shortcut in shortcuts) {
198- allShortcuts.add (shortcut);
199- stdout.writeln (' Found shortcut: ${shortcut .shortTitle }' );
200- }
241+ // Check for @AppShortcutsProvider
242+ if (shortcutAnalyzer.hasAppShortcutsProviderAnnotation (element)) {
243+ final shortcuts = shortcutAnalyzer.analyze (element);
244+ for (final shortcut in shortcuts) {
245+ allShortcuts.add (shortcut);
246+ stdout.writeln (' Found shortcut: ${shortcut .shortTitle }' );
201247 }
202248 }
249+ }
203250
204- // Check for @EnumSpec on enums
205- for (final element in library.enums) {
206- if (enumAnalyzer.hasEnumSpecAnnotation (element)) {
207- final info = enumAnalyzer.analyze (element);
208- if (info != null && ! enumsMap.containsKey (info.identifier)) {
209- enumsMap[info.identifier] = info;
210- stdout.writeln (' Found enum: ${info .className }' );
211- }
251+ // Check for @EnumSpec on enums
252+ for (final element in library.enums) {
253+ if (enumAnalyzer.hasEnumSpecAnnotation (element)) {
254+ final info = enumAnalyzer.analyze (element);
255+ if (info != null && ! enumsMap.containsKey (info.identifier)) {
256+ enumsMap[info.identifier] = info;
257+ stdout.writeln (' Found enum: ${info .className }' );
212258 }
213259 }
214260 }
215- } catch (e) {
216- stderr. writeln ( ' Warning: Could not analyze $ filePath : $ e ' );
261+ } on InvalidGenerationSourceError catch (e) {
262+ invalidAnnotations. add ( InvalidAnnotation (filePath : filePath, error : e) );
217263 }
218264 }
219265
266+ if (invalidAnnotations.isNotEmpty) {
267+ throw InvalidAnnotationsException (invalidAnnotations);
268+ }
269+
220270 final intents = intentsMap.values.toList ();
221271 final entities = entitiesMap.values.toList ();
222272 final enums = enumsMap.values.toList ();
@@ -255,3 +305,14 @@ Future<AnalyzeResult> analyzeSourceFiles(String inputDir) async {
255305 unions: unions,
256306 );
257307}
308+
309+ /// [analyzeSourceFiles] for the CLIs: an invalid annotation is printed to
310+ /// stderr and the process exits 1, instead of generating incomplete output.
311+ Future <AnalyzeResult > analyzeSourceFilesOrExit (String inputDir) async {
312+ try {
313+ return await analyzeSourceFiles (inputDir);
314+ } on InvalidAnnotationsException catch (e) {
315+ stderr.writeln ('Error: $e ' );
316+ exit (1 );
317+ }
318+ }
0 commit comments