| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:convert'; |
| import 'dart:io' as io; |
| |
| import 'package:file/file.dart'; |
| import 'package:file/local.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:platform/platform.dart' show LocalPlatform, Platform; |
| import 'package:process/process.dart' show LocalProcessManager, ProcessManager; |
| import 'package:pub_semver/pub_semver.dart'; |
| |
| import 'data_types.dart'; |
| |
| /// An exception class to allow capture of exceptions generated by the Snippets |
| /// package. |
| class SnippetException implements Exception { |
| SnippetException(this.message, {this.file, this.line}); |
| final String message; |
| final String? file; |
| final int? line; |
| |
| @override |
| String toString() { |
| if (file != null || line != null) { |
| final String fileStr = file == null ? '' : '$file:'; |
| final String lineStr = line == null ? '' : '$line:'; |
| return '$runtimeType: $fileStr$lineStr: $message'; |
| } else { |
| return '$runtimeType: $message'; |
| } |
| } |
| } |
| |
| /// Gets the number of whitespace characters at the beginning of a line. |
| int getIndent(String line) => line.length - line.trimLeft().length; |
| |
| /// Contains information about the installed Flutter repo. |
| class FlutterInformation { |
| FlutterInformation({ |
| this.platform = const LocalPlatform(), |
| this.processManager = const LocalProcessManager(), |
| this.filesystem = const LocalFileSystem(), |
| }); |
| |
| final Platform platform; |
| final ProcessManager processManager; |
| final FileSystem filesystem; |
| |
| static FlutterInformation? _instance; |
| |
| static FlutterInformation get instance => _instance ??= FlutterInformation(); |
| |
| @visibleForTesting |
| static set instance(FlutterInformation? value) => _instance = value; |
| |
| Directory getFlutterRoot() { |
| if (platform.environment['FLUTTER_ROOT'] != null) { |
| return filesystem.directory(platform.environment['FLUTTER_ROOT']); |
| } |
| return getFlutterInformation()['flutterRoot'] as Directory; |
| } |
| |
| Version getFlutterVersion() => |
| getFlutterInformation()['frameworkVersion'] as Version; |
| |
| Version getDartSdkVersion() => |
| getFlutterInformation()['dartSdkVersion'] as Version; |
| |
| Map<String, dynamic>? _cachedFlutterInformation; |
| |
| Map<String, dynamic> getFlutterInformation() { |
| if (_cachedFlutterInformation != null) { |
| return _cachedFlutterInformation!; |
| } |
| |
| String flutterVersionJson; |
| if (platform.environment['FLUTTER_VERSION'] != null) { |
| flutterVersionJson = platform.environment['FLUTTER_VERSION']!; |
| } else { |
| String flutterCommand; |
| if (platform.environment['FLUTTER_ROOT'] != null) { |
| flutterCommand = filesystem |
| .directory(platform.environment['FLUTTER_ROOT']) |
| .childDirectory('bin') |
| .childFile('flutter') |
| .absolute |
| .path; |
| } else { |
| flutterCommand = 'flutter'; |
| } |
| io.ProcessResult result; |
| try { |
| result = processManager.runSync( |
| <String>[flutterCommand, '--version', '--machine'], |
| stdoutEncoding: utf8); |
| } on io.ProcessException catch (e) { |
| throw SnippetException( |
| 'Unable to determine Flutter information. Either set FLUTTER_ROOT, or place flutter command in your path.\n$e'); |
| } |
| if (result.exitCode != 0) { |
| throw SnippetException( |
| 'Unable to determine Flutter information, because of abnormal exit to flutter command.'); |
| } |
| flutterVersionJson = (result.stdout as String).replaceAll( |
| 'Waiting for another flutter command to release the startup lock...', |
| ''); |
| } |
| |
| final Map<String, dynamic> flutterVersion = |
| json.decode(flutterVersionJson) as Map<String, dynamic>; |
| if (flutterVersion['flutterRoot'] == null || |
| flutterVersion['frameworkVersion'] == null || |
| flutterVersion['dartSdkVersion'] == null) { |
| throw SnippetException( |
| 'Flutter command output has unexpected format, unable to determine flutter root location.'); |
| } |
| |
| final Map<String, dynamic> info = <String, dynamic>{}; |
| info['flutterRoot'] = |
| filesystem.directory(flutterVersion['flutterRoot']! as String); |
| info['frameworkVersion'] = |
| Version.parse(flutterVersion['frameworkVersion'] as String); |
| |
| final RegExpMatch? dartVersionRegex = |
| RegExp(r'(?<base>[\d.]+)(?:\s+\(build (?<detail>[-.\w]+)\))?') |
| .firstMatch(flutterVersion['dartSdkVersion'] as String); |
| if (dartVersionRegex == null) { |
| throw SnippetException( |
| 'Flutter command output has unexpected format, unable to parse dart SDK version ${flutterVersion['dartSdkVersion']}.'); |
| } |
| info['dartSdkVersion'] = Version.parse( |
| dartVersionRegex.namedGroup('detail') ?? |
| dartVersionRegex.namedGroup('base')!); |
| _cachedFlutterInformation = info; |
| |
| return info; |
| } |
| } |
| |
| /// Injects the [injections] into the [template], while turning the |
| /// "description" injection into a comment. |
| String interpolateTemplate( |
| List<SkeletonInjection> injections, |
| String template, |
| Map<String, Object?> metadata, { |
| bool addCopyright = false, |
| }) { |
| String wrapSectionMarker(Iterable<String> contents, {required String name}) { |
| if (contents.join().trim().isEmpty) { |
| // Skip empty sections. |
| return ''; |
| } |
| // We don't wrap some sections, because otherwise they generate invalid files. |
| final String result = <String>[ |
| ...contents, |
| ].join('\n'); |
| final RegExp wrappingNewlines = RegExp(r'^\n*(.*)\n*$', dotAll: true); |
| return result.replaceAllMapped( |
| wrappingNewlines, (Match match) => match.group(1)!); |
| } |
| |
| return '${addCopyright ? '{{copyright}}\n\n' : ''}$template' |
| .replaceAllMapped(RegExp(r'{{([^}]+)}}'), (Match match) { |
| final String name = match[1]!; |
| final int componentIndex = injections |
| .indexWhere((SkeletonInjection injection) => injection.name == name); |
| if (metadata[name] != null && componentIndex == -1) { |
| // If the match isn't found in the injections, then just return the |
| // metadata entry. |
| return wrapSectionMarker((metadata[name]! as String).split('\n'), |
| name: name); |
| } |
| return wrapSectionMarker( |
| componentIndex >= 0 |
| ? injections[componentIndex].stringContents |
| : <String>[], |
| name: name); |
| }).replaceAll(RegExp(r'\n\n+'), '\n\n'); |
| } |
| |
| class SampleStats { |
| const SampleStats({ |
| this.totalSamples = 0, |
| this.dartpadSamples = 0, |
| this.snippetSamples = 0, |
| this.applicationSamples = 0, |
| this.wordCount = 0, |
| this.lineCount = 0, |
| this.linkCount = 0, |
| this.description = '', |
| }); |
| |
| final int totalSamples; |
| final int dartpadSamples; |
| final int snippetSamples; |
| final int applicationSamples; |
| final int wordCount; |
| final int lineCount; |
| final int linkCount; |
| final String description; |
| bool get allOneKind => |
| totalSamples == snippetSamples || |
| totalSamples == applicationSamples || |
| totalSamples == dartpadSamples; |
| |
| @override |
| String toString() { |
| return description; |
| } |
| } |
| |
| Iterable<CodeSample> getSamplesInElements(Iterable<SourceElement>? elements) { |
| return elements |
| ?.expand<CodeSample>((SourceElement element) => element.samples) ?? |
| const <CodeSample>[]; |
| } |
| |
| SampleStats getSampleStats(SourceElement element) { |
| if (element.comment.isEmpty) { |
| return const SampleStats(); |
| } |
| final int total = element.sampleCount; |
| if (total == 0) { |
| return const SampleStats(); |
| } |
| final int dartpads = element.dartpadSampleCount; |
| final int snippets = element.snippetCount; |
| final int applications = element.applicationSampleCount; |
| final String sampleCount = <String>[ |
| if (snippets > 0) '$snippets snippet${snippets != 1 ? 's' : ''}', |
| if (applications > 0) |
| '$applications application sample${applications != 1 ? 's' : ''}', |
| if (dartpads > 0) '$dartpads dartpad sample${dartpads != 1 ? 's' : ''}' |
| ].join(', '); |
| final int wordCount = element.wordCount; |
| final int lineCount = element.lineCount; |
| final int linkCount = element.referenceCount; |
| final String description = <String>[ |
| 'Documentation has $wordCount ${wordCount == 1 ? 'word' : 'words'} on ', |
| '$lineCount ${lineCount == 1 ? 'line' : 'lines'}', |
| if (linkCount > 0 && element.hasSeeAlso) ', ', |
| if (linkCount > 0 && !element.hasSeeAlso) ' and ', |
| if (linkCount > 0) |
| 'refers to $linkCount other ${linkCount == 1 ? 'symbol' : 'symbols'}', |
| if (linkCount > 0 && element.hasSeeAlso) ', and ', |
| if (linkCount == 0 && element.hasSeeAlso) 'and ', |
| if (element.hasSeeAlso) 'has a "See also:" section', |
| '.', |
| ].join(); |
| return SampleStats( |
| totalSamples: total, |
| dartpadSamples: dartpads, |
| snippetSamples: snippets, |
| applicationSamples: applications, |
| wordCount: wordCount, |
| lineCount: lineCount, |
| linkCount: linkCount, |
| description: 'Has $sampleCount. $description', |
| ); |
| } |
| |
| /// Exit the app with a message to stderr. |
| /// Can be overridden by tests to avoid exits. |
| // ignore: prefer_function_declarations_over_variables |
| void Function(String message) errorExit = (String message) { |
| io.stderr.writeln(message); |
| io.exit(1); |
| }; |