Unverified Commit 202b045b authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Rewrite the analyze-sample-code script to also analyze snippets (#23893)

This rewrites the sample code analysis script to be a little less of a hack (but still not pretty), and to handle snippets as well.

It also changes the semantics of how sample code is handled: the namespace for the sample code is now limited to the file that it appears in, so some additional "Examples can assume:" blocks were added. The upside of this is that there will be far fewer name collisions.

I fixed the output too: no longer will you get 4000 lines of numbered output with the error at the top and have to grep for the actual problem. It gives the filename and line number of the original location of the code (in the comment in the tree), and prints out the source code on the line that caused the problem along with the error.

For snippets, it prints out the location of the start of the snippet and the source code line that causes the problem. It can't print out the original line, because snippets get formatted when they are written, so the line might not be in the same place.
parent 34bc1e3c
This diff is collapsed.
......@@ -2,68 +2,29 @@
// 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';
import 'common.dart';
void main() {
test('analyze-sample-code', () async {
final Process process = await Process.start(
test('analyze-sample-code', () {
final ProcessResult process = Process.runSync(
'../../bin/cache/dart-sdk/bin/dart',
<String>['analyze-sample-code.dart', 'test/analyze-sample-code-test-input'],
);
final List<String> stdout = await process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).toList();
final List<String> stderr = await process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).toList();
final Match line = RegExp(r'^(.+)/main\.dart:[0-9]+:[0-9]+: .+$').matchAsPrefix(stdout[1]);
expect(line, isNot(isNull));
final String directory = line.group(1);
Directory(directory).deleteSync(recursive: true);
expect(await process.exitCode, 1);
expect(stderr, isEmpty);
expect(stdout, <String>[
'Found 2 sample code sections.',
"$directory/main.dart:1:8: Unused import: 'dart:async'",
"$directory/main.dart:2:8: Unused import: 'dart:convert'",
"$directory/main.dart:3:8: Unused import: 'dart:math'",
"$directory/main.dart:4:8: Unused import: 'dart:typed_data'",
"$directory/main.dart:5:8: Unused import: 'dart:ui'",
"$directory/main.dart:6:8: Unused import: 'package:flutter_test/flutter_test.dart'",
"$directory/main.dart:9:8: Target of URI doesn't exist: 'package:flutter/known_broken_documentation.dart'",
'test/analyze-sample-code-test-input/known_broken_documentation.dart:27:5: Unnecessary new keyword (unnecessary_new)',
"test/analyze-sample-code-test-input/known_broken_documentation.dart:27:9: Undefined class 'Opacity' (undefined_class)",
"test/analyze-sample-code-test-input/known_broken_documentation.dart:29:20: Undefined class 'Text' (undefined_class)",
'test/analyze-sample-code-test-input/known_broken_documentation.dart:39:5: Unnecessary new keyword (unnecessary_new)',
"test/analyze-sample-code-test-input/known_broken_documentation.dart:39:9: Undefined class 'Opacity' (undefined_class)",
"test/analyze-sample-code-test-input/known_broken_documentation.dart:41:20: Undefined class 'Text' (undefined_class)",
'test/analyze-sample-code-test-input/known_broken_documentation.dart:42:5: unexpected comma at end of sample code',
'Kept $directory because it had errors (see above).',
'-------8<-------',
' 1: // generated code',
" 2: import 'dart:async';",
" 3: import 'dart:convert';",
" 4: import 'dart:math' as math;",
" 5: import 'dart:typed_data';",
" 6: import 'dart:ui' as ui;",
" 7: import 'package:flutter_test/flutter_test.dart';",
' 8: ',
' 9: // test/analyze-sample-code-test-input/known_broken_documentation.dart',
" 10: import 'package:flutter/known_broken_documentation.dart';",
' 11: ',
' 12: bool _visible = true;',
' 13: dynamic expression1 = ',
' 14: new Opacity(',
' 15: opacity: _visible ? 1.0 : 0.0,',
" 16: child: const Text('Poor wandering ones!'),",
' 17: )',
' 18: ;',
' 19: dynamic expression2 = ',
' 20: new Opacity(',
' 21: opacity: _visible ? 1.0 : 0.0,',
" 22: child: const Text('Poor wandering ones!'),",
' 23: ),',
' 24: ;',
'-------8<-------',
final List<String> stdoutLines = process.stdout.toString().split('\n');
final List<String> stderrLines = process.stderr.toString().split('\n')
..removeWhere((String line) => line.startsWith('Analyzer output:'));
expect(process.exitCode, isNot(equals(0)));
expect(stderrLines, <String>[
'known_broken_documentation.dart:27:9: new Opacity(',
'>>> Unnecessary new keyword (unnecessary_new)',
'known_broken_documentation.dart:39:9: new Opacity(',
'>>> Unnecessary new keyword (unnecessary_new)',
'',
'Found 1 sample code errors.',
'',
]);
}, skip: !Platform.isLinux);
expect(stdoutLines, <String>['Found 2 sample code sections.', 'Starting analysis of samples.', '']);
}, skip: Platform.isWindows);
}
......@@ -13,20 +13,20 @@ class MyApp extends StatelessWidget {
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: '{{id}} Sample'),
home: new MyStatefulWidget(),
);
}
}
{{code-preamble}}
class MyHomePage extends StatelessWidget {
MyHomePage({Key key}) : super(key: key);
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key}) : super(key: key);
@override
_MyHomePageState createState() => new _MyHomePageState();
_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}
class _MyHomePageState extends State<MyHomePage> {
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
{{code}}
}
{{description}}
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Code Sample for {{id}}',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyStatelessWidget(),
);
}
}
{{code-preamble}}
class MyStatelessWidget extends StatelessWidget {
MyStatelessWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return {{code}}
}
}
......@@ -5,6 +5,7 @@
import 'dart:io' hide Platform;
import 'package:args/args.dart';
import 'package:path/path.dart' as path;
import 'package:platform/platform.dart';
import 'configuration.dart';
......@@ -16,6 +17,7 @@ const String _kLibraryOption = 'library';
const String _kPackageOption = 'package';
const String _kTemplateOption = 'template';
const String _kTypeOption = 'type';
const String _kOutputOption = 'output';
/// Generates snippet dartdoc output for a given input, and creates any sample
/// applications needed by the snippet.
......@@ -44,6 +46,13 @@ void main(List<String> argList) {
defaultsTo: null,
help: 'The name of the template to inject the code into.',
);
parser.addOption(
_kOutputOption,
defaultsTo: null,
help: 'The output path for the generated snippet application. Overrides '
'the naming generated by the --package/--library/--element arguments. '
'The basename of this argument is used as the ID',
);
parser.addOption(
_kInputOption,
defaultsTo: environment['INPUT'],
......@@ -93,22 +102,25 @@ void main(List<String> argList) {
}
final List<String> id = <String>[];
if (args[_kPackageOption] != null &&
args[_kPackageOption].isNotEmpty &&
args[_kPackageOption] != 'flutter') {
id.add(args[_kPackageOption]);
}
if (args[_kLibraryOption] != null && args[_kLibraryOption].isNotEmpty) {
id.add(args[_kLibraryOption]);
}
if (args[_kElementOption] != null && args[_kElementOption].isNotEmpty) {
id.add(args[_kElementOption]);
}
if (id.isEmpty) {
errorExit('Unable to determine ID. At least one of --$_kPackageOption, '
'--$_kLibraryOption, --$_kElementOption, or the environment variables '
'PACKAGE_NAME, LIBRARY_NAME, or ELEMENT_NAME must be non-empty.');
if (args[_kOutputOption] != null) {
id.add(path.basename(path.basenameWithoutExtension(args[_kOutputOption])));
} else {
if (args[_kPackageOption] != null &&
args[_kPackageOption].isNotEmpty &&
args[_kPackageOption] != 'flutter') {
id.add(args[_kPackageOption]);
}
if (args[_kLibraryOption] != null && args[_kLibraryOption].isNotEmpty) {
id.add(args[_kLibraryOption]);
}
if (args[_kElementOption] != null && args[_kElementOption].isNotEmpty) {
id.add(args[_kElementOption]);
}
if (id.isEmpty) {
errorExit('Unable to determine ID. At least one of --$_kPackageOption, '
'--$_kLibraryOption, --$_kElementOption, or the environment variables '
'PACKAGE_NAME, LIBRARY_NAME, or ELEMENT_NAME must be non-empty.');
}
}
final SnippetGenerator generator = SnippetGenerator();
......@@ -117,6 +129,7 @@ void main(List<String> argList) {
snippetType,
template: template,
id: id.join('.'),
output: args[_kOutputOption] != null ? File(args[_kOutputOption]) : null,
));
exit(0);
}
......@@ -55,9 +55,7 @@ class SnippetGenerator {
/// "description" injection into a comment. Only used for
/// [SnippetType.application] snippets.
String interpolateTemplate(List<_ComponentTuple> injections, String template) {
final String injectionMatches =
injections.map<String>((_ComponentTuple tuple) => RegExp.escape(tuple.name)).join('|');
final RegExp moustacheRegExp = RegExp('{{($injectionMatches)}}');
final RegExp moustacheRegExp = RegExp('{{([^}]+)}}');
return template.replaceAllMapped(moustacheRegExp, (Match match) {
if (match[1] == 'description') {
// Place the description into a comment.
......@@ -77,9 +75,13 @@ class SnippetGenerator {
}
return description.join('\n').trim();
} else {
// If the match isn't found in the injections, then just remove the
// moustache reference, since we want to allow the sections to be
// "optional" in the input: users shouldn't be forced to add an empty
// "```dart preamble" section if that section would be empty.
return injections
.firstWhere((_ComponentTuple tuple) => tuple.name == match[1])
.mergedContent;
.firstWhere((_ComponentTuple tuple) => tuple.name == match[1], orElse: () => null)
?.mergedContent ?? '';
}
}).trim();
}
......@@ -103,17 +105,11 @@ class SnippetGenerator {
if (result.length > 3) {
result.removeRange(result.length - 3, result.length);
}
String formattedCode;
try {
formattedCode = formatter.format(result.join('\n'));
} on FormatterException catch (exception) {
errorExit('Unable to format snippet code: $exception');
}
final Map<String, String> substitutions = <String, String>{
'description': injections
.firstWhere((_ComponentTuple tuple) => tuple.name == 'description')
.mergedContent,
'code': formattedCode,
'code': result.join('\n'),
}..addAll(type == SnippetType.application
? <String, String>{
'id':
......@@ -182,7 +178,7 @@ class SnippetGenerator {
/// The [id] is a string ID to use for the output file, and to tell the user
/// about in the `flutter create` hint. It must not be null if the [type] is
/// [SnippetType.application].
String generate(File input, SnippetType type, {String template, String id}) {
String generate(File input, SnippetType type, {String template, String id, File output}) {
assert(template != null || type != SnippetType.application);
assert(id != null || type != SnippetType.application);
assert(input != null);
......@@ -207,11 +203,14 @@ class SnippetGenerator {
try {
app = formatter.format(app);
} on FormatterException catch (exception) {
stderr.write('Code to format:\n$app\n');
errorExit('Unable to format snippet app template: $exception');
}
snippetData.add(_ComponentTuple('app', app.split('\n')));
getOutputFile(id).writeAsStringSync(app);
final File outputFile = output ?? getOutputFile(id);
stderr.writeln('Writing to ${outputFile.absolute.path}');
outputFile.writeAsStringSync(app);
break;
case SnippetType.sample:
break;
......
......@@ -8,6 +8,9 @@ import 'package:flutter/foundation.dart';
import 'tween.dart';
// Examples can assume:
// AnimationController _controller;
/// The status of an animation
enum AnimationStatus {
/// The animation is stopped at the beginning
......
......@@ -19,6 +19,7 @@ export 'package:flutter/scheduler.dart' show TickerFuture, TickerCanceled;
// Examples can assume:
// AnimationController _controller, fadeAnimationController, sizeAnimationController;
// bool dismissed;
// void setState(VoidCallback fn) { }
/// The direction in which an animation is running.
enum _AnimationDirection {
......
......@@ -11,6 +11,9 @@ import 'animation.dart';
import 'curves.dart';
import 'listener_helpers.dart';
// Examples can assume:
// AnimationController controller;
class _AlwaysCompleteAnimation extends Animation<double> {
const _AlwaysCompleteAnimation();
......
......@@ -12,6 +12,7 @@ import 'curves.dart';
// Examples can assume:
// Animation<Offset> _animation;
// AnimationController _controller;
/// An object that can produce a value of type `T` given an [Animation<double>]
/// as input.
......@@ -413,7 +414,7 @@ class ConstantTween<T> extends Tween<T> {
/// animation produced by an [AnimationController] `controller`:
///
/// ```dart
/// final Animation<double> animation = controller.drive(
/// final Animation<double> animation = _controller.drive(
/// CurveTween(curve: Curves.ease),
/// );
/// ```
......
......@@ -14,6 +14,7 @@ import 'thumb_painter.dart';
// Examples can assume:
// int _cupertinoSliderValue = 1;
// void setState(VoidCallback fn) { }
/// An iOS-style slider.
///
......
......@@ -13,6 +13,10 @@ import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'thumb_painter.dart';
// Examples can assume:
// bool _lights;
// void setState(VoidCallback fn) { }
/// An iOS-style switch.
///
/// Used to toggle the on/off state of a single setting.
......
......@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Examples can assume:
// class Cat { }
/// A category with which to annotate a class, for documentation
/// purposes.
///
......
......@@ -6,6 +6,11 @@ import 'package:meta/meta.dart';
import 'print.dart';
// Examples can assume:
// int rows, columns;
// String _name;
// bool inherit;
/// The various priority levels used to filter which diagnostics are shown and
/// omitted.
///
......
......@@ -219,7 +219,9 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// For less common operations, consider using a [PopupMenuButton] as the
/// last action.
///
/// ## Sample code
/// {@tool snippet --template=stateless_widget}
///
/// This sample shows adding an action to an [AppBar] that opens a shopping cart.
///
/// ```dart
/// Scaffold(
......@@ -235,8 +237,9 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// ),
/// ],
/// ),
/// )
/// );
/// ```
/// {@end-tool}
final List<Widget> actions;
/// This widget is stacked behind the toolbar and the tabbar. It's height will
......
......@@ -12,9 +12,10 @@ import 'theme.dart';
/// A card is a sheet of [Material] used to represent some related information,
/// for example an album, a geographical location, a meal, contact details, etc.
///
/// ## Sample code
/// {@tool snippet --template=stateless_widget}
///
/// Here is an example of using a [Card] widget.
/// This sample shows creation of a [Card] widget that shows album information
/// and two actions.
///
/// ```dart
/// Card(
......@@ -42,10 +43,11 @@ import 'theme.dart';
/// ),
/// ],
/// ),
/// )
/// );
/// ```
/// {@end-tool}
///
/// This is what it would look like:
/// This is what it looks like when run:
///
/// ![A card with a slight shadow, consisting of two rows, one with an icon and
/// some text describing a musical, and the other with buttons for buying
......
......@@ -9,6 +9,9 @@ import 'list_tile.dart';
import 'theme.dart';
import 'theme_data.dart';
// Examples can assume:
// void setState(VoidCallback fn) { }
/// A [ListTile] with a [Checkbox]. In other words, a checkbox with a label.
///
/// The entire list tile is interactive: tapping anywhere in the tile toggles
......
......@@ -20,6 +20,7 @@ import 'theme.dart';
// Examples can assume:
// enum Department { treasury, state }
// BuildContext context;
/// A material design dialog.
///
......
......@@ -7,6 +7,9 @@ import 'package:flutter/painting.dart';
import 'theme.dart';
// Examples can assume:
// BuildContext context;
/// A one device pixel thick horizontal line, with padding on either
/// side.
///
......
......@@ -37,15 +37,25 @@ const double _kMinButtonSize = 48.0;
/// requirements in the Material Design specification. The [alignment] controls
/// how the icon itself is positioned within the hit region.
///
/// ## Sample code
/// {@tool snippet --template=stateful_widget}
///
/// This sample shows an `IconButton` that uses the Material icon "volume_up" to
/// increase the volume.
///
/// ```dart preamble
/// double _volume = 0.0;
/// ```
///
/// ```dart
/// IconButton(
/// icon: Icon(Icons.volume_up),
/// tooltip: 'Increase volume by 10%',
/// onPressed: () { setState(() { _volume *= 1.1; }); },
/// )
/// Widget build(BuildContext) {
/// return IconButton(
/// icon: Icon(Icons.volume_up),
/// tooltip: 'Increase volume by 10%',
/// onPressed: () { setState(() { _volume *= 1.1; }); },
/// );
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
......
......@@ -21,6 +21,9 @@ import 'theme.dart';
// Examples can assume:
// enum Commands { heroAndScholar, hurricaneCame }
// dynamic _heroAndScholar;
// dynamic _selection;
// BuildContext context;
// void setState(VoidCallback fn) { }
const Duration _kMenuDuration = Duration(milliseconds: 300);
const double _kBaselineOffsetFromBottom = 20.0;
......
......@@ -9,6 +9,9 @@ import 'radio.dart';
import 'theme.dart';
import 'theme_data.dart';
// Examples can assume:
// void setState(VoidCallback fn) { }
/// A [ListTile] with a [Radio]. In other words, a radio button with a label.
///
/// The entire list tile is interactive: tapping anywhere in the tile selects
......
......@@ -20,6 +20,7 @@ import 'theme.dart';
// Examples can assume:
// int _dollars = 0;
// int _duelCommandment = 1;
// void setState(VoidCallback fn) { }
/// A callback that formats a numeric value from a [Slider] widget.
///
......
......@@ -9,6 +9,10 @@ import 'switch.dart';
import 'theme.dart';
import 'theme_data.dart';
// Examples can assume:
// void setState(VoidCallback fn) { }
// bool _lights;
/// A [ListTile] with a [Switch]. In other words, a switch with a label.
///
/// The entire list tile is interactive: tapping anywhere in the tile toggles
......
......@@ -9,6 +9,9 @@ import 'border_radius.dart';
import 'borders.dart';
import 'edge_insets.dart';
// Examples can assume:
// BuildContext context;
/// The shape to use when rendering a [Border] or [BoxDecoration].
///
/// Consider using [ShapeBorder] subclasses directly (with [ShapeDecoration]),
......
......@@ -13,6 +13,9 @@ const String _kDefaultDebugLabel = 'unknown';
const String _kColorForegroundWarning = 'Cannot provide both a color and a foreground\n'
'The color argument is just a shorthand for "foreground: new Paint()..color = color".';
// Examples can assume:
// BuildContext context;
/// An immutable style in which paint text.
///
/// ## Sample code
......
......@@ -4,6 +4,9 @@
import 'simulation.dart';
// Examples can assume:
// AnimationController _controller;
/// A simulation that applies a constant accelerating force.
///
/// Models a particle that follows Newton's second law of motion. The simulation
......
......@@ -10,6 +10,9 @@ import 'framework.dart';
import 'ticker_provider.dart';
import 'transitions.dart';
// Examples can assume:
// bool _first;
/// Specifies which of two children to show. See [AnimatedCrossFade].
///
/// The child that is shown will fade in, while the other will fade out.
......
......@@ -12,6 +12,7 @@ import 'framework.dart';
// Examples can assume:
// dynamic _lot;
// Future<String> _calculation;
/// Base class for widgets that build themselves based on interaction with
/// a specified [Stream].
......
......@@ -65,6 +65,10 @@ export 'package:flutter/rendering.dart' show
// Examples can assume:
// class TestWidget extends StatelessWidget { @override Widget build(BuildContext context) => const Placeholder(); }
// WidgetTester tester;
// bool _visible;
// class Sky extends CustomPainter { @override void paint(Canvas c, Size s) => null; @override bool shouldRepaint(Sky s) => false; }
// BuildContext context;
// dynamic userAvatarUrl;
// BIDIRECTIONAL TEXT SUPPORT
......
......@@ -10,6 +10,9 @@ import 'basic.dart';
import 'framework.dart';
import 'image.dart';
// Examples can assume:
// BuildContext context;
/// A widget that paints a [Decoration] either before or after its child paints.
///
/// [Container] insets its child by the widths of the borders; this widget does
......
......@@ -34,6 +34,11 @@ export 'package:flutter/gestures.dart' show
TapUpDetails,
Velocity;
// Examples can assume:
// bool _lights;
// void setState(VoidCallback fn) { }
// String _last;
/// Factory for creating gesture recognizers.
///
/// `T` is the type of gesture recognizer this class manages.
......
......@@ -20,6 +20,7 @@ import 'ticker_provider.dart';
// class MyPage extends Placeholder { MyPage({String title}); }
// class MyHomePage extends Placeholder { }
// NavigatorState navigator;
// BuildContext context;
/// Creates a route for the given route settings.
///
......
......@@ -8,6 +8,9 @@ import 'basic.dart';
import 'framework.dart';
import 'media_query.dart';
// Examples can assume:
// String _name;
/// The text style to apply to descendant [Text] widgets without explicit style.
class DefaultTextStyle extends InheritedWidget {
/// Creates a default text style for the given subtree.
......
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