adding packages
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ansicolor/ansicolor.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class AppLogger {
|
||||
static AppLogger log = new AppLogger._('FlutterCli');
|
||||
|
||||
final Logger _logger;
|
||||
|
||||
AppLogger._(String name) : _logger = new Logger(name) {
|
||||
final pens = {
|
||||
Level.FINE: new AnsiPen()..blue(),
|
||||
Level.INFO: new AnsiPen()..green(),
|
||||
Level.WARNING: new AnsiPen()..magenta(),
|
||||
Level.SEVERE: new AnsiPen()..red(bold: true)
|
||||
};
|
||||
|
||||
Logger.root.onRecord.listen((record) {
|
||||
for (final line in record.message.split('\n')) {
|
||||
stderr
|
||||
.writeln('${pens[record.level]('${record.level.name}:')} ${line}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Configures the log level.
|
||||
///
|
||||
/// By default, FINE log will not be printed.
|
||||
/// Sets [isVerbose] to print FINE logs also.
|
||||
set isVerbose(bool value) {
|
||||
Logger.root.level = value ? Level.FINE : Level.INFO;
|
||||
}
|
||||
|
||||
void fine(message, [Object error, StackTrace stackTrace]) =>
|
||||
_logger.log(Level.FINE, message, error, stackTrace);
|
||||
|
||||
void info(message, [Object error, StackTrace stackTrace]) =>
|
||||
_logger.log(Level.INFO, message, error, stackTrace);
|
||||
|
||||
void warning(message, [Object error, StackTrace stackTrace]) =>
|
||||
_logger.log(Level.WARNING, message, error, stackTrace);
|
||||
|
||||
void severe(message, [Object error, StackTrace stackTrace]) =>
|
||||
_logger.log(Level.SEVERE, message, error, stackTrace);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
|
||||
import 'app_logger.dart';
|
||||
import 'commands/generate.dart';
|
||||
import 'commands/new.dart';
|
||||
|
||||
class NgDartCommanderRunner extends CommandRunner {
|
||||
static const _verboseOption = 'verbose';
|
||||
|
||||
NgDartCommanderRunner()
|
||||
: super('ngflutter', 'Ngflutter is a command line interface for Flutter.') {
|
||||
argParser.addFlag(_verboseOption,
|
||||
abbr: 'v',
|
||||
help: 'Output extra logging information.',
|
||||
defaultsTo: false);
|
||||
|
||||
addCommand(new NewProjectCommand());
|
||||
addCommand(new GenerateCommand());
|
||||
}
|
||||
|
||||
Future run(Iterable<String> args) async {
|
||||
var option = super.parse(args);
|
||||
AppLogger.log.isVerbose = option[_verboseOption];
|
||||
|
||||
await runCommand(option);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
|
||||
/// Base class for commands for ngflutter executable.
|
||||
abstract class NgDartCommand extends Command {
|
||||
static const binaryName = 'ngflutter';
|
||||
ArgParser get argParser => _argParser;
|
||||
final _argParser = new ArgParser(allowTrailingOptions: true);
|
||||
|
||||
/// Reads argument for current command.
|
||||
String readArg(String errorMessage) {
|
||||
var args = argResults.rest;
|
||||
|
||||
if (args == null || args.length == 0) {
|
||||
// Usage is provided by command runner.
|
||||
throw new UsageException(errorMessage, '');
|
||||
}
|
||||
|
||||
var arg = args.first;
|
||||
args = args.skip(1).toList();
|
||||
|
||||
if (args.length > 0) {
|
||||
throw new UsageException('Unexpected argument $args', '');
|
||||
}
|
||||
|
||||
return arg;
|
||||
}
|
||||
|
||||
/// Reads argument for current command and create an EntityName.
|
||||
ReCase readArgAsEntityName(String errorMessage) =>
|
||||
getEntityName(readArg(errorMessage));
|
||||
|
||||
ReCase getEntityName(String entity) {
|
||||
ReCase entityName;
|
||||
|
||||
try {
|
||||
entityName = new ReCase(entity);
|
||||
} on ArgumentError catch (error) {
|
||||
throw new UsageException(error.message, '');
|
||||
}
|
||||
|
||||
return entityName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
|
||||
import 'command.dart';
|
||||
import 'generate_test.dart';
|
||||
import 'package:ngflutter/src/commands/generate_widget.dart';
|
||||
|
||||
/// Handles the `generate` ngflutter command.
|
||||
class GenerateCommand extends NgDartCommand {
|
||||
String get name => 'generate';
|
||||
String get description => 'Generate component or test.';
|
||||
String get invocation => '${NgDartCommand.binaryName} generate <subcommand>';
|
||||
|
||||
GenerateCommand() {
|
||||
addSubcommand(new GenerateTestCommand());
|
||||
addSubcommand(new GenerateWidgetCommand());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import '../generators/test.dart';
|
||||
import '../path_util.dart';
|
||||
import 'command.dart';
|
||||
|
||||
/// Handles the `generate test` ngflutter command.
|
||||
class GenerateTestCommand extends NgDartCommand {
|
||||
static const _classOption = 'class';
|
||||
static const _testPathOption = 'path';
|
||||
static const _tagOption = 'tag';
|
||||
|
||||
String get name => 'test';
|
||||
String get description => 'Generate Flutter component test, '
|
||||
'this command should be run under root directory of the project.';
|
||||
String get invocation => '${NgDartCommand.binaryName} generate test '
|
||||
'<component/file/path> [--class <class name>] [--path <test/file/path>] '
|
||||
'[--tag <test tag>]';
|
||||
|
||||
String get _classUnderTest => argResults[_classOption];
|
||||
String get _testPath => getNormalizedPath(argResults[_testPathOption]);
|
||||
String get _testTag => argResults[_tagOption];
|
||||
|
||||
GenerateTestCommand() {
|
||||
argParser.addOption(_classOption,
|
||||
abbr: 'c',
|
||||
help: 'Flutter component class to be tested. '
|
||||
'Will select one from the specified file if it is null.',
|
||||
defaultsTo: null);
|
||||
argParser.addOption(_testPathOption,
|
||||
abbr: 'p', help: 'Test file path', defaultsTo: 'test');
|
||||
argParser.addOption(_tagOption,
|
||||
help: 'Tag for the test', defaultsTo: 'aot');
|
||||
}
|
||||
|
||||
Future run() async {
|
||||
await new TestGenerator(
|
||||
_testTag,
|
||||
readArg('path for Flutter component file is needed.'),
|
||||
_classUnderTest,
|
||||
_testPath)
|
||||
.generate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import '../generators/widget.dart';
|
||||
import '../path_util.dart';
|
||||
import 'command.dart';
|
||||
|
||||
/// Handles the `generate widget` ngflutter command.
|
||||
class GenerateWidgetCommand extends NgDartCommand {
|
||||
static const _pathOption = 'path';
|
||||
|
||||
String get name => 'widget';
|
||||
String get description => 'Generate Flutter Widget.';
|
||||
String get invocation => '${NgDartCommand.binaryName} generate widget '
|
||||
'<WidgetName> [--path <widget/file/path>]';
|
||||
|
||||
String get _widgetPath => getNormalizedPath(argResults[_pathOption]);
|
||||
|
||||
GenerateWidgetCommand() {
|
||||
argParser.addOption(_pathOption,
|
||||
abbr: 'p', help: 'Widget file path', defaultsTo: 'lib/ui/common');
|
||||
}
|
||||
|
||||
Future run() async {
|
||||
await new WidgetGenerator(
|
||||
readArgAsEntityName('Widget name is needed.'), _widgetPath)
|
||||
.generate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import '../generators/project.dart';
|
||||
import '../path_util.dart';
|
||||
import 'command.dart';
|
||||
|
||||
/// Handles the `new` ngflutter command.
|
||||
class NewProjectCommand extends NgDartCommand {
|
||||
static const _rootComponentOption = 'root_component';
|
||||
static const _pathOption = 'path';
|
||||
|
||||
String get name => 'new';
|
||||
String get description => 'Create an Flutter project.';
|
||||
String get invocation => '${NgDartCommand.binaryName} new <project_name> '
|
||||
'[--path <project/path>] [--root_component <RootComponentName>]';
|
||||
|
||||
String get _rootComponent => argResults[_rootComponentOption];
|
||||
String get _projectPath => getNormalizedPath(argResults[_pathOption]);
|
||||
|
||||
NewProjectCommand() {
|
||||
argParser.addOption(_pathOption,
|
||||
abbr: 'p',
|
||||
help: 'Project path, '
|
||||
'a new folder will be created unde this path for the project.',
|
||||
defaultsTo: '.');
|
||||
argParser.addOption(_rootComponentOption,
|
||||
abbr: 'r',
|
||||
help: 'Class name of root component.',
|
||||
defaultsTo: 'Screen');
|
||||
}
|
||||
|
||||
Future run() async {
|
||||
await new ProjectGenerator(readArgAsEntityName('Project name is needed.'),
|
||||
_projectPath, getEntityName(_rootComponent))
|
||||
.generate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// // Copyright 2017 Google Inc.
|
||||
// //
|
||||
// // Use of this source code is governed by a BSD-style
|
||||
// // license that can be found in the LICENSE file or at
|
||||
// // https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
// /// Entity name which can produce multiple formats.
|
||||
// class EntityName {
|
||||
// static final validPatterns = <RegExp>[
|
||||
// // abc_bcd
|
||||
// new RegExp(r'^[a-z][a-z0-9]*(_[a-z0-9]+)*$'),
|
||||
// // AbcBcd or abcBcd
|
||||
// new RegExp(r'^([A-Za-z][a-z0-9]*)*$'),
|
||||
// // abc-bcd
|
||||
// new RegExp(r'^[a-z][a-z0-9]*(-[a-z0-9]+)*$')
|
||||
// ];
|
||||
// static final splitPattern = new RegExp(r'(?=[A-Z])|_|-');
|
||||
|
||||
// // Segments for the name.
|
||||
// //
|
||||
// // Each segment is a word in the name in its lowercase format.
|
||||
// final List<String> _segments;
|
||||
|
||||
// /// Camel Cased format.
|
||||
// ///
|
||||
// /// Example: AbcBcdCde.
|
||||
// String get camelCased =>
|
||||
// _segments.map((s) => '${s[0].toUpperCase()}${s.substring(1)}').join('');
|
||||
|
||||
// /// Lower Camel Cased format.
|
||||
// ///
|
||||
// /// Example: abcBcdCde.
|
||||
// String get lowerCamelCased =>
|
||||
// '${camelCased[0].toLowerCase()}${camelCased.substring(1)}';
|
||||
|
||||
// /// Underscored format.
|
||||
// ///
|
||||
// /// Example: abc_bcd_cde.
|
||||
// String get underscored => _segments.join('_');
|
||||
|
||||
// /// Dashed format.
|
||||
// ///
|
||||
// /// Example: abc-bcd-cde.
|
||||
// String get dashed => _segments.join('-');
|
||||
|
||||
// /// Space separated format.
|
||||
// ///
|
||||
// /// Example: Abc Bcd Cde.
|
||||
// String get spaced =>
|
||||
// _segments.map((s) => '${s[0].toUpperCase()}${s.substring(1)}').join(' ');
|
||||
|
||||
// /// Accepts multiple formats of the name.
|
||||
// ///
|
||||
// /// Currently patterns like abc_bcd, AbcBcd, abcBcd, abc-bcd are supported.
|
||||
// factory EntityName(String name) {
|
||||
// if (!validPatterns.any((pattern) => pattern.hasMatch(name))) {
|
||||
// throw new ArgumentError(
|
||||
// '$name is not valid. It should be of form "abc_bcd", '
|
||||
// '"AbcBcd", "abcBcd", or "abc-bcd".');
|
||||
// }
|
||||
|
||||
// final segments =
|
||||
// name.split(splitPattern).map((s) => s.toLowerCase()).toList();
|
||||
// return new EntityName._(segments);
|
||||
// }
|
||||
|
||||
// EntityName._(this._segments);
|
||||
// }
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
export 'package:args/command_runner.dart' show UsageException;
|
||||
|
||||
/// An exception class for invalid expression when analyzing the dart file.
|
||||
class InvalidExpressionError extends Error {
|
||||
final String message;
|
||||
|
||||
InvalidExpressionError(this.message);
|
||||
|
||||
String toString() => "Invalid expression: $message";
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
class FileReader {
|
||||
static FileReader reader = new FileReader._();
|
||||
|
||||
FileReader._();
|
||||
|
||||
String readAsString(String filePath, {Encoding encoding: utf8}) =>
|
||||
new File(filePath).readAsStringSync(encoding: encoding);
|
||||
|
||||
List<String> readAsLines(String filePath, {Encoding encoding: utf8}) =>
|
||||
new File(filePath).readAsLinesSync(encoding: encoding);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'app_logger.dart';
|
||||
|
||||
class FileWriter {
|
||||
static FileWriter writer = new FileWriter._();
|
||||
|
||||
FileWriter._();
|
||||
|
||||
/// Writes content to [destination]. Throws StateError if
|
||||
/// destination exists. Folder will be created if not exists.
|
||||
void write(String destination, String content) {
|
||||
var file = new File(destination);
|
||||
|
||||
if (file.existsSync()) {
|
||||
throw new StateError('File $destination already exists');
|
||||
}
|
||||
|
||||
if (!file.parent.existsSync()) {
|
||||
file.parent.createSync(recursive: true);
|
||||
}
|
||||
|
||||
AppLogger.log.info('Saving $destination');
|
||||
file.writeAsStringSync(content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'file_writer.dart';
|
||||
import 'template_file.dart';
|
||||
|
||||
/// An abstract class defines a template generator.
|
||||
abstract class Generator {
|
||||
final String _destinationFolder;
|
||||
|
||||
Generator(this._destinationFolder);
|
||||
|
||||
/// Renders templates and writes to target files.
|
||||
Future renderAndWriteTemplates(Map<String, String> templateTargets) async {
|
||||
for (final template in templateTargets.keys) {
|
||||
final content = await new TemplateFile(template, this).renderString();
|
||||
FileWriter.writer.write(
|
||||
path.join(_destinationFolder, templateTargets[template]), content);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates files defined for this generator.
|
||||
Future generate();
|
||||
|
||||
Map<String, String> toMap();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../file_reader.dart';
|
||||
import '../generator.dart';
|
||||
import '../page_object_data.dart';
|
||||
import 'package:ngflutter/src/visitors/component_info.dart';
|
||||
|
||||
/// Generator for page object.
|
||||
class PoGenerator extends Generator {
|
||||
static const _templateFolder = 'pageobject';
|
||||
static const _templateFileName = 'po.dart.mustache';
|
||||
|
||||
final String componentPath;
|
||||
final String poClassName;
|
||||
final String poFileName;
|
||||
final PageObjectData pageObjectData;
|
||||
|
||||
PoGenerator._(this.componentPath, this.poClassName, this.poFileName,
|
||||
this.pageObjectData, String destinationFolder)
|
||||
: super(destinationFolder);
|
||||
|
||||
factory PoGenerator(ComponentInfo componentInfo, String componentPath,
|
||||
String poClassName, String destinationFolder) {
|
||||
PageObjectData pageObjectData;
|
||||
|
||||
if (componentInfo.inlineTemplate == null) {
|
||||
// componentInfo.templatePath is not null here
|
||||
|
||||
var componentTemplateFilePath =
|
||||
path.join(path.dirname(componentPath), componentInfo.templatePath);
|
||||
|
||||
pageObjectData = new PageObjectData(
|
||||
FileReader.reader.readAsString(componentTemplateFilePath));
|
||||
} else {
|
||||
pageObjectData = new PageObjectData(componentInfo.inlineTemplate);
|
||||
}
|
||||
|
||||
return new PoGenerator._(
|
||||
componentPath,
|
||||
poClassName,
|
||||
'${path.basenameWithoutExtension(componentPath)}_po.dart',
|
||||
pageObjectData,
|
||||
destinationFolder);
|
||||
}
|
||||
|
||||
// Gets a map from template file name to target file name.
|
||||
Map<String, String> _getTemplateTargetPaths() {
|
||||
var results = <String, String>{};
|
||||
|
||||
results[path.join(_templateFolder, _templateFileName)] = poFileName;
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@override
|
||||
Future generate() async {
|
||||
await renderAndWriteTemplates(_getTemplateTargetPaths());
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> toMap() {
|
||||
return {
|
||||
"componentPath": componentPath,
|
||||
"poClassName": poClassName,
|
||||
"poFileName": poFileName
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:recase/recase.dart';
|
||||
|
||||
import '../generator.dart';
|
||||
import 'widget.dart';
|
||||
|
||||
/// Generator for sample Flutter project.
|
||||
class ProjectGenerator extends Generator {
|
||||
static const _templateFolder = 'project';
|
||||
static final List<String> _templateFilePaths = [
|
||||
path.join('web', 'index.html.mustache'),
|
||||
path.join('lib', 'main.dart.mustache'),
|
||||
'analysis_options.yaml',
|
||||
'.gitignore',
|
||||
'pubspec.yaml.mustache',
|
||||
];
|
||||
|
||||
/// Project name in format abc_bcd.
|
||||
final String name;
|
||||
final String description;
|
||||
|
||||
/// Root component of this project.
|
||||
final WidgetGenerator component;
|
||||
|
||||
ProjectGenerator._(
|
||||
this.name, this.description, this.component, String destinationFolder)
|
||||
: super(destinationFolder);
|
||||
|
||||
factory ProjectGenerator(ReCase projectEntityName, String destinationFolder,
|
||||
ReCase componentClassEntityName) {
|
||||
destinationFolder =
|
||||
path.join(destinationFolder, projectEntityName.snakeCase);
|
||||
var component = new WidgetGenerator(
|
||||
componentClassEntityName, path.join(destinationFolder, 'lib/ui/home'));
|
||||
|
||||
return new ProjectGenerator._(projectEntityName.snakeCase,
|
||||
projectEntityName.titleCase, component, destinationFolder);
|
||||
}
|
||||
|
||||
// Gets a map from template file name to target file name.
|
||||
Map<String, String> _getTemplateTargetPaths() {
|
||||
var results = <String, String>{};
|
||||
for (final templatePath in _templateFilePaths) {
|
||||
results[path.join(_templateFolder, templatePath)] =
|
||||
templatePath.replaceAll('.mustache', '');
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@override
|
||||
Future generate() async {
|
||||
await renderAndWriteTemplates(_getTemplateTargetPaths());
|
||||
await component.generate();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> toMap() {
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:ngflutter/src/project_model.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../generator.dart';
|
||||
import 'po.dart';
|
||||
|
||||
/// Generator for component test.
|
||||
class TestGenerator extends Generator {
|
||||
static const _templateFolder = 'test';
|
||||
static const _templateFileName = 'test.dart.mustache';
|
||||
|
||||
static final _suffixPattern = new RegExp(r'(Component|View|PO|UnitTestPO)*$');
|
||||
|
||||
final String tag;
|
||||
final String componentPath;
|
||||
final ProjectModel projectModel;
|
||||
|
||||
final PoGenerator poGenerator;
|
||||
|
||||
TestGenerator._(this.tag, this.componentPath, this.projectModel,
|
||||
this.poGenerator, String destinationFolder)
|
||||
: super(destinationFolder);
|
||||
|
||||
factory TestGenerator(String tag, String componentPath, String className,
|
||||
String destinationFolder) {
|
||||
var projectModel =
|
||||
new ProjectModel('.packages', 'pubspec.yaml', componentPath, className);
|
||||
var poClassName =
|
||||
projectModel.componentClassName.replaceAll(_suffixPattern, '') + 'PO';
|
||||
var poGenerator = new PoGenerator(
|
||||
projectModel.components[projectModel.componentClassName],
|
||||
componentPath,
|
||||
poClassName,
|
||||
destinationFolder);
|
||||
|
||||
return new TestGenerator._(
|
||||
tag, componentPath, projectModel, poGenerator, destinationFolder);
|
||||
}
|
||||
|
||||
// Gets a map from template file name to target file name.
|
||||
Map<String, String> _getTemplateTargetPaths() {
|
||||
var results = <String, String>{};
|
||||
|
||||
results[path.join(_templateFolder, _templateFileName)] =
|
||||
'${path.basenameWithoutExtension(componentPath)}_test.dart';
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@override
|
||||
Future generate() async {
|
||||
await renderAndWriteTemplates(_getTemplateTargetPaths());
|
||||
await poGenerator.generate();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> toMap() {
|
||||
return {
|
||||
"tag": tag,
|
||||
"componentPath": componentPath,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:recase/recase.dart';
|
||||
|
||||
import '../generator.dart';
|
||||
|
||||
/// Generator for Flutter Widget.
|
||||
class WidgetGenerator extends Generator {
|
||||
static const _templateFolder = 'widgets';
|
||||
static const List<String> _templateFileNames = const [
|
||||
'widget.dart.mustache',
|
||||
];
|
||||
|
||||
/// Class name of this Widget.
|
||||
final String className;
|
||||
|
||||
final String selector;
|
||||
|
||||
/// Widget file name without extension.
|
||||
final String targetName;
|
||||
|
||||
WidgetGenerator._(
|
||||
this.className, this.selector, this.targetName, String destinationFolder)
|
||||
: super(destinationFolder);
|
||||
|
||||
factory WidgetGenerator(
|
||||
ReCase classEntityName,
|
||||
String destinationFolder,
|
||||
) {
|
||||
return new WidgetGenerator._(
|
||||
classEntityName.pascalCase,
|
||||
classEntityName.paramCase,
|
||||
classEntityName.snakeCase,
|
||||
destinationFolder);
|
||||
}
|
||||
|
||||
// Gets a map from template file name to target file name.
|
||||
Map<String, String> _getTemplateTargetPaths() {
|
||||
var results = <String, String>{};
|
||||
for (String templateFileName in _templateFileNames) {
|
||||
final _template = path.join(_templateFolder, templateFileName);
|
||||
final _path = '$targetName.${templateFileName.split('.')[1]}';
|
||||
print('Path: $_template -> $_path');
|
||||
results[_template] = _path;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@override
|
||||
Future generate() async {
|
||||
await renderAndWriteTemplates(_getTemplateTargetPaths());
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> toMap() {
|
||||
return {
|
||||
"className": className,
|
||||
"selector": selector,
|
||||
"targetName": targetName,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'exceptions.dart';
|
||||
import 'file_reader.dart';
|
||||
import 'path_util.dart';
|
||||
|
||||
/// Class to convert a package URI to file URI.
|
||||
class PackageUriResolver {
|
||||
/// Path of file .packages
|
||||
final String _dotPackagesFilePath;
|
||||
|
||||
/// Maps package name to folder URI of this package.
|
||||
Map<String, String> _packageMap;
|
||||
|
||||
PackageUriResolver(this._dotPackagesFilePath);
|
||||
|
||||
void _buildPackageMap() {
|
||||
List<String> lines;
|
||||
|
||||
try {
|
||||
lines = FileReader.reader.readAsLines(_dotPackagesFilePath);
|
||||
} catch (e) {
|
||||
throw new UsageException(
|
||||
'Error when reading $_dotPackagesFilePath, '
|
||||
'please run pub get first.',
|
||||
'');
|
||||
}
|
||||
|
||||
_packageMap = <String, String>{};
|
||||
for (var line in lines) {
|
||||
if (line.startsWith('#')) continue;
|
||||
var commaPosition = line.indexOf(':');
|
||||
if (commaPosition == -1) continue;
|
||||
_packageMap[line.substring(0, commaPosition)] =
|
||||
line.substring(commaPosition + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves a package URI to a file path.
|
||||
String resolve(String packageUri) {
|
||||
if (_packageMap == null) _buildPackageMap();
|
||||
|
||||
var packageName = getPackageName(packageUri);
|
||||
|
||||
if (_packageMap[packageName] == null) {
|
||||
throw new UsageException(
|
||||
'Cannot locate $packageName, '
|
||||
'probably you need to run pub get again',
|
||||
'');
|
||||
}
|
||||
|
||||
var packagePath = getPath(packageUri);
|
||||
return Uri.parse('${_packageMap[packageName]}$packagePath').toFilePath();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:html/parser.dart' show parseFragment;
|
||||
|
||||
import 'visitors/dart_class_info.dart';
|
||||
|
||||
/// Data for generating page object.
|
||||
class PageObjectData {
|
||||
static const pageLoaderDependency = 'package:pageloader/objects.dart';
|
||||
static const asyncDependency = 'dart:async';
|
||||
|
||||
/// Common dependencies for page object classes.
|
||||
List<String> get commonDependencies => [pageLoaderDependency]
|
||||
..addAll(variables.isNotEmpty ? [asyncDependency] : []);
|
||||
|
||||
final List<_Variable> variables;
|
||||
final DocumentFragment document;
|
||||
|
||||
factory PageObjectData(String templateFileContent) {
|
||||
var document = parseFragment(templateFileContent);
|
||||
|
||||
var variables = document
|
||||
.querySelectorAll('*')
|
||||
.map((element) => new _Variable.fromElement(element))
|
||||
.where((variable) => variable != null)
|
||||
.toList(growable: false)
|
||||
..sort();
|
||||
|
||||
return new PageObjectData._internal(document, variables);
|
||||
}
|
||||
|
||||
PageObjectData._internal(this.document, this.variables);
|
||||
}
|
||||
|
||||
class _Selector {
|
||||
static const ignoredTags = const [
|
||||
'a',
|
||||
'p',
|
||||
'b',
|
||||
'i',
|
||||
'small',
|
||||
'strong',
|
||||
'object',
|
||||
'canvas',
|
||||
'table',
|
||||
'span',
|
||||
'img',
|
||||
'form',
|
||||
'fieldset',
|
||||
'li',
|
||||
'center',
|
||||
'label',
|
||||
'br',
|
||||
'legend',
|
||||
'ul',
|
||||
'ng-content'
|
||||
];
|
||||
|
||||
final String type;
|
||||
final String name;
|
||||
final String value;
|
||||
|
||||
factory _Selector.fromElement(Element element) {
|
||||
if (ignoredTags.contains(element.localName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (element.id.isNotEmpty) {
|
||||
return new _Selector('ById', element.id);
|
||||
}
|
||||
|
||||
if (element.classes.isNotEmpty) {
|
||||
return new _Selector('ByClass', element.classes.first);
|
||||
}
|
||||
|
||||
return new _Selector('ByTagName', element.localName);
|
||||
}
|
||||
|
||||
_Selector(this.type, this.name, [this.value]);
|
||||
|
||||
@override
|
||||
String toString() => "@$type('${value ?? name}')";
|
||||
}
|
||||
|
||||
class _Variable implements Comparable<_Variable> {
|
||||
static final wordReg = new RegExp(r'(^|[\-._])(\w)');
|
||||
|
||||
final Element element;
|
||||
final _Selector selector;
|
||||
final bool isList;
|
||||
final bool isOptional;
|
||||
final DartClassInfo type;
|
||||
final String name;
|
||||
|
||||
factory _Variable.fromElement(Element element) {
|
||||
var selector = new _Selector.fromElement(element);
|
||||
if (selector == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var type = new DartClassInfo(
|
||||
'PageLoaderElement', 'package:pageloader/objects.dart');
|
||||
|
||||
return new _Variable(element, _getCamelCasedName(selector.name), selector,
|
||||
type, _isElementInList(element), _isElementInIf(element));
|
||||
}
|
||||
|
||||
_Variable(this.element, this.name, this.selector, this.type,
|
||||
[this.isList = false, this.isOptional = false]);
|
||||
|
||||
static bool _isElementInList(Element element) {
|
||||
while (element != null) {
|
||||
if (element.attributes.keys.any((a) => ['*ngfor'].contains(a))) {
|
||||
return true;
|
||||
}
|
||||
element = element.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool _isElementInIf(Element element) {
|
||||
while (element != null) {
|
||||
if (element.attributes.keys.contains('*ngif') ||
|
||||
element.localName == 'template' &&
|
||||
element.attributes.keys.contains('[ngif]')) {
|
||||
return true;
|
||||
}
|
||||
element = element.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static String _getCamelCasedName(String name) =>
|
||||
name.replaceAllMapped(wordReg, (m) => m[2].toUpperCase());
|
||||
|
||||
String get internalString => '${_getOptionalString()}'
|
||||
'$selector\n'
|
||||
' Lazy<${_getTypeString()}> _get$name;';
|
||||
|
||||
String get getterString =>
|
||||
'Future<${_getTypeString()}> get $_getFirstCharacterLoweredName'
|
||||
' => _get$name();';
|
||||
|
||||
String _getOptionalString() => isOptional ? '@optional\n' : '';
|
||||
|
||||
String _getTypeString() {
|
||||
var s = type.className;
|
||||
if (isList) {
|
||||
s = 'List<$s>';
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
String get _getFirstCharacterLoweredName =>
|
||||
name.replaceFirstMapped(new RegExp('^(.)'), (m) => m[1].toLowerCase());
|
||||
|
||||
@override
|
||||
int compareTo(_Variable other) => name.compareTo(other.name);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
/// Fixes invalid path.
|
||||
///
|
||||
/// For example:
|
||||
/// 'path\to/folder' -> 'path/to/folder' (posix)
|
||||
String getNormalizedPath(String oldPath) =>
|
||||
path.normalize(path.joinAll(oldPath.split(new RegExp(r'[\\/]')))).trim();
|
||||
|
||||
/// Converts '\' in [uri] into '/'.
|
||||
String fixUri(String uri) => uri.replaceAll('\\', '/');
|
||||
|
||||
/// Extracts package name from package [uri].
|
||||
String getPackageName(String uri) =>
|
||||
uri.substring(0, uri.indexOf('/')).replaceAll('package:', '');
|
||||
|
||||
/// Extracts path from package [uri].
|
||||
String getPath(String uri) => uri.substring(uri.indexOf('/') + 1);
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'exceptions.dart';
|
||||
import 'file_reader.dart';
|
||||
import 'package_uri_resolver.dart';
|
||||
import 'path_util.dart';
|
||||
import 'visitors/flutter_component_visitor.dart';
|
||||
import 'visitors/ast_cache.dart';
|
||||
import 'visitors/binding_info.dart';
|
||||
import 'visitors/binding_visitor.dart';
|
||||
import 'visitors/component_info.dart';
|
||||
import 'visitors/dart_class_info.dart';
|
||||
import 'visitors/dart_class_visitor.dart';
|
||||
import 'visitors/visit_resources.dart';
|
||||
|
||||
/// Project model built from AST cache.
|
||||
///
|
||||
/// The project model built will contains all referenced Dart classes,
|
||||
/// all Flutter component classes, and all binding modules.
|
||||
class ProjectModel {
|
||||
final String projectName;
|
||||
|
||||
/// Name of component class to be tested.
|
||||
final String componentClassName;
|
||||
|
||||
/// Service classes needed when construct the component
|
||||
/// class [componentClassName].
|
||||
final List<String> serviceClasses;
|
||||
|
||||
/// Package URI of [componentClassName].
|
||||
final String componentClassUri;
|
||||
|
||||
/// Map from type name to info for each Dart class.
|
||||
final Map<String, DartClassInfo> dartClasses;
|
||||
|
||||
/// Map from type name to info of each Flutter component.
|
||||
final Map<String, ComponentInfo> components;
|
||||
|
||||
/// Map from var name to info of bindings.
|
||||
final Map<String, ModuleInfo> modules;
|
||||
|
||||
ProjectModel._(this.projectName, this.componentClassName, this.serviceClasses,
|
||||
this.componentClassUri, this.dartClasses, this.components, this.modules);
|
||||
|
||||
/// Whether providers are needed when generating test.
|
||||
bool get needProviders => serviceClasses != null && serviceClasses.isNotEmpty;
|
||||
|
||||
/// Uris for service classes used.
|
||||
List<String> get referencedUris => serviceClasses
|
||||
.map((className) => dartClasses[className].uri)
|
||||
.toList(growable: false);
|
||||
|
||||
factory ProjectModel(String dotPackagesFilePath, String pubspecFilePath,
|
||||
String componentPath, String className) {
|
||||
var projectName = _getProjectName(pubspecFilePath);
|
||||
var libPrefix = 'lib${path.separator}';
|
||||
var componentClassUri = componentPath.startsWith(libPrefix)
|
||||
? fixUri(
|
||||
'package:$projectName/${componentPath.substring(libPrefix.length)}')
|
||||
: fixUri('package:$projectName/$componentPath');
|
||||
|
||||
var uriResolver = new PackageUriResolver(dotPackagesFilePath);
|
||||
var asts = new AstCache(componentClassUri, uriResolver);
|
||||
asts.build();
|
||||
|
||||
var dartClasses = <String, DartClassInfo>{};
|
||||
dartClasses.addAll(visitUris<DartClassInfo>(
|
||||
asts, (file, out) => new DartClassVisitor(file, out, asts.publicUris)));
|
||||
|
||||
var components = <String, ComponentInfo>{};
|
||||
components.addAll(visitUris<ComponentInfo>(
|
||||
asts, (_, out) => new FlutterComponentVisitor(dartClasses, out)));
|
||||
|
||||
var modules = <String, ModuleInfo>{};
|
||||
modules.addAll(visitUris<ModuleInfo>(
|
||||
asts,
|
||||
(file, out) => new BindingVisitor(
|
||||
file, out, asts.publicUris, _getBindingVariables(components))));
|
||||
|
||||
var componentClassName = className;
|
||||
if (componentClassName == null) {
|
||||
componentClassName =
|
||||
_getComponentClassName(componentClassUri, components);
|
||||
}
|
||||
|
||||
var serviceClasses = _getServiceClasses(
|
||||
componentClassName, dartClasses, components, modules);
|
||||
|
||||
return new ProjectModel._(projectName, componentClassName, serviceClasses,
|
||||
componentClassUri, dartClasses, components, modules);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets project name from pubsepc [pubspecFilePath].
|
||||
String _getProjectName(String pubspecFilePath) {
|
||||
List<String> lines;
|
||||
try {
|
||||
lines = FileReader.reader.readAsLines(pubspecFilePath);
|
||||
} catch (e) {
|
||||
throw new UsageException(
|
||||
'Error happened when reading pubspec.yaml. '
|
||||
'Command generate test should be run '
|
||||
'under root directory of the project.',
|
||||
'');
|
||||
}
|
||||
var namePrefix = 'name:';
|
||||
for (var line in lines) {
|
||||
line = line.trim();
|
||||
if (line.startsWith(namePrefix)) {
|
||||
return line.substring(namePrefix.length).trim();
|
||||
}
|
||||
}
|
||||
|
||||
throw new FormatException('Invalid pubspec.yaml: cannot find project name');
|
||||
}
|
||||
|
||||
/// Gets binding variables used in providers of @Component.
|
||||
Set<String> _getBindingVariables(Map<String, ComponentInfo> components) {
|
||||
var result = new Set<String>();
|
||||
|
||||
for (var component in components.values) {
|
||||
if (component.module == null) continue;
|
||||
|
||||
for (var binding in component.module.directChildren) {
|
||||
if (binding is String) result.add(binding);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Gets one component class name from [componentClassUri].
|
||||
String _getComponentClassName(
|
||||
String componentClassUri, Map<String, ComponentInfo> components) {
|
||||
for (var component in components.values) {
|
||||
if (component.classInfo.uri == componentClassUri) {
|
||||
return component.classInfo.className;
|
||||
}
|
||||
}
|
||||
|
||||
throw new UsageException(
|
||||
'Cannot find a component class in specified path.', '');
|
||||
}
|
||||
|
||||
/// Gets all service classes used in [componentClassName] that need binding.
|
||||
///
|
||||
/// Should return an empty list if no service class is used.
|
||||
List<String> _getServiceClasses(
|
||||
String componentClassName,
|
||||
Map<String, DartClassInfo> dartClasses,
|
||||
Map<String, ComponentInfo> components,
|
||||
Map<String, ModuleInfo> modules) {
|
||||
var dependencies = <String>[];
|
||||
for (var parameter in dartClasses[componentClassName].constructorParameters) {
|
||||
var service = parameter.dependency;
|
||||
if (dartClasses[service].uri == null) continue;
|
||||
|
||||
dependencies.add(service);
|
||||
}
|
||||
|
||||
var module = components[componentClassName].module;
|
||||
if (module != null) {
|
||||
for (var binding in module.getAllBindingInstances(modules)) {
|
||||
if (dependencies.contains(binding.className)) {
|
||||
dependencies.remove(binding.className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var serviceClasses = <String>[]..addAll(dependencies);
|
||||
|
||||
return serviceClasses;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import "dart:convert" show utf8;
|
||||
|
||||
// import 'package:analyzer/file_system/file_system.dart';
|
||||
// import 'package:flutter/services.dart';
|
||||
import 'package:reflected_mustache/mustache.dart';
|
||||
// import 'package:resource/resource.dart' show Resource;
|
||||
|
||||
import 'generator.dart';
|
||||
import 'path_util.dart';
|
||||
|
||||
/// Template file class wrapping operations on mustache template.
|
||||
class TemplateFile {
|
||||
/// Template file path relative to templates/.
|
||||
final String _path;
|
||||
|
||||
/// Data for this template.
|
||||
final Generator _data;
|
||||
|
||||
TemplateFile(this._path, this._data);
|
||||
|
||||
/// Renders template file on [_path] with values from [_data].
|
||||
Future<String> renderString() async {
|
||||
var uri = fixUri('lib/templates/$_path');
|
||||
// var uri = fixUri('package:ngflutter/templates/$_path');
|
||||
// var content = await rootBundle.loadString(uri);
|
||||
var resource = new File(uri);
|
||||
var content = await resource.readAsString(encoding: utf8);
|
||||
var template = new Template(content);
|
||||
return template.renderString(_data.toMap());
|
||||
}
|
||||
}
|
||||
|
||||
// var template = new Template(source, name: 'template-filename.html');
|
||||
|
||||
// final String output = template.renderString(
|
||||
// {'className': 'FlutterWidget'},
|
||||
// );
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:analyzer/analyzer.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../app_logger.dart';
|
||||
import '../exceptions.dart';
|
||||
import '../file_reader.dart';
|
||||
import '../package_uri_resolver.dart';
|
||||
import '../path_util.dart';
|
||||
|
||||
/// Caches the output of the Dart analyzer to avoid re-analyzing dart resources.
|
||||
class AstCache {
|
||||
/// Package URI that needs to be parsed.
|
||||
final String _uri;
|
||||
|
||||
final PackageUriResolver _uriResolver;
|
||||
|
||||
/// Maps URI to compilation unit.
|
||||
final Map<String, CompilationUnit> _uriToAst = {};
|
||||
|
||||
/// Maps an internal URI to a public one.
|
||||
final Map<String, String> publicUris = {};
|
||||
|
||||
/// Creates AST cache from [_uri] and files referenced.
|
||||
///
|
||||
/// Caller should make sure that [_uri] starts with 'package';
|
||||
AstCache(this._uri, this._uriResolver);
|
||||
|
||||
/// Parses the source file and create AST cache.
|
||||
void build() {
|
||||
publicUris[_uri] = _uri;
|
||||
_setAst(_uri);
|
||||
|
||||
// Collects imported URIs using BFS.
|
||||
var queue = new Queue<String>();
|
||||
queue.add(_uri);
|
||||
while (queue.isNotEmpty) {
|
||||
var head = queue.removeFirst();
|
||||
var compilationUnit = _uriToAst[head];
|
||||
for (var uri in _getReferencedUris(head, compilationUnit)) {
|
||||
if (_uriToAst[uri] == null) {
|
||||
_setAst(uri);
|
||||
queue.add(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets AST cache for [uri].
|
||||
CompilationUnit getCompilationUnit(String uri) {
|
||||
if (_uriToAst[uri] == null) _setAst(uri);
|
||||
return _uriToAst[uri];
|
||||
}
|
||||
|
||||
List<String> get allUris => _uriToAst.keys.toList(growable: false);
|
||||
|
||||
/// Parses [uri] into AST and set it to _uriToAst[uri].
|
||||
void _setAst(String uri) {
|
||||
if (_uriToAst[uri] != null) return;
|
||||
if (_uriToAst[uri] != null) return;
|
||||
CompilationUnit compilationUnit;
|
||||
try {
|
||||
var filePath = _uriResolver.resolve(uri);
|
||||
AppLogger.log.fine('Parsing file $filePath...');
|
||||
|
||||
compilationUnit = parseCompilationUnit(
|
||||
FileReader.reader.readAsString(filePath),
|
||||
name: filePath);
|
||||
} on UsageException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
AppLogger.log.warning('Could not parse $uri: $e');
|
||||
compilationUnit = parseCompilationUnit('');
|
||||
}
|
||||
|
||||
_uriToAst[uri] = compilationUnit;
|
||||
}
|
||||
|
||||
/// Gets all files imported or exported by [uri].
|
||||
///
|
||||
/// This function will also set [publicUris] in case an implementation file
|
||||
/// is exported.
|
||||
Set<String> _getReferencedUris(String uri, CompilationUnit compilationUnit) {
|
||||
var results = new Set<String>();
|
||||
|
||||
for (var directive in compilationUnit.directives) {
|
||||
if (directive is! ImportDirective && directive is! ExportDirective) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var referencedUri = (directive as UriBasedDirective).uri.stringValue;
|
||||
|
||||
// Skips dart imports.
|
||||
if (referencedUri.startsWith('dart:')) continue;
|
||||
if (referencedUri.startsWith('package:')) {
|
||||
// Skips Flutter imports.
|
||||
if (getPackageName(referencedUri) == 'flutter') continue;
|
||||
} else {
|
||||
// Relative path.
|
||||
var directoryName = path.posix.dirname(getPath(uri));
|
||||
var referencedFile =
|
||||
path.posix.normalize(path.posix.join(directoryName, referencedUri));
|
||||
var packageName = getPackageName(uri);
|
||||
referencedUri =
|
||||
'package:${path.posix.join(packageName, referencedFile)}';
|
||||
}
|
||||
|
||||
results.add(referencedUri);
|
||||
if (directive is ExportDirective && referencedUri.contains('/src/')) {
|
||||
publicUris[referencedUri] = publicUris[uri];
|
||||
} else {
|
||||
publicUris[referencedUri] = referencedUri;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'package:analyzer/analyzer.dart';
|
||||
|
||||
import '../app_logger.dart';
|
||||
import '../exceptions.dart';
|
||||
import 'binding_info.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
/// Extracts binding information from list literal.
|
||||
///
|
||||
/// Given a list of bindings in [bindingList], extracts all information to
|
||||
/// [module].
|
||||
void extractBindingInfo(ListLiteral bindingList, ModuleInfo module) {
|
||||
for (var node in bindingList.elements) {
|
||||
try {
|
||||
processBindingElement(node, module);
|
||||
} on UnsupportedError catch (e) {
|
||||
AppLogger.log.fine(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a single binding creation expression.
|
||||
///
|
||||
/// For one binding expression [node], saves the information to [module].
|
||||
/// For code block below:
|
||||
///
|
||||
/// const testBindings = Const [
|
||||
/// X,
|
||||
/// otherBindings,
|
||||
/// const Binding(A, toAlias: B)
|
||||
/// ]
|
||||
///
|
||||
/// Simple identifier "X" and "otherBindings" will be stored in
|
||||
/// directChildren of [module] as a String, and binding creation expression
|
||||
/// "const Binding(A, toAlias: B)" will be parsed into a BindingInstance
|
||||
/// and be stored in directChildren of [module].
|
||||
void processBindingElement(Expression node, ModuleInfo module) {
|
||||
Object binding;
|
||||
if (node is SimpleIdentifier) {
|
||||
// Ignores PrefixedIdentifier here. node here can be
|
||||
// a module (binding list) or
|
||||
// an OpaqueToken or
|
||||
// a class name.
|
||||
binding = extractName(node);
|
||||
} else if (node is InstanceCreationExpression) {
|
||||
binding = _getBindingFromCreationExpression(node);
|
||||
} else if (node is MethodInvocation) {
|
||||
binding = _getBindingFromMethodInvocation(node);
|
||||
} else {
|
||||
throw new UnsupportedError('Unable to handle $node.');
|
||||
}
|
||||
|
||||
if (binding != null) module.directChildren.add(binding);
|
||||
}
|
||||
|
||||
/// Extracts binding information from instance creation expression [node].
|
||||
/// [node] can be something like below.
|
||||
///
|
||||
/// const/new Provider(Expression, ...)
|
||||
BindingInstance _getBindingFromCreationExpression(
|
||||
InstanceCreationExpression node) {
|
||||
if (extractName(node.constructorName.type.name) != 'Provider') {
|
||||
throw new UnsupportedError(
|
||||
'Unable to handle ${node.constructorName.type.name} in $node');
|
||||
}
|
||||
|
||||
return _buildBindingInstance(node.argumentList.arguments, node.toString());
|
||||
}
|
||||
|
||||
/// Extracts binding information from method invocation [node].
|
||||
/// [node] can be something like below.
|
||||
///
|
||||
/// provide(ClassABC, ...).
|
||||
BindingInstance _getBindingFromMethodInvocation(MethodInvocation node) {
|
||||
if (node.target != null) {
|
||||
throw new UnsupportedError('Unable to handle ${node.target} in $node');
|
||||
}
|
||||
|
||||
if (node.methodName.name != 'provide') {
|
||||
throw new UnsupportedError(
|
||||
'Unable to handle ${node.methodName.name} in $node');
|
||||
}
|
||||
|
||||
return _buildBindingInstance(node.argumentList.arguments, node.toString());
|
||||
}
|
||||
|
||||
BindingInstance _buildBindingInstance(
|
||||
NodeList<Expression> args, String creationExpression) {
|
||||
if (args.isEmpty) throw new InvalidExpressionError(creationExpression);
|
||||
|
||||
BindingInstance binding;
|
||||
var token = args[0];
|
||||
if (token is SimpleIdentifier || token is PrefixedIdentifier) {
|
||||
// Expression is an OpaqueToken or a class name.
|
||||
binding = new BindingInstance(extractName(token), creationExpression);
|
||||
} else if (token is InstanceCreationExpression) {
|
||||
// Expression is to create a class instance.
|
||||
binding = new BindingInstance(
|
||||
extractName(token.constructorName.type.name), creationExpression);
|
||||
} else if (token is SimpleStringLiteral) {
|
||||
binding = new BindingInstance(token.value, creationExpression);
|
||||
}
|
||||
|
||||
if (binding != null) {
|
||||
_handleBindingArgs(args, binding);
|
||||
return binding;
|
||||
}
|
||||
|
||||
throw new UnsupportedError('Unable to handle $token '
|
||||
'(${token.runtimeType}) in $creationExpression');
|
||||
}
|
||||
|
||||
/// Extracts name of the class from [expression] used in toClass / toAlias.
|
||||
String _extractClassName(Expression expression) {
|
||||
if (expression is SimpleIdentifier || expression is PrefixedIdentifier) {
|
||||
return extractName(expression);
|
||||
}
|
||||
|
||||
throw new UnsupportedError(
|
||||
'Unable to handle $expression for toClass / toAlias');
|
||||
}
|
||||
|
||||
/// Collects all referenced classes in binding creation [expression]
|
||||
/// to [bindingInstance] recursively.
|
||||
void _addReferencedClasses(
|
||||
Expression expression, BindingInstance bindingInstance) {
|
||||
if (expression is Identifier) {
|
||||
bindingInstance.referencedClasses.add(extractName(expression));
|
||||
} else if (expression is InstanceCreationExpression) {
|
||||
// Annotation, skip.
|
||||
AppLogger.log.fine('Ignore expression "$expression" in deps');
|
||||
} else if (expression is ListLiteral) {
|
||||
for (var dependency in expression.elements) {
|
||||
_addReferencedClasses(dependency, bindingInstance);
|
||||
}
|
||||
} else {
|
||||
throw new InvalidExpressionError(expression.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts binding information from arguments in calling
|
||||
///
|
||||
/// const/new Provider(token, ...)
|
||||
/// provider(token, ...)
|
||||
void _handleBindingArgs(
|
||||
NodeList<Expression> args, BindingInstance bindingInstance) {
|
||||
// Element 0 is already processed by caller.
|
||||
for (var i = 1; i < args.length; ++i) {
|
||||
if (args[i] is! NamedExpression) {
|
||||
throw new InvalidExpressionError('Invalid parameter ${args[i]} at $i');
|
||||
}
|
||||
|
||||
var namedExpression = args[i] as NamedExpression;
|
||||
var expression = namedExpression.expression;
|
||||
switch (namedExpression.name.label.name) {
|
||||
case 'useClass': // Fallthrough, they share the same format.
|
||||
case 'useExisting':
|
||||
bindingInstance.referencedClasses.add(_extractClassName(expression));
|
||||
break;
|
||||
case 'useValue':
|
||||
if (expression is InstanceCreationExpression) {
|
||||
bindingInstance.referencedClasses
|
||||
.add(extractName(expression.constructorName.type.name));
|
||||
}
|
||||
break;
|
||||
case 'useFactory':
|
||||
break;
|
||||
case 'multi': // Do nothing.
|
||||
break;
|
||||
case 'deps':
|
||||
_addReferencedClasses(expression, bindingInstance);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedError('Unimplemented named expression:'
|
||||
' ${namedExpression.name.label.name}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
/// Base class for binding information.
|
||||
abstract class BindingInfo {
|
||||
/// String representation of this binding.
|
||||
String get expression;
|
||||
}
|
||||
|
||||
/// Expression that creates a Provider.
|
||||
class BindingInstance extends BindingInfo {
|
||||
final String className;
|
||||
final String creationExpression;
|
||||
|
||||
/// Classes used in [creationExpression].
|
||||
final Set<String> referencedClasses = new Set();
|
||||
|
||||
BindingInstance(this.className, this.creationExpression);
|
||||
|
||||
@override
|
||||
String toString() => creationExpression;
|
||||
|
||||
@override
|
||||
String get expression => creationExpression;
|
||||
}
|
||||
|
||||
/// Models binding information.
|
||||
class ModuleInfo extends BindingInfo {
|
||||
/// Uri of file that contains this binding module.
|
||||
String uri;
|
||||
|
||||
/// Name of this module, null if there is no name.
|
||||
String name;
|
||||
|
||||
/// Raw data of bindings in this module.
|
||||
///
|
||||
/// Elements in this list have the same order where the module/bindings
|
||||
/// was defined. Elements in this list can only be
|
||||
/// 1. String
|
||||
/// 2. BindingInstance
|
||||
/// If element is String, it may be a
|
||||
/// a. class name
|
||||
/// b. another binding variable
|
||||
/// c. an OpaqueToken
|
||||
List<Object> directChildren = [];
|
||||
|
||||
// All binding instances of this module.
|
||||
List<BindingInstance> _allBindingInstances;
|
||||
|
||||
/// Expands binding information in this module.
|
||||
List<BindingInstance> getAllBindingInstances(
|
||||
Map<String, ModuleInfo> allModules) {
|
||||
if (_allBindingInstances != null) {
|
||||
return _allBindingInstances;
|
||||
}
|
||||
|
||||
_allBindingInstances = [];
|
||||
|
||||
for (var binding in directChildren) {
|
||||
if (binding is String) {
|
||||
if (allModules[binding] == null) {
|
||||
// This is a class or an OpaqueToken.
|
||||
_allBindingInstances.add(new BindingInstance(binding, binding));
|
||||
} else {
|
||||
_allBindingInstances
|
||||
.addAll(allModules[binding].getAllBindingInstances(allModules));
|
||||
}
|
||||
} else if (binding is BindingInstance) {
|
||||
_allBindingInstances.add(binding);
|
||||
}
|
||||
}
|
||||
|
||||
return _allBindingInstances;
|
||||
}
|
||||
|
||||
@override
|
||||
String get expression => name;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'package:analyzer/analyzer.dart';
|
||||
|
||||
import 'binding_helper.dart';
|
||||
import 'binding_info.dart';
|
||||
|
||||
/// Collects all binding info declared as top level variable.
|
||||
class BindingVisitor extends RecursiveAstVisitor {
|
||||
/// Maps module name to module info.
|
||||
final Map<String, ModuleInfo> _bindingInfo;
|
||||
|
||||
/// Maps an internal URI to a public one.
|
||||
final Map<String, String> _publicUris;
|
||||
|
||||
/// All binding variables used as providers in @Component
|
||||
final Set<String> _bindingVariablesInComponents;
|
||||
|
||||
// Uri of Dart file currently visited.
|
||||
final String _uri;
|
||||
|
||||
BindingVisitor(this._uri, this._bindingInfo, this._publicUris,
|
||||
this._bindingVariablesInComponents);
|
||||
|
||||
@override
|
||||
visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
|
||||
if (node.variables.variables.length != 1) return;
|
||||
|
||||
var variable = node.variables.variables[0];
|
||||
var name = variable.name.name;
|
||||
if (!_bindingVariablesInComponents.contains(name) &&
|
||||
!name.endsWith('Bindings') &&
|
||||
!name.endsWith('Binding') &&
|
||||
!name.endsWith('Module')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var initializer = variable.initializer;
|
||||
if (initializer is! SimpleIdentifier &&
|
||||
initializer is! PrefixedIdentifier &&
|
||||
initializer is! ListLiteral &&
|
||||
initializer is! InstanceCreationExpression &&
|
||||
initializer is! MethodInvocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
var module = _bindingInfo.putIfAbsent(name, () => new ModuleInfo());
|
||||
module.name = name;
|
||||
module.uri = _publicUris[_uri];
|
||||
|
||||
if (initializer is ListLiteral) {
|
||||
extractBindingInfo(initializer, module);
|
||||
} else {
|
||||
processBindingElement(initializer, module);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'binding_info.dart';
|
||||
import 'dart_class_info.dart';
|
||||
|
||||
/// Information about a component.
|
||||
class ComponentInfo {
|
||||
/// Underlying class.
|
||||
DartClassInfo classInfo;
|
||||
|
||||
/// Selector name, same as tag name.
|
||||
String selectorName;
|
||||
|
||||
/// Template (html) path.
|
||||
String templatePath;
|
||||
|
||||
/// HTML template that is set inline in @Component.
|
||||
String inlineTemplate;
|
||||
|
||||
/// For component types, this is a list of all directive classes used
|
||||
/// in the component's template.
|
||||
List<ComponentInfo> templateTypes = [];
|
||||
|
||||
/// Value of providers in @Component.
|
||||
ModuleInfo module;
|
||||
|
||||
ComponentInfo(this.classInfo);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'package:analyzer/analyzer.dart';
|
||||
|
||||
/// Models a Dart class found during analysis.
|
||||
class DartClassInfo {
|
||||
/// Uri of file that contains this Dart class.
|
||||
String uri;
|
||||
|
||||
/// Class name of this class.
|
||||
String className;
|
||||
|
||||
/// The Dart type that this class extends.
|
||||
///
|
||||
/// If it doesn't extend anything, this will be 'null'
|
||||
String extendsType;
|
||||
|
||||
final List<ConstructorParameter> constructorParameters = [];
|
||||
|
||||
final Map<String, DartClassInfo> memberTypes = {};
|
||||
|
||||
final Map<String, List<Annotation>> memberAnnotations = {};
|
||||
|
||||
/// A list of Dart types that this class implements.
|
||||
final List<String> implementsTypes = [];
|
||||
|
||||
DartClassInfo(this.className, [this.uri]);
|
||||
|
||||
/// Gets or creates a member type entry.
|
||||
DartClassInfo getMemberType(String memberName) =>
|
||||
memberTypes.putIfAbsent(memberName, () => null);
|
||||
|
||||
@override
|
||||
String toString() => className == 'dynamic' ? '' : className;
|
||||
}
|
||||
|
||||
/// Parameter used in component constructor.
|
||||
class ConstructorParameter {
|
||||
/// Annotations for the parameter.
|
||||
///
|
||||
/// Examples include things like @Optional(), @Inject() etc.
|
||||
List<String> annotations;
|
||||
|
||||
/// Parameter's type.
|
||||
DartClassInfo type;
|
||||
|
||||
/// Parameter's name.
|
||||
///
|
||||
/// If the parameter is of form 'this.xxx', [name] will xxx since it's what's
|
||||
/// visible to the caller.
|
||||
String name;
|
||||
|
||||
String _dependency;
|
||||
|
||||
/// Dependency to be used when building dependency graph.
|
||||
///
|
||||
/// This can be the parameter's type or the @Inject token used.
|
||||
String get dependency => _dependency ?? type.className;
|
||||
|
||||
ConstructorParameter(this.annotations, this.type, this.name,
|
||||
[this._dependency]);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'package:analyzer/analyzer.dart';
|
||||
|
||||
import '../app_logger.dart';
|
||||
import '../exceptions.dart';
|
||||
import 'dart_class_info.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
/// Visitor to get dependencies of a class.
|
||||
///
|
||||
/// Types used in constructor are collected. Type info of some variables in
|
||||
/// constructor are only available in field definitions, which are collected
|
||||
/// by visiting field declarations.
|
||||
class DartClassVisitor extends RecursiveAstVisitor {
|
||||
/// Maps class name to class info.
|
||||
final Map<String, DartClassInfo> _dartClasses;
|
||||
|
||||
/// Maps an internal URI to a public one.
|
||||
final Map<String, String> _publicUris;
|
||||
|
||||
/// Uri of Dart file currently visited.
|
||||
String _uri;
|
||||
|
||||
DartClassVisitor(this._uri, this._dartClasses, this._publicUris);
|
||||
|
||||
/// Visits top level variable to collect OpaqueToken.
|
||||
///
|
||||
/// Top level OpaqueToken is treated as dart class. It is something we need
|
||||
/// to provide bindings.
|
||||
@override
|
||||
visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
|
||||
if (node.variables.variables.length != 1) return;
|
||||
|
||||
var variable = node.variables.variables[0];
|
||||
if (variable.initializer is! InstanceCreationExpression) return;
|
||||
|
||||
var initializer = variable.initializer as InstanceCreationExpression;
|
||||
if (extractName(initializer.constructorName.type.name) == 'OpaqueToken') {
|
||||
var tokenInfo = _getClass(variable.name.name);
|
||||
tokenInfo.uri = _publicUris[_uri];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitClassDeclaration(ClassDeclaration classDeclaration) {
|
||||
var classInfo = _getClass(className(classDeclaration));
|
||||
|
||||
// Only first appeared class is used to get more accurate matching.
|
||||
if (classInfo.uri != null) return;
|
||||
|
||||
classInfo.uri = _publicUris[_uri];
|
||||
|
||||
var extendsClause = classDeclaration.extendsClause;
|
||||
if (extendsClause != null) {
|
||||
classInfo.extendsType = extractName(extendsClause.superclass.name);
|
||||
}
|
||||
|
||||
var implementsClause = classDeclaration.implementsClause;
|
||||
if (implementsClause != null) {
|
||||
for (var typeName in implementsClause.interfaces) {
|
||||
classInfo.implementsTypes.add(extractName(typeName.name));
|
||||
}
|
||||
}
|
||||
|
||||
classDeclaration.visitChildren(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitConstructorDeclaration(ConstructorDeclaration constructor) {
|
||||
// Only unnamed constructors.
|
||||
if (constructor.name != null) return;
|
||||
var classInfo = _classInfo(constructor);
|
||||
|
||||
if (classInfo.constructorParameters.isNotEmpty) {
|
||||
AppLogger.log.fine('Duplicate constructor visit: $constructor');
|
||||
return;
|
||||
}
|
||||
|
||||
for (var parameter in constructor.parameters.parameters) {
|
||||
// Annotations like ViewQuery, ViewChild, and ViewChildren
|
||||
// will use a type like QueryList<SomeComponent>, such dependency can
|
||||
// be handled by flutter.
|
||||
var isOptional = parameter.metadata.any((annotation) {
|
||||
var name = extractName(annotation.name);
|
||||
return name == 'Optional' || name == 'SkipSelf';
|
||||
});
|
||||
|
||||
if (isOptional) continue;
|
||||
|
||||
if (parameter is SimpleFormalParameter ||
|
||||
parameter is FieldFormalParameter) {
|
||||
final tokenName = _extractAnnotatedType(parameter.metadata);
|
||||
final annotations = parameter.metadata
|
||||
.map((annotation) => annotation.toString())
|
||||
.toList();
|
||||
|
||||
final parameterType = _getParameterType(parameter, classInfo);
|
||||
|
||||
classInfo.constructorParameters.add(new ConstructorParameter(
|
||||
annotations, parameterType, parameter.identifier.name, tokenName));
|
||||
} else if (parameter is DefaultFormalParameter ||
|
||||
parameter is FunctionTypedFormalParameter) {
|
||||
// Defaults are not supported in DI.
|
||||
} else {
|
||||
throw new UnsupportedError(
|
||||
'Unable to handle parameter ${parameter.runtimeType} '
|
||||
'for ${className(constructor.parent)} in $_uri.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets parameter type.
|
||||
///
|
||||
/// Different types of parameter can have different ways to extract type.
|
||||
/// Currently we support [SimpleFormalParameter] and [FieldFormalParameter].
|
||||
/// When we can't handle the parameter or the type is implicit, we will return
|
||||
/// [DartClassInfo] object that represents 'dynamic'.
|
||||
DartClassInfo _getParameterType(
|
||||
FormalParameter parameter, DartClassInfo classInfo) {
|
||||
if (parameter is SimpleFormalParameter) {
|
||||
return _getClassForTypeAnnotation(parameter.type);
|
||||
} else if (parameter is FieldFormalParameter) {
|
||||
if (parameter.type != null) {
|
||||
// Even if this is FieldFormalParameter, it can also have type info.
|
||||
return _getClassForTypeAnnotation(parameter.type);
|
||||
} else {
|
||||
// The actual type will be filled when the corresponding member is
|
||||
// visited.
|
||||
return classInfo.getMemberType(parameter.identifier.name);
|
||||
}
|
||||
}
|
||||
|
||||
return _getClass('dynamic');
|
||||
}
|
||||
|
||||
@override
|
||||
void visitFieldDeclaration(FieldDeclaration field) {
|
||||
if (field.parent is! ClassDeclaration) return;
|
||||
var classInfo = _classInfo(field);
|
||||
|
||||
var typeOfField = field.fields.type;
|
||||
for (var variable in field.fields.variables) {
|
||||
bool isReferenced = classInfo.memberTypes.containsKey(variable.name.name);
|
||||
if (typeOfField != null) {
|
||||
classInfo.memberTypes[variable.name.name] =
|
||||
_getClass(typeOfField.toString());
|
||||
} else if (variable.initializer is InstanceCreationExpression) {
|
||||
classInfo.memberTypes[variable.name.name] =
|
||||
_getClass(extractConstructorName(variable.initializer));
|
||||
} else {
|
||||
classInfo.memberTypes[variable.name.name] = _getClass('dynamic');
|
||||
}
|
||||
|
||||
if (isReferenced) {
|
||||
for (final parameter in classInfo.constructorParameters) {
|
||||
if (parameter.name == variable.name.name) {
|
||||
parameter.type = classInfo.memberTypes[variable.name.name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
classInfo.memberAnnotations[variable.name.name] = field.metadata;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitMethodDeclaration(MethodDeclaration method) {
|
||||
if (method.parent is! ClassDeclaration) return;
|
||||
if (!method.isSetter) return;
|
||||
|
||||
var classInfo = _classInfo(method);
|
||||
|
||||
var parameter = method.parameters.parameters[0];
|
||||
|
||||
if (parameter is! SimpleFormalParameter) {
|
||||
AppLogger.log
|
||||
.fine('Unimplemented parameter type ${parameter.runtimeType} '
|
||||
'from $parameter');
|
||||
return;
|
||||
}
|
||||
|
||||
var setterParameter = parameter as SimpleFormalParameter;
|
||||
|
||||
// This member won't be referenced in constructor.
|
||||
classInfo.memberTypes[method.name.name] =
|
||||
_getClassForTypeAnnotation(setterParameter.type);
|
||||
classInfo.memberAnnotations[method.name.name] = method.metadata;
|
||||
}
|
||||
|
||||
/// Pattern to extract generic class.
|
||||
///
|
||||
/// For example, for class 'List<InnerType>', the group 1 of this class will
|
||||
/// be 'InnerType'.
|
||||
static final RegExp _genericClassPattern =
|
||||
new RegExp(r'^[a-zA-Z_0-9]*<(.*)>$');
|
||||
|
||||
/// Gets [DartClassInfo] for [className].
|
||||
///
|
||||
/// If the class is generic, the inner most class's library path will be used,
|
||||
/// which is more likely to be a special import requirement.
|
||||
/// There can be cases where [className] is like 'A, B', which comes from the
|
||||
/// type parameters inside a generic class. For now, we simply treat 'A, B' as
|
||||
/// a real class.
|
||||
DartClassInfo _getClass(String className) {
|
||||
if (!_dartClasses.containsKey(className)) {
|
||||
final match = _genericClassPattern.firstMatch(className);
|
||||
if (match != null) {
|
||||
final innerClass = _getClass(match.group(1));
|
||||
return _dartClasses[className] =
|
||||
new DartClassInfo(className, innerClass.uri);
|
||||
}
|
||||
|
||||
_dartClasses[className] = new DartClassInfo(className);
|
||||
}
|
||||
return _dartClasses[className];
|
||||
}
|
||||
|
||||
/// Gets [DartClassInfo] for a [TypeAnnotation].
|
||||
DartClassInfo _getClassForTypeAnnotation(TypeAnnotation type) {
|
||||
if (type == null) return _getClass('dynamic');
|
||||
|
||||
return _getClass(type.toString());
|
||||
}
|
||||
|
||||
DartClassInfo _classInfo(AstNode node) => _getClass(className(node.parent));
|
||||
|
||||
String _extractAnnotatedType(NodeList<Annotation> metadata) {
|
||||
if (metadata == null || metadata.length != 1) return null;
|
||||
|
||||
var name = extractName(metadata[0].name);
|
||||
|
||||
// Returns annotation as type if there is one.
|
||||
if (name != 'Inject') return name;
|
||||
|
||||
var args = metadata[0].arguments.arguments;
|
||||
if (args.length != 1) {
|
||||
throw new InvalidExpressionError('$metadata in $_uri.');
|
||||
}
|
||||
|
||||
var args0 = args[0];
|
||||
if (args0 is Identifier) {
|
||||
return extractName(args0);
|
||||
} else if (args0 is InstanceCreationExpression) {
|
||||
return extractName(args0.constructorName.type.name);
|
||||
} else if (args0 is SimpleStringLiteral) {
|
||||
return args0.value;
|
||||
}
|
||||
|
||||
throw new InvalidExpressionError('$metadata in $_uri.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'package:analyzer/analyzer.dart';
|
||||
|
||||
import '../app_logger.dart';
|
||||
import '../exceptions.dart';
|
||||
import 'binding_helper.dart';
|
||||
import 'binding_info.dart';
|
||||
import 'component_info.dart';
|
||||
import 'dart_class_info.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
/// Visitor to collect value of providers, selector, directives,
|
||||
/// and templateUrl in @Component or @View.
|
||||
class FlutterComponentVisitor extends RecursiveAstVisitor {
|
||||
final Map<String, DartClassInfo> _classes;
|
||||
|
||||
final Map<String, ComponentInfo> _components;
|
||||
|
||||
// Map to save variables that may be used in @Component.
|
||||
final Map<String, Expression> _variables = new Map();
|
||||
|
||||
FlutterComponentVisitor(this._classes, this._components);
|
||||
|
||||
/// Collects top level variables that may be used in @Component.
|
||||
@override
|
||||
void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
|
||||
if (node.variables.variables.length != 1) return;
|
||||
var variable = node.variables.variables[0];
|
||||
_variables[variable.name.name] = variable.initializer;
|
||||
}
|
||||
|
||||
@override
|
||||
void visitAnnotation(Annotation annotation) {
|
||||
if (annotation.parent is! ClassDeclaration) return;
|
||||
|
||||
var name = annotation.name.name;
|
||||
if (name == 'Component' || name == 'View') {
|
||||
visitComponent(annotation);
|
||||
}
|
||||
}
|
||||
|
||||
void visitComponent(Annotation annotation) {
|
||||
final name = className(annotation.parent);
|
||||
|
||||
final component = _getComponent(name);
|
||||
|
||||
for (var arg in annotation.arguments.arguments) {
|
||||
if (arg is! NamedExpression) return;
|
||||
var namedExpression = arg as NamedExpression;
|
||||
var key = namedExpression.name.label.name;
|
||||
if (key == 'selector') {
|
||||
component.selectorName = _stringValue(namedExpression.expression);
|
||||
_components.putIfAbsent(component.classInfo.className, () => component);
|
||||
} else if (key == 'providers') {
|
||||
if (namedExpression.expression is ListLiteral) {
|
||||
component.module = new ModuleInfo();
|
||||
extractBindingInfo(namedExpression.expression, component.module);
|
||||
} else if (namedExpression.expression is Identifier) {
|
||||
component.module = new ModuleInfo();
|
||||
processBindingElement(namedExpression.expression, component.module);
|
||||
} else {
|
||||
throw new InvalidExpressionError(annotation.toString());
|
||||
}
|
||||
} else if (key == 'directives') {
|
||||
_extractDirectives(namedExpression, annotation, component);
|
||||
} else if (key == 'templateUrl') {
|
||||
component.templatePath = _stringValue(namedExpression.expression);
|
||||
} else if (key == 'template') {
|
||||
component.inlineTemplate = _stringValue(namedExpression.expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _extractDirectives(NamedExpression namedExpression,
|
||||
Annotation annotation, ComponentInfo component) {
|
||||
ListLiteral directives;
|
||||
var value = namedExpression.expression;
|
||||
if (value is SimpleIdentifier && _variables[value.name] is ListLiteral) {
|
||||
directives = _variables[value.name];
|
||||
} else if (value is ListLiteral) {
|
||||
directives = value;
|
||||
} else {
|
||||
AppLogger.log.warning('Cannot parse variable used'
|
||||
' in directives in $annotation');
|
||||
return;
|
||||
}
|
||||
|
||||
for (var node in directives.elements) {
|
||||
if (node is SimpleIdentifier || node is PrefixedIdentifier) {
|
||||
final templateTypeComponentName = extractName(node);
|
||||
final templateTypeComponent = _getComponent(templateTypeComponentName);
|
||||
component.templateTypes.add(templateTypeComponent);
|
||||
} else {
|
||||
throw new InvalidExpressionError(annotation.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ComponentInfo _getComponent(String name) {
|
||||
if (!_components.containsKey(name)) {
|
||||
if (!_classes.containsKey(name)) {
|
||||
_classes[name] = new DartClassInfo(name);
|
||||
}
|
||||
_components[name] = new ComponentInfo(_classes[name]);
|
||||
}
|
||||
|
||||
return _components[name];
|
||||
}
|
||||
|
||||
String _stringValue(Expression expression) {
|
||||
String value;
|
||||
if (expression is SimpleStringLiteral || expression is AdjacentStrings) {
|
||||
value = (expression as StringLiteral).stringValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'package:analyzer/analyzer.dart';
|
||||
|
||||
import '../app_logger.dart';
|
||||
|
||||
/// Returns the name of a class declaration.
|
||||
String className(ClassDeclaration classDeclaration) =>
|
||||
classDeclaration.name.name;
|
||||
|
||||
/// Extracts name from an Identifier.
|
||||
///
|
||||
/// Returns last part if [id] is PrefixedIdentifier, otherwise returns name.
|
||||
String extractName(Identifier id) {
|
||||
if (id is SimpleIdentifier) {
|
||||
return id.name;
|
||||
} else if (id is PrefixedIdentifier) {
|
||||
return id.identifier.name;
|
||||
} else {
|
||||
AppLogger.log.fine('Unsupported Identifier ${id.runtimeType}');
|
||||
return id.name;
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets name of the constructor (also the class name).
|
||||
String extractConstructorName(InstanceCreationExpression instanceCreationExp) {
|
||||
var constructorId = instanceCreationExp.constructorName.type.name;
|
||||
var constructorName;
|
||||
|
||||
// Work around an issue that constructorName.type.name will
|
||||
// return fixed for new Clock.fixed.
|
||||
if (constructorId is PrefixedIdentifier) {
|
||||
constructorName = constructorId.prefix.name;
|
||||
} else {
|
||||
constructorName = constructorId.name;
|
||||
}
|
||||
return constructorName;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
import 'package:analyzer/analyzer.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../app_logger.dart';
|
||||
import '../path_util.dart';
|
||||
import 'ast_cache.dart';
|
||||
|
||||
/// Used to make analyzing Dart files easier by treating libraries and
|
||||
/// parts as one unit.
|
||||
Map<String, T> visitUris<T>(
|
||||
AstCache asts, AstVisitor visitorFn(String uri, Map<String, T> out)) {
|
||||
var out = <String, T>{};
|
||||
|
||||
for (var uri in asts.allUris) {
|
||||
var visitor = visitorFn(uri, out);
|
||||
new _LibraryAndParts(asts, uri).accept(visitor);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
class _LibraryAndParts {
|
||||
/// Maps URI to parts of this file.
|
||||
static final Map<String, List<String>> _uriParts = {};
|
||||
|
||||
final AstCache _asts;
|
||||
final String _uri;
|
||||
|
||||
_LibraryAndParts(this._asts, this._uri);
|
||||
|
||||
void accept(AstVisitor visitor) {
|
||||
var compilationUnit = _asts.getCompilationUnit(_uri);
|
||||
var isLibraryVisitor = new _IsLibraryVisitor();
|
||||
compilationUnit.accept(isLibraryVisitor);
|
||||
if (!isLibraryVisitor.isLibrary) return;
|
||||
|
||||
compilationUnit.accept(visitor);
|
||||
|
||||
var parts = _uriParts.putIfAbsent(_uri, () {
|
||||
var partVisitor = new _PartVisitor();
|
||||
compilationUnit.accept(partVisitor);
|
||||
return partVisitor.parts;
|
||||
});
|
||||
|
||||
var directoryName = path.posix.dirname(getPath(_uri));
|
||||
var packageName = getPackageName(_uri);
|
||||
|
||||
for (var partName in parts) {
|
||||
var referencedFile =
|
||||
path.posix.normalize(path.posix.join(directoryName, partName));
|
||||
|
||||
var partUri = 'package:${path.posix.join(packageName, referencedFile)}';
|
||||
|
||||
_asts.publicUris[partUri] = _uri;
|
||||
try {
|
||||
var partCompilationUnit = _asts.getCompilationUnit(partUri);
|
||||
partCompilationUnit.accept(visitor);
|
||||
} catch (e) {
|
||||
AppLogger.log.fine('Failed to parse $partUri: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects 'part' names from Dart files.
|
||||
class _PartVisitor extends RecursiveAstVisitor {
|
||||
var parts = <String>[];
|
||||
|
||||
@override
|
||||
visitPartDirective(PartDirective directive) {
|
||||
parts.add(directive.uri.stringValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// Visits Dart files and sets a member field if the file is a
|
||||
/// library (not part of a library).
|
||||
class _IsLibraryVisitor extends RecursiveAstVisitor {
|
||||
bool isLibrary = true;
|
||||
|
||||
@override
|
||||
visitPartOfDirective(_) {
|
||||
isLibrary = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user