1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:meta/meta.dart' ; |
6 | import 'package:yaml/yaml.dart' ; |
7 | |
8 | import '../base/common.dart'; |
9 | import '../base/file_system.dart'; |
10 | import '../base/logger.dart'; |
11 | import '../globals.dart' as globals; |
12 | import '../runner/flutter_command.dart'; |
13 | import 'gen_l10n_types.dart'; |
14 | import 'language_subtag_registry.dart'; |
15 | |
16 | typedef HeaderGenerator = String Function(String regenerateInstructions); |
17 | typedef ConstructorGenerator = String Function(LocaleInfo locale); |
18 | |
19 | int sortFilesByPath(File a, File b) { |
20 | return a.path.compareTo(b.path); |
21 | } |
22 | |
23 | /// Simple data class to hold parsed locale. Does not promise validity of any data. |
24 | @immutable |
25 | class LocaleInfo implements Comparable<LocaleInfo> { |
26 | const LocaleInfo({ |
27 | required this.languageCode, |
28 | required this.scriptCode, |
29 | required this.countryCode, |
30 | required this.length, |
31 | required this.originalString, |
32 | }); |
33 | |
34 | /// Simple parser. Expects the locale string to be in the form of 'language_script_COUNTRY' |
35 | /// where the language is 2 characters, script is 4 characters with the first uppercase, |
36 | /// and country is 2-3 characters and all uppercase. |
37 | /// |
38 | /// 'language_COUNTRY' or 'language_script' are also valid. Missing fields will be null. |
39 | /// |
40 | /// When `deriveScriptCode` is true, if [scriptCode] was unspecified, it will |
41 | /// be derived from the [languageCode] and [countryCode] if possible. |
42 | factory LocaleInfo.fromString(String locale, {bool deriveScriptCode = false}) { |
43 | final List<String> codes = locale.split('_' ); // [language, script, country] |
44 | assert(codes.isNotEmpty && codes.length < 4); |
45 | final String languageCode = codes[0]; |
46 | String? scriptCode; |
47 | String? countryCode; |
48 | int length = codes.length; |
49 | var originalString = locale; |
50 | if (codes.length == 2) { |
51 | scriptCode = codes[1].length >= 4 ? codes[1] : null; |
52 | countryCode = codes[1].length < 4 ? codes[1] : null; |
53 | } else if (codes.length == 3) { |
54 | scriptCode = codes[1].length > codes[2].length ? codes[1] : codes[2]; |
55 | countryCode = codes[1].length < codes[2].length ? codes[1] : codes[2]; |
56 | } |
57 | assert(codes[0].isNotEmpty); |
58 | assert(countryCode == null || countryCode.isNotEmpty); |
59 | assert(scriptCode == null || scriptCode.isNotEmpty); |
60 | |
61 | /// Adds scriptCodes to locales where we are able to assume it to provide |
62 | /// finer granularity when resolving locales. |
63 | /// |
64 | /// The basis of the assumptions here are based off of known usage of scripts |
65 | /// across various countries. For example, we know Taiwan uses traditional (Hant) |
66 | /// script, so it is safe to apply (Hant) to Taiwanese languages. |
67 | if (deriveScriptCode && scriptCode == null) { |
68 | scriptCode = switch ((languageCode, countryCode)) { |
69 | ('zh' , 'CN' || 'SG' || null) => 'Hans' , |
70 | ('zh' , 'TW' || 'HK' || 'MO' ) => 'Hant' , |
71 | ('sr' , null) => 'Cyrl' , |
72 | _ => null, |
73 | }; |
74 | // Increment length if we were able to assume a scriptCode. |
75 | if (scriptCode != null) { |
76 | length += 1; |
77 | } |
78 | // Update the base string to reflect assumed scriptCodes. |
79 | originalString = languageCode; |
80 | if (scriptCode != null) { |
81 | originalString += '_ $scriptCode' ; |
82 | } |
83 | if (countryCode != null) { |
84 | originalString += '_ $countryCode' ; |
85 | } |
86 | } |
87 | |
88 | return LocaleInfo( |
89 | languageCode: languageCode, |
90 | scriptCode: scriptCode, |
91 | countryCode: countryCode, |
92 | length: length, |
93 | originalString: originalString, |
94 | ); |
95 | } |
96 | |
97 | final String languageCode; |
98 | final String? scriptCode; |
99 | final String? countryCode; |
100 | final int length; // The number of fields. Ranges from 1-3. |
101 | final String originalString; // Original un-parsed locale string. |
102 | |
103 | String camelCase() { |
104 | return originalString |
105 | .split('_' ) |
106 | .map<String>( |
107 | (String part) => part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase(), |
108 | ) |
109 | .join(); |
110 | } |
111 | |
112 | @override |
113 | bool operator ==(Object other) { |
114 | return other is LocaleInfo && other.originalString == originalString; |
115 | } |
116 | |
117 | @override |
118 | int get hashCode => originalString.hashCode; |
119 | |
120 | @override |
121 | String toString() { |
122 | return originalString; |
123 | } |
124 | |
125 | @override |
126 | int compareTo(LocaleInfo other) { |
127 | return originalString.compareTo(other.originalString); |
128 | } |
129 | } |
130 | |
131 | // See also //master/tools/gen_locale.dart in the engine repo. |
132 | Map<String, List<String>> _parseSection(String section) { |
133 | final result = <String, List<String>>{}; |
134 | late List<String> lastHeading; |
135 | for (final String line in section.split('\n' )) { |
136 | if (line == '' ) { |
137 | continue; |
138 | } |
139 | if (line.startsWith(' ' )) { |
140 | lastHeading[lastHeading.length - 1] = ' ${lastHeading.last}${line.substring(1)}' ; |
141 | continue; |
142 | } |
143 | final int colon = line.indexOf(':' ); |
144 | if (colon <= 0) { |
145 | throw Exception('not sure how to deal with " $line"' ); |
146 | } |
147 | final String name = line.substring(0, colon); |
148 | final String value = line.substring(colon + 2); |
149 | lastHeading = result.putIfAbsent(name, () => <String>[]); |
150 | result[name]!.add(value); |
151 | } |
152 | return result; |
153 | } |
154 | |
155 | final _languages = <String, String>{}; |
156 | final _regions = <String, String>{}; |
157 | final _scripts = <String, String>{}; |
158 | const kProvincePrefix = ', Province of ' ; |
159 | const kParentheticalPrefix = ' (' ; |
160 | |
161 | /// Prepares the data for the [describeLocale] method below. |
162 | /// |
163 | /// The data is obtained from the official IANA registry. |
164 | void precacheLanguageAndRegionTags() { |
165 | final List<Map<String, List<String>>> sections = languageSubtagRegistry |
166 | .split('%%' ) |
167 | .skip(1) |
168 | .map<Map<String, List<String>>>(_parseSection) |
169 | .toList(); |
170 | for (final section in sections) { |
171 | assert(section.containsKey('Type' ), section.toString()); |
172 | final String type = section['Type' ]!.single; |
173 | if (type == 'language' || type == 'region' || type == 'script' ) { |
174 | assert( |
175 | section.containsKey('Subtag' ) && section.containsKey('Description' ), |
176 | section.toString(), |
177 | ); |
178 | final String subtag = section['Subtag' ]!.single; |
179 | String description = section['Description' ]!.join(' ' ); |
180 | if (description.startsWith('United ' )) { |
181 | description = 'the $description' ; |
182 | } |
183 | if (description.contains(kParentheticalPrefix)) { |
184 | description = description.substring(0, description.indexOf(kParentheticalPrefix)); |
185 | } |
186 | if (description.contains(kProvincePrefix)) { |
187 | description = description.substring(0, description.indexOf(kProvincePrefix)); |
188 | } |
189 | if (description.endsWith(' Republic' )) { |
190 | description = 'the $description' ; |
191 | } |
192 | switch (type) { |
193 | case 'language' : |
194 | _languages[subtag] = description; |
195 | case 'region' : |
196 | _regions[subtag] = description; |
197 | case 'script' : |
198 | _scripts[subtag] = description; |
199 | } |
200 | } |
201 | } |
202 | } |
203 | |
204 | String describeLocale(String tag) { |
205 | final List<String> subtags = tag.split('_' ); |
206 | assert(subtags.isNotEmpty); |
207 | final String languageCode = subtags[0]; |
208 | if (!_languages.containsKey(languageCode)) { |
209 | throw L10nException( |
210 | '" $languageCode" is not a supported language code.\n' |
211 | 'See https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry ' |
212 | 'for the supported list.' , |
213 | ); |
214 | } |
215 | final String language = _languages[languageCode]!; |
216 | var output = language; |
217 | String? region; |
218 | String? script; |
219 | if (subtags.length == 2) { |
220 | region = _regions[subtags[1]]; |
221 | script = _scripts[subtags[1]]; |
222 | assert(region != null || script != null); |
223 | } else if (subtags.length >= 3) { |
224 | region = _regions[subtags[2]]; |
225 | script = _scripts[subtags[1]]; |
226 | assert(region != null && script != null); |
227 | } |
228 | if (region != null) { |
229 | output += ', as used in $region' ; |
230 | } |
231 | if (script != null) { |
232 | output += ', using the $script script' ; |
233 | } |
234 | return output; |
235 | } |
236 | |
237 | /// Return the input string as a Dart-parsable string. |
238 | /// |
239 | /// ```none |
240 | /// foo => 'foo' |
241 | /// foo "bar" => 'foo "bar"' |
242 | /// foo 'bar' => "foo 'bar'" |
243 | /// foo 'bar' "baz" => '''foo 'bar' "baz"''' |
244 | /// ``` |
245 | /// |
246 | /// This function is used by tools that take in a JSON-formatted file to |
247 | /// generate Dart code. For this reason, characters with special meaning |
248 | /// in JSON files are escaped. For example, the backspace character (\b) |
249 | /// has to be properly escaped by this function so that the generated |
250 | /// Dart code correctly represents this character: |
251 | /// ```none |
252 | /// foo\bar => 'foo\\bar' |
253 | /// foo\nbar => 'foo\\nbar' |
254 | /// foo\\nbar => 'foo\\\\nbar' |
255 | /// foo\\bar => 'foo\\\\bar' |
256 | /// foo\ bar => 'foo\\ bar' |
257 | /// foo$bar = 'foo\$bar' |
258 | /// ``` |
259 | String generateString(String value) { |
260 | const backslash = '__BACKSLASH__' ; |
261 | assert( |
262 | !value.contains(backslash), |
263 | 'Input string cannot contain the sequence: ' |
264 | '"__BACKSLASH__", as it is used as part of ' |
265 | 'backslash character processing.' , |
266 | ); |
267 | |
268 | value = value |
269 | // Replace backslashes with a placeholder for now to properly parse |
270 | // other special characters. |
271 | .replaceAll(r'\' , backslash) |
272 | .replaceAll(r'$' , r'\$' ) |
273 | .replaceAll("'" , r"\'" ) |
274 | .replaceAll('"' , r'\"' ) |
275 | .replaceAll('\n' , r'\n' ) |
276 | .replaceAll('\f' , r'\f' ) |
277 | .replaceAll('\t' , r'\t' ) |
278 | .replaceAll('\r' , r'\r' ) |
279 | .replaceAll('\b' , r'\b' ) |
280 | // Reintroduce escaped backslashes into generated Dart string. |
281 | .replaceAll(backslash, r'\\' ); |
282 | |
283 | return value; |
284 | } |
285 | |
286 | /// Given a list of normal strings or interpolated variables, concatenate them |
287 | /// into a single dart string to be returned. An example of a normal string |
288 | /// would be "'Hello world!'" and an example of a interpolated variable would be |
289 | /// "'$placeholder'". |
290 | /// |
291 | /// Each of the strings in [expressions] should be a raw string, which, if it |
292 | /// were to be added to a dart file, would be a properly formatted dart string |
293 | /// with escapes and/or interpolation. The purpose of this function is to |
294 | /// concatenate these dart strings into a single dart string which can be |
295 | /// returned in the generated localization files. |
296 | /// |
297 | /// The following rules describe the kinds of string expressions that can be |
298 | /// handled: |
299 | /// 1. If [expressions] is empty, return the empty string "''". |
300 | /// 2. If [expressions] has only one [String] which is an interpolated variable, |
301 | /// it is converted to the variable itself e.g. ["'$expr'"] -> "expr". |
302 | /// 3. If one string in [expressions] is an interpolation and the next begins |
303 | /// with an alphanumeric character, then the former interpolation should be |
304 | /// wrapped in braces e.g. ["'$expr1'", "'another'"] -> "'${expr1}another'". |
305 | String generateReturnExpr(List<String> expressions, {bool isSingleStringVar = false}) { |
306 | if (expressions.isEmpty) { |
307 | return "''" ; |
308 | } else if (isSingleStringVar) { |
309 | // If our expression is "$varName" where varName is a String, this is equivalent to just varName. |
310 | return expressions[0].substring(1); |
311 | } else { |
312 | final String string = expressions.reversed.fold<String>('' , (String string, String expression) { |
313 | if (expression[0] != r'$' ) { |
314 | return expression + string; |
315 | } |
316 | final alphanumeric = RegExp(r'^([0-9a-zA-Z]|_)+$' ); |
317 | if (alphanumeric.hasMatch(expression.substring(1)) && |
318 | !(string.isNotEmpty && alphanumeric.hasMatch(string[0]))) { |
319 | return ' $expression$string' ; |
320 | } else {
|
321 | return '\${ ${expression.substring(1)}} $string' ;
|
322 | }
|
323 | });
|
324 | return "' $string'" ;
|
325 | }
|
326 | }
|
327 |
|
328 | /// Typed configuration from the localizations config file.
|
329 | class LocalizationOptions {
|
330 | LocalizationOptions({
|
331 | required this.arbDir,
|
332 | this.outputDir,
|
333 | String? templateArbFile,
|
334 | String? outputLocalizationFile,
|
335 | this.untranslatedMessagesFile,
|
336 | String? outputClass,
|
337 | this.preferredSupportedLocales,
|
338 | this.header,
|
339 | this.headerFile,
|
340 | bool? useDeferredLoading,
|
341 | this.genInputsAndOutputsList,
|
342 | this.projectDir,
|
343 | bool? requiredResourceAttributes,
|
344 | bool? nullableGetter,
|
345 | bool? format,
|
346 | bool? useEscaping,
|
347 | bool? suppressWarnings,
|
348 | bool? relaxSyntax,
|
349 | bool? useNamedParameters,
|
350 | }) : templateArbFile = templateArbFile ?? 'app_en.arb' ,
|
351 | outputLocalizationFile = outputLocalizationFile ?? 'app_localizations.dart' ,
|
352 | outputClass = outputClass ?? 'AppLocalizations' ,
|
353 | useDeferredLoading = useDeferredLoading ?? false,
|
354 | requiredResourceAttributes = requiredResourceAttributes ?? false,
|
355 | nullableGetter = nullableGetter ?? true,
|
356 | format = format ?? true,
|
357 | useEscaping = useEscaping ?? false,
|
358 | suppressWarnings = suppressWarnings ?? false,
|
359 | relaxSyntax = relaxSyntax ?? false,
|
360 | useNamedParameters = useNamedParameters ?? false;
|
361 |
|
362 | /// The `--arb-dir` argument.
|
363 | ///
|
364 | /// The directory where all input localization files should reside.
|
365 | final String arbDir;
|
366 |
|
367 | /// The `--output-dir` argument.
|
368 | ///
|
369 | /// The directory where all output localization files should be generated.
|
370 | final String? outputDir;
|
371 |
|
372 | /// The `--template-arb-file` argument.
|
373 | ///
|
374 | /// This path is relative to [arbDir].
|
375 | final String templateArbFile;
|
376 |
|
377 | /// The `--output-localization-file` argument.
|
378 | ///
|
379 | /// This path is relative to [arbDir].
|
380 | final String outputLocalizationFile;
|
381 |
|
382 | /// The `--untranslated-messages-file` argument.
|
383 | ///
|
384 | /// This path is relative to [arbDir].
|
385 | final String? untranslatedMessagesFile;
|
386 |
|
387 | /// The `--output-class` argument.
|
388 | final String outputClass;
|
389 |
|
390 | /// The `--preferred-supported-locales` argument.
|
391 | final List<String>? preferredSupportedLocales;
|
392 |
|
393 | /// The `--header` argument.
|
394 | ///
|
395 | /// The header to prepend to the generated Dart localizations.
|
396 | final String? header;
|
397 |
|
398 | /// The `--header-file` argument.
|
399 | ///
|
400 | /// A file containing the header to prepend to the generated
|
401 | /// Dart localizations.
|
402 | final String? headerFile;
|
403 |
|
404 | /// The `--use-deferred-loading` argument.
|
405 | ///
|
406 | /// Whether to generate the Dart localization file with locales imported
|
407 | /// as deferred.
|
408 | final bool useDeferredLoading;
|
409 |
|
410 | /// The `--gen-inputs-and-outputs-list` argument.
|
411 | ///
|
412 | /// This path is relative to [arbDir].
|
413 | final String? genInputsAndOutputsList;
|
414 |
|
415 | /// The `--project-dir` argument.
|
416 | ///
|
417 | /// This path is relative to [arbDir].
|
418 | final String? projectDir;
|
419 |
|
420 | /// The `required-resource-attributes` argument.
|
421 | ///
|
422 | /// Whether to require all resource ids to contain a corresponding
|
423 | /// resource attribute.
|
424 | final bool requiredResourceAttributes;
|
425 |
|
426 | /// The `nullable-getter` argument.
|
427 | ///
|
428 | /// Whether or not the localizations class getter is nullable.
|
429 | final bool nullableGetter;
|
430 |
|
431 | /// The `format` argument.
|
432 | ///
|
433 | /// Whether or not to format the generated files.
|
434 | final bool format;
|
435 |
|
436 | /// The `use-escaping` argument.
|
437 | ///
|
438 | /// Whether or not the ICU escaping syntax is used.
|
439 | final bool useEscaping;
|
440 |
|
441 | /// The `suppress-warnings` argument.
|
442 | ///
|
443 | /// Whether or not to suppress warnings.
|
444 | final bool suppressWarnings;
|
445 |
|
446 | /// The `relax-syntax` argument.
|
447 | ///
|
448 | /// Whether or not to relax the syntax. When specified, the syntax will be
|
449 | /// relaxed so that the special character "{" is treated as a string if it is
|
450 | /// not followed by a valid placeholder and "}" is treated as a string if it
|
451 | /// does not close any previous "{" that is treated as a special character.
|
452 | /// This was added in for backward compatibility and is not recommended
|
453 | /// as it may mask errors.
|
454 | final bool relaxSyntax;
|
455 |
|
456 | /// The `use-named-parameters` argument.
|
457 | ///
|
458 | /// Whether or not to use named parameters for the generated localization
|
459 | /// methods.
|
460 | ///
|
461 | /// Defaults to `false`.
|
462 | final bool useNamedParameters;
|
463 | }
|
464 |
|
465 | /// Parse the localizations configuration options from [file].
|
466 | ///
|
467 | /// Throws [Exception] if any of the contents are invalid. Returns a
|
468 | /// [LocalizationOptions] with all fields as `null` if the config file exists
|
469 | /// but is empty.
|
470 | LocalizationOptions parseLocalizationsOptionsFromYAML({
|
471 | required File file,
|
472 | required Logger logger,
|
473 | required FileSystem fileSystem,
|
474 | required String defaultArbDir,
|
475 | }) {
|
476 | final String contents = file.readAsStringSync();
|
477 | if (contents.trim().isEmpty) {
|
478 | return LocalizationOptions(arbDir: defaultArbDir);
|
479 | }
|
480 | final YamlNode yamlNode;
|
481 | try {
|
482 | yamlNode = loadYamlNode(file.readAsStringSync());
|
483 | } on YamlException catch (err) {
|
484 | throwToolExit(err.message);
|
485 | }
|
486 | if (yamlNode is! YamlMap) {
|
487 | logger.printError('Expected ${file.path} to contain a map, instead was $yamlNode' );
|
488 | throw Exception();
|
489 | }
|
490 | const kSyntheticPackage = 'synthetic-package' ;
|
491 | const kFlutterGenNotice = 'http://flutter.dev/to/flutter-gen-deprecation';
|
492 | final bool? syntheticPackage = _tryReadBool(yamlNode, kSyntheticPackage, logger);
|
493 | if (syntheticPackage != null) {
|
494 | if (syntheticPackage) {
|
495 | throwToolExit(
|
496 | ' ${file.path}: Cannot enable " $kSyntheticPackage", this feature has '
|
497 | 'been removed. See $kFlutterGenNotice.' ,
|
498 | );
|
499 | } else {
|
500 | logger.printWarning(
|
501 | ' ${file.path}: The argument " $kSyntheticPackage" no longer has any '
|
502 | 'effect and should be removed. See $kFlutterGenNotice' ,
|
503 | );
|
504 | }
|
505 | }
|
506 | return LocalizationOptions(
|
507 | arbDir: _tryReadFilePath(yamlNode, 'arb-dir' , logger, fileSystem) ?? defaultArbDir,
|
508 | outputDir: _tryReadFilePath(yamlNode, 'output-dir' , logger, fileSystem),
|
509 | templateArbFile: _tryReadFilePath(yamlNode, 'template-arb-file' , logger, fileSystem),
|
510 | outputLocalizationFile: _tryReadFilePath(
|
511 | yamlNode,
|
512 | 'output-localization-file' ,
|
513 | logger,
|
514 | fileSystem,
|
515 | ),
|
516 | untranslatedMessagesFile: _tryReadFilePath(
|
517 | yamlNode,
|
518 | 'untranslated-messages-file' ,
|
519 | logger,
|
520 | fileSystem,
|
521 | ),
|
522 | outputClass: _tryReadString(yamlNode, 'output-class' , logger),
|
523 | header: _tryReadString(yamlNode, 'header' , logger),
|
524 | headerFile: _tryReadFilePath(yamlNode, 'header-file' , logger, fileSystem),
|
525 | useDeferredLoading: _tryReadBool(yamlNode, 'use-deferred-loading' , logger),
|
526 | preferredSupportedLocales: _tryReadStringList(yamlNode, 'preferred-supported-locales' , logger),
|
527 | requiredResourceAttributes: _tryReadBool(yamlNode, 'required-resource-attributes' , logger),
|
528 | nullableGetter: _tryReadBool(yamlNode, 'nullable-getter' , logger),
|
529 | format: _tryReadBool(yamlNode, 'format' , logger),
|
530 | useEscaping: _tryReadBool(yamlNode, 'use-escaping' , logger),
|
531 | suppressWarnings: _tryReadBool(yamlNode, 'suppress-warnings' , logger),
|
532 | relaxSyntax: _tryReadBool(yamlNode, 'relax-syntax' , logger),
|
533 | useNamedParameters: _tryReadBool(yamlNode, 'use-named-parameters' , logger),
|
534 | );
|
535 | }
|
536 |
|
537 | /// Parse the localizations configuration from [FlutterCommand].
|
538 | LocalizationOptions parseLocalizationsOptionsFromCommand({
|
539 | required FlutterCommand command,
|
540 | required String defaultArbDir,
|
541 | }) {
|
542 | const kSyntheticPackage = 'synthetic-package' ;
|
543 | const kFlutterGenNotice = 'http://flutter.dev/to/flutter-gen-deprecation';
|
544 | if (command.argResults!.wasParsed(kSyntheticPackage)) {
|
545 | if (command.boolArg(kSyntheticPackage)) {
|
546 | throwToolExit(
|
547 | 'Cannot enable " $kSyntheticPackage", this feature has been removed. '
|
548 | 'See $kFlutterGenNotice.' ,
|
549 | );
|
550 | } else {
|
551 | globals.logger.printWarning(
|
552 | 'The argument " $kSyntheticPackage" no longer has any effect and should '
|
553 | 'be removed. See $kFlutterGenNotice' ,
|
554 | );
|
555 | }
|
556 | }
|
557 | return LocalizationOptions(
|
558 | arbDir: command.stringArg('arb-dir' ) ?? defaultArbDir,
|
559 | outputDir: command.stringArg('output-dir' ),
|
560 | outputLocalizationFile: command.stringArg('output-localization-file' ),
|
561 | templateArbFile: command.stringArg('template-arb-file' ),
|
562 | untranslatedMessagesFile: command.stringArg('untranslated-messages-file' ),
|
563 | outputClass: command.stringArg('output-class' ),
|
564 | header: command.stringArg('header' ),
|
565 | headerFile: command.stringArg('header-file' ),
|
566 | useDeferredLoading: command.boolArg('use-deferred-loading' ),
|
567 | genInputsAndOutputsList: command.stringArg('gen-inputs-and-outputs-list' ),
|
568 | projectDir: command.stringArg('project-dir' ),
|
569 | requiredResourceAttributes: command.boolArg('required-resource-attributes' ),
|
570 | nullableGetter: command.boolArg('nullable-getter' ),
|
571 | format: command.boolArg('format' ),
|
572 | useEscaping: command.boolArg('use-escaping' ),
|
573 | suppressWarnings: command.boolArg('suppress-warnings' ),
|
574 | useNamedParameters: command.boolArg('use-named-parameters' ),
|
575 | );
|
576 | }
|
577 |
|
578 | // Try to read a `bool` value or null from `yamlMap`, otherwise throw.
|
579 | bool? _tryReadBool(YamlMap yamlMap, String key, Logger logger) {
|
580 | final Object? value = yamlMap[key];
|
581 | if (value == null) {
|
582 | return null;
|
583 | }
|
584 | if (value is! bool) {
|
585 | logger.printError('Expected " $key" to have a bool value, instead was " $value"' );
|
586 | throw Exception();
|
587 | }
|
588 | return value;
|
589 | }
|
590 |
|
591 | // Try to read a `String` value or null from `yamlMap`, otherwise throw.
|
592 | String? _tryReadString(YamlMap yamlMap, String key, Logger logger) {
|
593 | final Object? value = yamlMap[key];
|
594 | if (value == null) {
|
595 | return null;
|
596 | }
|
597 | if (value is! String) {
|
598 | logger.printError('Expected " $key" to have a String value, instead was " $value"' );
|
599 | throw Exception();
|
600 | }
|
601 | return value;
|
602 | }
|
603 |
|
604 | List<String>? _tryReadStringList(YamlMap yamlMap, String key, Logger logger) {
|
605 | final Object? value = yamlMap[key];
|
606 | if (value == null) {
|
607 | return null;
|
608 | }
|
609 | if (value is String) {
|
610 | return <String>[value];
|
611 | }
|
612 | if (value is Iterable) {
|
613 | return value.map((dynamic e) => e.toString()).toList();
|
614 | }
|
615 | logger.printError('" $value" must be String or List.' );
|
616 | throw Exception();
|
617 | }
|
618 |
|
619 | // Try to read a valid file `Uri` or null from `yamlMap` to file path, otherwise throw.
|
620 | String? _tryReadFilePath(YamlMap yamlMap, String key, Logger logger, FileSystem fileSystem) {
|
621 | final String? value = _tryReadString(yamlMap, key, logger);
|
622 | if (value == null) {
|
623 | return null;
|
624 | }
|
625 | final Uri? uri = Uri.tryParse(value);
|
626 | if (uri == null) {
|
627 | logger.printError('" $value" must be a relative file URI' );
|
628 | }
|
629 | return uri != null ? fileSystem.path.normalize(uri.path) : null;
|
630 | }
|
631 |
|