Unverified Commit 7bff366b authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Convert snippets tool to null safety (#78646)

parent bd4f8c4b
...@@ -355,7 +355,10 @@ class SampleChecker { ...@@ -355,7 +355,10 @@ class SampleChecker {
} else { } else {
return Process.runSync( return Process.runSync(
_dartExecutable, _dartExecutable,
<String>[path.canonicalize(_snippetsSnapshotPath!), ...args], <String>[
path.canonicalize(_snippetsSnapshotPath!),
...args,
],
workingDirectory: workingDirectory, workingDirectory: workingDirectory,
); );
} }
......
...@@ -27,7 +27,7 @@ String getEnumName(dynamic enumItem) { ...@@ -27,7 +27,7 @@ String getEnumName(dynamic enumItem) {
/// A class to compute the configuration of the snippets input and output /// A class to compute the configuration of the snippets input and output
/// locations based in the current location of the snippets main.dart. /// locations based in the current location of the snippets main.dart.
class Configuration { class Configuration {
Configuration({@required this.flutterRoot}) : assert(flutterRoot != null); Configuration({required this.flutterRoot}) : assert(flutterRoot != null);
final Directory flutterRoot; final Directory flutterRoot;
...@@ -37,20 +37,22 @@ class Configuration { ...@@ -37,20 +37,22 @@ class Configuration {
Directory get configDirectory { Directory get configDirectory {
_configPath ??= Directory( _configPath ??= Directory(
path.canonicalize(path.join(flutterRoot.absolute.path, 'dev', 'snippets', 'config'))); path.canonicalize(path.join(flutterRoot.absolute.path, 'dev', 'snippets', 'config')));
return _configPath; return _configPath!;
} }
Directory _configPath; // Nullable so that we can use it as a lazy cache.
Directory? _configPath;
/// This is where the snippets themselves will be written, in order to be /// This is where the snippets themselves will be written, in order to be
/// uploaded to the docs site. /// uploaded to the docs site.
Directory get outputDirectory { Directory get outputDirectory {
_docsDirectory ??= Directory( _docsDirectory ??= Directory(
path.canonicalize(path.join(flutterRoot.absolute.path, 'dev', 'docs', 'doc', 'snippets'))); path.canonicalize(path.join(flutterRoot.absolute.path, 'dev', 'docs', 'doc', 'snippets')));
return _docsDirectory; return _docsDirectory!;
} }
Directory _docsDirectory; // Nullable so that we can use it as a lazy cache.
Directory? _docsDirectory;
/// This makes sure that the output directory exists. /// This makes sure that the output directory exists.
void createOutputDirectory() { void createOutputDirectory() {
......
...@@ -22,13 +22,13 @@ const String _kTypeOption = 'type'; ...@@ -22,13 +22,13 @@ const String _kTypeOption = 'type';
const String _kShowDartPad = 'dartpad'; const String _kShowDartPad = 'dartpad';
String getChannelName() { String getChannelName() {
final RegExp gitBranchRegexp = RegExp(r'^## (.*)'); final RegExp gitBranchRegexp = RegExp(r'^## (?<branch>.*)');
final ProcessResult gitResult = Process.runSync('git', <String>['status', '-b', '--porcelain']); final ProcessResult gitResult = Process.runSync('git', <String>['status', '-b', '--porcelain']);
if (gitResult.exitCode != 0) if (gitResult.exitCode != 0)
throw 'git status exit with non-zero exit code: ${gitResult.exitCode}'; throw 'git status exit with non-zero exit code: ${gitResult.exitCode}';
final Match gitBranchMatch = gitBranchRegexp.firstMatch( final RegExpMatch? gitBranchMatch = gitBranchRegexp.firstMatch(
(gitResult.stdout as String).trim().split('\n').first); (gitResult.stdout as String).trim().split('\n').first);
return gitBranchMatch == null ? '<unknown>' : gitBranchMatch.group(1).split('...').first; return gitBranchMatch == null ? '<unknown>' : gitBranchMatch.namedGroup('branch')!.split('...').first;
} }
/// Generates snippet dartdoc output for a given input, and creates any sample /// Generates snippet dartdoc output for a given input, and creates any sample
...@@ -113,8 +113,7 @@ void main(List<String> argList) { ...@@ -113,8 +113,7 @@ void main(List<String> argList) {
} }
final SnippetType snippetType = SnippetType.values final SnippetType snippetType = SnippetType.values
.firstWhere((SnippetType type) => getEnumName(type) == args[_kTypeOption], orElse: () => null); .firstWhere((SnippetType type) => getEnumName(type) == args[_kTypeOption]);
assert(snippetType != null, "Unable to find '${args[_kTypeOption]}' in SnippetType enum.");
if (args[_kShowDartPad] == true && snippetType != SnippetType.sample) { if (args[_kShowDartPad] == true && snippetType != SnippetType.sample) {
errorExit('${args[_kTypeOption]} was selected, but the --dartpad flag is only valid ' errorExit('${args[_kTypeOption]} was selected, but the --dartpad flag is only valid '
...@@ -132,7 +131,7 @@ void main(List<String> argList) { ...@@ -132,7 +131,7 @@ void main(List<String> argList) {
errorExit('The input file ${input.path} does not exist.'); errorExit('The input file ${input.path} does not exist.');
} }
String template; String? template;
if (snippetType == SnippetType.sample) { if (snippetType == SnippetType.sample) {
final String templateArg = args[_kTemplateOption] as String; final String templateArg = args[_kTemplateOption] as String;
if (templateArg == null || templateArg.isEmpty) { if (templateArg == null || templateArg.isEmpty) {
...@@ -143,25 +142,24 @@ void main(List<String> argList) { ...@@ -143,25 +142,24 @@ void main(List<String> argList) {
template = templateArg.replaceAll(RegExp(r'.tmpl$'), ''); template = templateArg.replaceAll(RegExp(r'.tmpl$'), '');
} }
String emptyToNull(String value) => value?.isEmpty ?? true ? null : value; final String packageName = args[_kPackageOption] as String? ?? '';
final String packageName = emptyToNull(args[_kPackageOption] as String); final String libraryName = args[_kLibraryOption] as String? ?? '';
final String libraryName = emptyToNull(args[_kLibraryOption] as String); final String elementName = args[_kElementOption] as String? ?? '';
final String elementName = emptyToNull(args[_kElementOption] as String); final String serial = args[_kSerialOption] as String? ?? '';
final String serial = emptyToNull(args[_kSerialOption] as String);
final List<String> id = <String>[]; final List<String> id = <String>[];
if (args[_kOutputOption] != null) { if (args[_kOutputOption] != null) {
id.add(path.basename(path.basenameWithoutExtension(args[_kOutputOption] as String))); id.add(path.basename(path.basenameWithoutExtension(args[_kOutputOption] as String)));
} else { } else {
if (packageName != null && packageName != 'flutter') { if (packageName.isNotEmpty && packageName != 'flutter') {
id.add(packageName); id.add(packageName);
} }
if (libraryName != null) { if (libraryName.isNotEmpty) {
id.add(libraryName); id.add(libraryName);
} }
if (elementName != null) { if (elementName.isNotEmpty) {
id.add(elementName); id.add(elementName);
} }
if (serial != null) { if (serial.isNotEmpty) {
id.add(serial); id.add(serial);
} }
if (id.isEmpty) { if (id.isEmpty) {
...@@ -178,10 +176,10 @@ void main(List<String> argList) { ...@@ -178,10 +176,10 @@ void main(List<String> argList) {
showDartPad: args[_kShowDartPad] as bool, showDartPad: args[_kShowDartPad] as bool,
template: template, template: template,
output: args[_kOutputOption] != null ? File(args[_kOutputOption] as String) : null, output: args[_kOutputOption] != null ? File(args[_kOutputOption] as String) : null,
metadata: <String, Object>{ metadata: <String, Object?>{
'sourcePath': environment['SOURCE_PATH'], 'sourcePath': environment['SOURCE_PATH'],
'sourceLine': environment['SOURCE_LINE'] != null 'sourceLine': environment['SOURCE_LINE'] != null
? int.tryParse(environment['SOURCE_LINE']) ? int.tryParse(environment['SOURCE_LINE']!)
: null, : null,
'id': id.join('.'), 'id': id.join('.'),
'channel': getChannelName(), 'channel': getChannelName(),
......
...@@ -6,7 +6,6 @@ import 'dart:convert'; ...@@ -6,7 +6,6 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dart_style/dart_style.dart'; import 'package:dart_style/dart_style.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'configuration.dart'; import 'configuration.dart';
...@@ -19,7 +18,7 @@ void errorExit(String message) { ...@@ -19,7 +18,7 @@ void errorExit(String message) {
// A Tuple containing the name and contents associated with a code block in a // A Tuple containing the name and contents associated with a code block in a
// snippet. // snippet.
class _ComponentTuple { class _ComponentTuple {
_ComponentTuple(this.name, this.contents, {String language}) : language = language ?? ''; _ComponentTuple(this.name, this.contents, {this.language = ''});
final String name; final String name;
final List<String> contents; final List<String> contents;
final String language; final String language;
...@@ -29,7 +28,7 @@ class _ComponentTuple { ...@@ -29,7 +28,7 @@ class _ComponentTuple {
/// Generates the snippet HTML, as well as saving the output snippet main to /// Generates the snippet HTML, as well as saving the output snippet main to
/// the output directory. /// the output directory.
class SnippetGenerator { class SnippetGenerator {
SnippetGenerator({Configuration configuration}) SnippetGenerator({Configuration? configuration})
: configuration = configuration ?? : configuration = configuration ??
// Flutter's root is four directories up from this script. // Flutter's root is four directories up from this script.
Configuration(flutterRoot: Directory(Platform.environment['FLUTTER_ROOT'] Configuration(flutterRoot: Directory(Platform.environment['FLUTTER_ROOT']
...@@ -52,7 +51,7 @@ class SnippetGenerator { ...@@ -52,7 +51,7 @@ class SnippetGenerator {
File getOutputFile(String id) => File(path.join(configuration.outputDirectory.path, '$id.dart')); File getOutputFile(String id) => File(path.join(configuration.outputDirectory.path, '$id.dart'));
/// Gets the path to the template file requested. /// Gets the path to the template file requested.
File getTemplatePath(String templateName, {Directory templatesDir}) { File? getTemplatePath(String templateName, {Directory? templatesDir}) {
final Directory templateDir = templatesDir ?? configuration.templatesDirectory; final Directory templateDir = templatesDir ?? configuration.templatesDirectory;
final File templateFile = File(path.join(templateDir.path, '$templateName.tmpl')); final File templateFile = File(path.join(templateDir.path, '$templateName.tmpl'));
return templateFile.existsSync() ? templateFile : null; return templateFile.existsSync() ? templateFile : null;
...@@ -61,7 +60,7 @@ class SnippetGenerator { ...@@ -61,7 +60,7 @@ class SnippetGenerator {
/// Injects the [injections] into the [template], and turning the /// Injects the [injections] into the [template], and turning the
/// "description" injection into a comment. Only used for /// "description" injection into a comment. Only used for
/// [SnippetType.sample] snippets. /// [SnippetType.sample] snippets.
String interpolateTemplate(List<_ComponentTuple> injections, String template, Map<String, Object> metadata) { String interpolateTemplate(List<_ComponentTuple> injections, String template, Map<String, Object?> metadata) {
final RegExp moustacheRegExp = RegExp('{{([^}]+)}}'); final RegExp moustacheRegExp = RegExp('{{([^}]+)}}');
return template.replaceAllMapped(moustacheRegExp, (Match match) { return template.replaceAllMapped(moustacheRegExp, (Match match) {
if (match[1] == 'description') { if (match[1] == 'description') {
...@@ -86,9 +85,12 @@ class SnippetGenerator { ...@@ -86,9 +85,12 @@ class SnippetGenerator {
// mustache reference, since we want to allow the sections to be // mustache reference, since we want to allow the sections to be
// "optional" in the input: users shouldn't be forced to add an empty // "optional" in the input: users shouldn't be forced to add an empty
// "```dart preamble" section if that section would be empty. // "```dart preamble" section if that section would be empty.
final _ComponentTuple result = injections final int componentIndex = injections
.firstWhere((_ComponentTuple tuple) => tuple.name == match[1], orElse: () => null); .indexWhere((_ComponentTuple tuple) => tuple.name == match[1]);
return result?.mergedContent ?? (metadata[match[1]] ?? '').toString(); if (componentIndex == -1) {
return (metadata[match[1]] ?? '').toString();
}
return injections[componentIndex].mergedContent;
} }
}).trim(); }).trim();
} }
...@@ -100,10 +102,15 @@ class SnippetGenerator { ...@@ -100,10 +102,15 @@ class SnippetGenerator {
/// ///
/// Takes into account the [type] and doesn't substitute in the id and the app /// Takes into account the [type] and doesn't substitute in the id and the app
/// if not a [SnippetType.sample] snippet. /// if not a [SnippetType.sample] snippet.
String interpolateSkeleton(SnippetType type, List<_ComponentTuple> injections, String skeleton, Map<String, Object> metadata) { String interpolateSkeleton(
SnippetType type,
List<_ComponentTuple> injections,
String skeleton,
Map<String, Object?> metadata,
) {
final List<String> result = <String>[]; final List<String> result = <String>[];
const HtmlEscape htmlEscape = HtmlEscape(); const HtmlEscape htmlEscape = HtmlEscape();
String language; String language = 'dart';
for (final _ComponentTuple injection in injections) { for (final _ComponentTuple injection in injections) {
if (!injection.name.startsWith('code')) { if (!injection.name.startsWith('code')) {
continue; continue;
...@@ -133,11 +140,11 @@ class SnippetGenerator { ...@@ -133,11 +140,11 @@ class SnippetGenerator {
final Map<String, String> substitutions = <String, String>{ final Map<String, String> substitutions = <String, String>{
'description': description, 'description': description,
'code': htmlEscape.convert(result.join('\n')), 'code': htmlEscape.convert(result.join('\n')),
'language': language ?? 'dart', 'language': language,
'serial': '', 'serial': '',
'id': metadata['id'] as String, 'id': metadata['id']! as String,
'channel': channel, 'channel': channel,
'element': metadata['element'] as String ?? '', 'element': (metadata['element'] ?? '') as String,
'app': '', 'app': '',
}; };
if (type == SnippetType.sample) { if (type == SnippetType.sample) {
...@@ -146,7 +153,7 @@ class SnippetGenerator { ...@@ -146,7 +153,7 @@ class SnippetGenerator {
..['app'] = htmlEscape.convert(injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'app').mergedContent); ..['app'] = htmlEscape.convert(injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'app').mergedContent);
} }
return skeleton.replaceAllMapped(RegExp('{{(${substitutions.keys.join('|')})}}'), (Match match) { return skeleton.replaceAllMapped(RegExp('{{(${substitutions.keys.join('|')})}}'), (Match match) {
return substitutions[match[1]]; return substitutions[match[1]] ?? '';
}); });
} }
...@@ -157,16 +164,16 @@ class SnippetGenerator { ...@@ -157,16 +164,16 @@ class SnippetGenerator {
input = input.trim(); input = input.trim();
final List<String> description = <String>[]; final List<String> description = <String>[];
final List<_ComponentTuple> components = <_ComponentTuple>[]; final List<_ComponentTuple> components = <_ComponentTuple>[];
String language; String? language;
final RegExp codeStartEnd = RegExp(r'^\s*```([-\w]+|[-\w]+ ([-\w]+))?\s*$'); final RegExp codeStartEnd = RegExp(r'^\s*```(?<language>[-\w]+|[-\w]+ (?<section>[-\w]+))?\s*$');
for (final String line in input.split('\n')) { for (final String line in input.split('\n')) {
final Match match = codeStartEnd.firstMatch(line); final RegExpMatch? match = codeStartEnd.firstMatch(line);
if (match != null) { // If we saw the start or end of a code block if (match != null) { // If we saw the start or end of a code block
inCodeBlock = !inCodeBlock; inCodeBlock = !inCodeBlock;
if (match[1] != null) { if (match.namedGroup('language') != null) {
language = match[1]; language = match[1]!;
if (match[2] != null) { if (match.namedGroup('section') != null) {
components.add(_ComponentTuple('code-${match[2]}', <String>[], language: language)); components.add(_ComponentTuple('code-${match.namedGroup('section')}', <String>[], language: language));
} else { } else {
components.add(_ComponentTuple('code', <String>[], language: language)); components.add(_ComponentTuple('code', <String>[], language: language));
} }
...@@ -191,7 +198,7 @@ class SnippetGenerator { ...@@ -191,7 +198,7 @@ class SnippetGenerator {
} }
String _loadFileAsUtf8(File file) { String _loadFileAsUtf8(File file) {
return file.readAsStringSync(encoding: Encoding.getByName('utf-8')); return file.readAsStringSync(encoding: utf8);
} }
String _addLineNumbers(String app) { String _addLineNumbers(String app) {
...@@ -228,13 +235,12 @@ class SnippetGenerator { ...@@ -228,13 +235,12 @@ class SnippetGenerator {
File input, File input,
SnippetType type, { SnippetType type, {
bool showDartPad = false, bool showDartPad = false,
String template, String? template,
File output, File? output,
@required Map<String, Object> metadata, required Map<String, Object?> metadata,
}) { }) {
assert(template != null || type != SnippetType.sample); assert(template != null || type != SnippetType.sample);
assert(metadata != null && metadata['id'] != null); assert(metadata['id'] != null);
assert(input != null);
assert(!showDartPad || type == SnippetType.sample, 'Only application samples work with dartpad.'); assert(!showDartPad || type == SnippetType.sample, 'Only application samples work with dartpad.');
final List<_ComponentTuple> snippetData = parseInput(_loadFileAsUtf8(input)); final List<_ComponentTuple> snippetData = parseInput(_loadFileAsUtf8(input));
switch (type) { switch (type) {
...@@ -244,10 +250,9 @@ class SnippetGenerator { ...@@ -244,10 +250,9 @@ class SnippetGenerator {
stderr.writeln('Unable to find the templates directory.'); stderr.writeln('Unable to find the templates directory.');
exit(1); exit(1);
} }
final File templateFile = getTemplatePath(template, templatesDir: templatesDir); final File? templateFile = getTemplatePath(template!, templatesDir: templatesDir);
if (templateFile == null) { if (templateFile == null) {
stderr.writeln( stderr.writeln('The template $template was not found in the templates directory ${templatesDir.path}');
'The template $template was not found in the templates directory ${templatesDir.path}');
exit(1); exit(1);
} }
final String templateContents = _loadFileAsUtf8(templateFile); final String templateContents = _loadFileAsUtf8(templateFile);
...@@ -261,20 +266,19 @@ class SnippetGenerator { ...@@ -261,20 +266,19 @@ class SnippetGenerator {
} }
snippetData.add(_ComponentTuple('app', app.split('\n'))); snippetData.add(_ComponentTuple('app', app.split('\n')));
final File outputFile = output ?? getOutputFile(metadata['id'] as String); final File outputFile = output ?? getOutputFile(metadata['id']! as String);
stderr.writeln('Writing to ${outputFile.absolute.path}'); stderr.writeln('Writing to ${outputFile.absolute.path}');
outputFile.writeAsStringSync(app); outputFile.writeAsStringSync(app);
final File metadataFile = File(path.join(path.dirname(outputFile.path), final File metadataFile = File(path.join(path.dirname(outputFile.path),
'${path.basenameWithoutExtension(outputFile.path)}.json')); '${path.basenameWithoutExtension(outputFile.path)}.json'));
stderr.writeln('Writing metadata to ${metadataFile.absolute.path}'); stderr.writeln('Writing metadata to ${metadataFile.absolute.path}');
final _ComponentTuple description = snippetData.firstWhere( final int descriptionIndex = snippetData.indexWhere(
(_ComponentTuple data) => data.name == 'description', (_ComponentTuple data) => data.name == 'description');
orElse: () => null, final String descriptionString = descriptionIndex == -1 ? '' : snippetData[descriptionIndex].mergedContent;
);
metadata.addAll(<String, Object>{ metadata.addAll(<String, Object>{
'file': path.basename(outputFile.path), 'file': path.basename(outputFile.path),
'description': description?.mergedContent, 'description': descriptionString,
}); });
metadataFile.writeAsStringSync(jsonEncoder.convert(metadata)); metadataFile.writeAsStringSync(jsonEncoder.convert(metadata));
break; break;
......
...@@ -5,7 +5,7 @@ description: A code snippet dartdoc extension for Flutter API docs. ...@@ -5,7 +5,7 @@ description: A code snippet dartdoc extension for Flutter API docs.
homepage: https://github.com/flutter/flutter homepage: https://github.com/flutter/flutter
environment: environment:
sdk: ">=2.2.2 <3.0.0" sdk: ">=2.12.1 <3.0.0"
dartdoc: dartdoc:
# Exclude this package from the hosted API docs (Ironically...). # Exclude this package from the hosted API docs (Ironically...).
......
...@@ -9,7 +9,7 @@ import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; ...@@ -9,7 +9,7 @@ import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
void main() { void main() {
group('Configuration', () { group('Configuration', () {
Configuration config; late Configuration config;
setUp(() { setUp(() {
config = Configuration(flutterRoot: Directory('/flutter sdk')); config = Configuration(flutterRoot: Directory('/flutter sdk'));
......
...@@ -12,10 +12,10 @@ import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; ...@@ -12,10 +12,10 @@ import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
void main() { void main() {
group('Generator', () { group('Generator', () {
Configuration configuration; late Configuration configuration;
SnippetGenerator generator; late SnippetGenerator generator;
Directory tmpDir; late Directory tmpDir;
File template; late File template;
setUp(() { setUp(() {
tmpDir = Directory.systemTemp.createTempSync('flutter_snippets_test.'); tmpDir = Directory.systemTemp.createTempSync('flutter_snippets_test.');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment