Skip to content

Commit c6123aa

Browse files
touyouclaude
andauthored
fix: 無効なアノテーションで CLI を失敗させる (#138)
analyzeSourceFiles は解析全体を広い catch で包んでいたため、各 analyzer が 投げる InvalidGenerationSourceError も Warning として握りつぶされ、 generate_swift / generate_widget_swift / generate_kotlin が不完全な出力のまま exit 0 していた。 - 解決 (getResolvedLibrary) の失敗だけを従来どおり Warning でスキップする - InvalidGenerationSourceError は全ファイル走査後に InvalidAnnotationsException としてまとめて投げる - CLI は analyzeSourceFilesOrExit 経由で、ファイルパス付きのエラーを出して exit 1 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 2c5f5af commit c6123aa

6 files changed

Lines changed: 291 additions & 55 deletions

File tree

packages/app_intents_codegen/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
## [Unreleased]
2+
3+
> **Behavior change for CLI users.** `generate_swift`, `generate_widget_swift`
4+
> and `generate_kotlin` now **fail with exit code 1** when an annotation is
5+
> invalid. Previously the analyzer's error was printed as a `Warning: Could not
6+
> analyze …` line, the spec was left out of the output, and the command still
7+
> exited 0 — so a project whose only specs were invalid got empty output with
8+
> no failure. If a build that used to pass now stops here, the printed error
9+
> names the file and the problem; that spec was never being generated.
10+
11+
- `analyzeSourceFiles` throws `InvalidAnnotationsException` instead of swallowing the analyzers' `InvalidGenerationSourceError`. All files are scanned before it throws, so every invalid spec is reported in one run. A file that fails to **resolve** is still skipped with a warning, as before.
12+
113
## 0.16.0
214

315
> **Heads-up for `@EntitySpec(valueQuery: true)` users.** The `IntentValueQuery`

packages/app_intents_codegen/bin/generate_kotlin.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ Future<void> generateKotlin({
100100
required String outputFile,
101101
required String packageName,
102102
}) async {
103-
final analyzeResult = await analyzeSourceFiles(inputDir);
103+
final analyzeResult = await analyzeSourceFilesOrExit(inputDir);
104104

105105
if (analyzeResult.isEmpty) {
106106
stdout.writeln(

packages/app_intents_codegen/bin/generate_swift.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ Future<void> generateSwift({
208208
String? appIntentsPackage,
209209
List<String> includedPackages = const [],
210210
}) async {
211-
final analyzeResult = await analyzeSourceFiles(inputDir);
211+
final analyzeResult = await analyzeSourceFilesOrExit(inputDir);
212212

213213
// Gate on the annotations this generator actually consumes. A project that
214214
// declares only @WidgetConfigurationSpec has nothing to emit here — its

packages/app_intents_codegen/bin/generate_widget_swift.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ Future<void> generateWidgetSwift({
180180
List<String> includedPackages = const [],
181181
bool publicAccess = false,
182182
}) async {
183-
final analyzeResult = await analyzeSourceFiles(inputDir);
183+
final analyzeResult = await analyzeSourceFilesOrExit(inputDir);
184184

185185
if (analyzeResult.widgetConfigurations.isEmpty) {
186186
stdout.writeln('No @WidgetConfigurationSpec annotations found.');

packages/app_intents_codegen/lib/src/cli/analyze_sources.dart

Lines changed: 113 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:analyzer/file_system/physical_file_system.dart';
88
import 'package:glob/glob.dart';
99
import 'package:glob/list_local_fs.dart';
1010
import 'package:path/path.dart' as path;
11+
import 'package:source_gen/source_gen.dart' show InvalidGenerationSourceError;
1112

1213
import '../analyzer/entity_analyzer.dart';
1314
import '../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.
83117
Future<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

Comments
 (0)