adding packages
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# Files and directories created by pub
|
||||
.packages
|
||||
.pub/
|
||||
build/
|
||||
packages
|
||||
pubspec.lock
|
||||
|
||||
# Files generated by dart tools
|
||||
.dart_tool
|
||||
doc/api/
|
||||
|
||||
# JetBrains IDEs
|
||||
.idea/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
@@ -0,0 +1,6 @@
|
||||
language: dart
|
||||
dart:
|
||||
- dev
|
||||
script:
|
||||
- dartanalyzer --fatal-warnings --fatal-lints .
|
||||
- pub run test
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"ngflutter"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
# Changelog
|
||||
|
||||
## 0.4.0
|
||||
|
||||
- Fixing Bugs Related to dart2native
|
||||
|
||||
## 0.3.1
|
||||
|
||||
- Added support for Flutter
|
||||
|
||||
## 0.1.2
|
||||
|
||||
- Added support for `generate directive`
|
||||
- Added support for `generate pipe`
|
||||
|
||||
## 0.1.1
|
||||
- Fixed issues on Windows.
|
||||
|
||||
## 0.1.0+3
|
||||
- Exported visitors which can be used to extract info from Dart files.
|
||||
|
||||
## 0.1.0+2
|
||||
- Fixed an issue on README.md formatting.
|
||||
- Enabled travis.
|
||||
|
||||
## 0.1.0+1
|
||||
- Added missing dependencies.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- New implementation, support to create project, generate Flutter component and test.
|
||||
|
||||
## 0.0.2
|
||||
|
||||
- Fixed `scaffold` command so that it uses the correct platform separator. Instead of creating files in the CWD, it creates them in $name/lib and $name/test of the CWD.
|
||||
|
||||
## 0.0.1
|
||||
|
||||
- Added `scaffold` command, which generates all of the files need for a component. This should be run from an empty folder and provider a selector name like `my-foo-bar`.
|
||||
@@ -0,0 +1,23 @@
|
||||
# How to Contribute
|
||||
|
||||
We'd love to accept your patches and contributions to this project. There are
|
||||
just a few small guidelines you need to follow.
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
Contributions to this project must be accompanied by a Contributor License
|
||||
Agreement. You (or your employer) retain the copyright to your contribution,
|
||||
this simply gives us permission to use and redistribute your contributions as
|
||||
part of the project. Head over to <https://cla.developers.google.com/> to see
|
||||
your current agreements on file or to sign a new one.
|
||||
|
||||
You generally only need to submit a CLA once, so if you've already submitted one
|
||||
(even if it was for a different project), you probably don't need to do it
|
||||
again.
|
||||
|
||||
## Code reviews
|
||||
|
||||
All submissions, including submissions by project members, require review. We
|
||||
use GitHub pull requests for this purpose. Consult
|
||||
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
|
||||
information on using pull requests.
|
||||
@@ -0,0 +1,28 @@
|
||||
Copyright 2017, Google Inc.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,85 @@
|
||||
## Flutter CLI
|
||||
|
||||
[](https://pub.dartlang.org/packages/ngflutter)
|
||||
[](https://travis-ci.org/google/ngflutter)
|
||||
|
||||
A command line interface for [Flutter][webdev_flutter].
|
||||
It can scaffold a skeleton Flutter project, component, and test with
|
||||
[page object][page_object].
|
||||
|
||||
## Installation
|
||||
|
||||
To install:
|
||||
|
||||
```bash
|
||||
pub global activate ngflutter
|
||||
pub global activate webdev
|
||||
```
|
||||
|
||||
To update:
|
||||
|
||||
```bash
|
||||
pub global activate ngflutter
|
||||
pub global activate webdev
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
ngflutter help
|
||||
```
|
||||
|
||||
For help on specific command, run `ngflutter help [command name]`
|
||||
For example:
|
||||
|
||||
```bash
|
||||
ngflutter help generate test
|
||||
```
|
||||
|
||||
will show how to use command `generate test`.
|
||||
|
||||
### Generating Flutter project
|
||||
|
||||
```bash
|
||||
ngflutter new project_name
|
||||
cd project_name
|
||||
pub get
|
||||
webdev serve
|
||||
```
|
||||
|
||||
Navigate to `http://localhost:8080` to visit the project you just built.
|
||||
Command following will assume that you are in the root directory of
|
||||
the project.
|
||||
|
||||
### Generating component
|
||||
|
||||
```bash
|
||||
ngflutter generate component AnotherComponent
|
||||
```
|
||||
This command will generate component under folder `lib/`.
|
||||
You can use option `-p` to change the folder.
|
||||
|
||||
|
||||
### Generating test
|
||||
|
||||
```bash
|
||||
ngflutter generate test lib/app_component.dart
|
||||
```
|
||||
|
||||
Command above will generate 2 files. One is page object file
|
||||
and the other one is test file.
|
||||
Test generated is using [flutter_test][pub_flutter_test]
|
||||
and [test][pub_test] package.
|
||||
|
||||
Use command
|
||||
|
||||
```bash
|
||||
pub run build_runner test --fail-on-severe -- -p chrome
|
||||
```
|
||||
|
||||
to run generated test with Chrome.
|
||||
|
||||
[webdev_flutter]: https://webdev.dartlang.org/flutter
|
||||
[page_object]: https://martinfowler.com/bliki/PageObject.html
|
||||
[pub_flutter_test]: https://pub.dartlang.org/packages/flutter_test
|
||||
[pub_test]: https://pub.dartlang.org/packages/test
|
||||
@@ -0,0 +1,3 @@
|
||||
analyzer:
|
||||
exclude:
|
||||
- 'lib/templates/**'
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:reflected_mustache/mustache.dart';
|
||||
|
||||
main() {
|
||||
var source = '''
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class {{className}} extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
var template = new Template(source, name: 'template-filename.html');
|
||||
|
||||
final String output = template.renderString(
|
||||
{'className': 'FlutterWidget'},
|
||||
);
|
||||
|
||||
print(output);
|
||||
}
|
||||
@@ -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 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ngflutter/src/command_runner.dart';
|
||||
import 'package:args/command_runner.dart';
|
||||
|
||||
Future main(List<String> args) async {
|
||||
var runner = new NgDartCommanderRunner();
|
||||
|
||||
try {
|
||||
await runner.run(args);
|
||||
} on UsageException catch (error) {
|
||||
print(error);
|
||||
print(runner.usage);
|
||||
// Exit code 64 indicates a usage error.
|
||||
Future.wait([stdout.close(), stderr.close()]).then((_) => exit(64));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{{#pageObjectData.commonDependencies}}
|
||||
import '{{{.}}}';
|
||||
{{/pageObjectData.commonDependencies}}
|
||||
|
||||
class {{poClassName}} {
|
||||
{{#pageObjectData.variables}}
|
||||
|
||||
{{{internalString}}}
|
||||
|
||||
{{/pageObjectData.variables}}
|
||||
{{#pageObjectData.variables}}
|
||||
|
||||
{{{getterString}}}
|
||||
|
||||
{{/pageObjectData.variables}}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Exceptions to above rules.
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
@@ -0,0 +1,10 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: ec1044a8773e31b4630bf162d9c374236ad1eaaf
|
||||
channel: master
|
||||
|
||||
project_type: app
|
||||
@@ -0,0 +1,16 @@
|
||||
# {{name}}
|
||||
|
||||
{{description}}
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
|
||||
|
||||
For help getting started with Flutter, view our
|
||||
[online documentation](https://flutter.dev/docs), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
@@ -0,0 +1,17 @@
|
||||
analyzer:
|
||||
exclude: [build/**]
|
||||
errors:
|
||||
uri_has_not_been_generated: ignore
|
||||
plugins:
|
||||
- flutter
|
||||
|
||||
# Lint rules and documentation, see http://dart-lang.github.io/linter/lints
|
||||
linter:
|
||||
rules:
|
||||
- cancel_subscriptions
|
||||
- hash_and_equals
|
||||
- iterable_contains_unrelated_type
|
||||
- list_remove_unrelated_type
|
||||
- test_types_in_equals
|
||||
- unrelated_type_equality_checks
|
||||
- valid_regexps
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:{{{name}}}/ui/home/screen.dart' as ng;
|
||||
|
||||
void main() => runApp(MyApp());
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: '{{name}}',
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
home: ng.HomeScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
name: {{name}}
|
||||
description: {{description}}
|
||||
version: 1.0.0+1
|
||||
environment:
|
||||
sdk: ">=2.1.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
cupertino_icons: ^0.1.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
@@ -0,0 +1,30 @@
|
||||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility that Flutter provides. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:flutter_project/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(MyApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="{{description}}">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<link rel="icon" type="image/png" href="favicon.png">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="flutter_project">
|
||||
<link rel="apple-touch-icon" href="/icons/Icon-192.png">
|
||||
|
||||
<title>{{name}}</title>
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<!-- This script installs service_worker.js to provide PWA functionality to
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/flutter_service_worker.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="main.dart.js" type="application/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "{{name}}",
|
||||
"short_name": "{{name}}",
|
||||
"start_url": ".",
|
||||
"display": "minimal-ui",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "{{description}}",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
@Tags(const ['{{tag}}'])
|
||||
@TestOn('browser')
|
||||
|
||||
import 'package:flutter/flutter.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '{{{projectModel.componentClassUri}}}';
|
||||
{{#projectModel.referencedUris}}
|
||||
import '{{{.}}}';
|
||||
{{/projectModel.referencedUris}}
|
||||
|
||||
import '{{{poGenerator.poFileName}}}';
|
||||
|
||||
@FlutterEntrypoint()
|
||||
void main() {
|
||||
final testBed = new NgTestBed<{{projectModel.componentClassName}}>();
|
||||
{{#projectModel.needProviders}}
|
||||
testBed.addProviders([
|
||||
// Please add provider for following classes.
|
||||
// For example, if Window is used in your component and you want to
|
||||
// mock it in test:
|
||||
// const Provider(Window, useClass: MockWindow)
|
||||
{{#projectModel.serviceClasses}}
|
||||
{{.}},
|
||||
{{/projectModel.serviceClasses}}
|
||||
]);
|
||||
{{/projectModel.needProviders}}
|
||||
NgTestFixture<{{projectModel.componentClassName}}> fixture;
|
||||
{{poGenerator.poClassName}} pageObject;
|
||||
|
||||
setUp(() async {
|
||||
fixture = await testBed.create();
|
||||
pageObject = await fixture.resolvePageObject({{poGenerator.poClassName}});
|
||||
});
|
||||
|
||||
tearDown(disposeAnyRunningTest);
|
||||
|
||||
test('Default greeting', () async {
|
||||
// Change it: Check content of the page using pageObject.
|
||||
expect(pageObject, isNotNull);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
@override
|
||||
_MyHomePageState createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
|
||||
void _incrementCounter() {
|
||||
setState(() {
|
||||
// This call to setState tells the Flutter framework that something has
|
||||
// changed in this State, which causes it to rerun the build method below
|
||||
// so that the display can reflect the updated values. If we changed
|
||||
// _counter without calling setState(), then the build method would not be
|
||||
// called again, and so nothing would appear to happen.
|
||||
_counter++;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text('My Home Page'),
|
||||
),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
// Column is also a layout widget. It takes a list of children and
|
||||
// arranges them vertically. By default, it sizes itself to fit its
|
||||
// children horizontally, and tries to be as tall as its parent.
|
||||
//
|
||||
// Invoke "debug painting" (press "p" in the console, choose the
|
||||
// "Toggle Debug Paint" action from the Flutter Inspector in Android
|
||||
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
|
||||
// to see the wireframe for each widget.
|
||||
//
|
||||
// Column has various properties to control how it sizes itself and
|
||||
// how it positions its children. Here we use mainAxisAlignment to
|
||||
// center the children vertically; the main axis here is the vertical
|
||||
// axis because Columns are vertical (the cross axis would be
|
||||
// horizontal).
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'You have pushed the button this many times:',
|
||||
),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.display1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _incrementCounter,
|
||||
tooltip: 'Increment',
|
||||
child: Icon(Icons.add),
|
||||
), // This trailing comma makes auto-formatting nicer for build methods.
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class {{className}} extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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
|
||||
|
||||
/// Helper classes for extracting info from Dart files.
|
||||
export 'src/visitors/flutter_component_visitor.dart';
|
||||
export 'src/visitors/ast_cache.dart';
|
||||
export 'src/visitors/binding_helper.dart';
|
||||
export 'src/visitors/binding_info.dart';
|
||||
export 'src/visitors/binding_visitor.dart';
|
||||
export 'src/visitors/component_info.dart';
|
||||
export 'src/visitors/dart_class_info.dart';
|
||||
export 'src/visitors/dart_class_visitor.dart';
|
||||
export 'src/visitors/utils.dart';
|
||||
export 'src/visitors/visit_resources.dart';
|
||||
@@ -0,0 +1,27 @@
|
||||
name: ngflutter
|
||||
version: 0.5.0
|
||||
authors:
|
||||
- Tianfei Zhu <tianfei@google.com>
|
||||
- Jing Bian <jingbian@google.com>
|
||||
- Jonah Williams <jonahwilliams@google.com>
|
||||
- Rody Davis <rody.davis.jr@gmail.com>
|
||||
homepage: https://github.com/rodydavis/flutter_cli
|
||||
description: Scaffolding tool for Flutter
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev.69.0 <3.0.0"
|
||||
executables:
|
||||
ngflutter:
|
||||
dependencies:
|
||||
analyzer: ^0.32.4
|
||||
ansicolor: ^1.0.2
|
||||
reflected_mustache: ^1.0.11
|
||||
logging: ^0.11.3+2
|
||||
path: ^1.6.2
|
||||
resource: ^2.1.5
|
||||
args: ^1.5.3
|
||||
recase: ^3.0.0
|
||||
html: ^0.14.0+3
|
||||
dev_dependencies:
|
||||
test: ^1.3.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
@@ -0,0 +1,112 @@
|
||||
// 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 'package:ngflutter/src/file_reader.dart';
|
||||
import 'package:ngflutter/src/package_uri_resolver.dart';
|
||||
import 'package:ngflutter/src/visitors/ast_cache.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('AstCache', () {
|
||||
FileReader.reader = new FileReaderMock();
|
||||
AstCache asts;
|
||||
setUp(() {
|
||||
asts =
|
||||
new AstCache('package:a/a.dart', new PackageUriResolver('.packages'));
|
||||
asts.build();
|
||||
});
|
||||
|
||||
test('should only collect correct URIs', () {
|
||||
var allUris = <String>[];
|
||||
for (var file in _files) {
|
||||
allUris.add(file['uri']);
|
||||
}
|
||||
expect(asts.allUris, equals(allUris));
|
||||
});
|
||||
|
||||
test('should report correct public URIs', () {
|
||||
for (var file in _files) {
|
||||
expect(asts.publicUris[file['uri']], equals(file['public_uri']),
|
||||
reason: "${file['uri']} does not have correct public URI");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var _files = [
|
||||
{
|
||||
'uri': 'package:a/a.dart',
|
||||
'public_uri': 'package:a/a.dart',
|
||||
'path': path.join('a', 'lib', 'a.dart'),
|
||||
'content': '''
|
||||
import 'package:b/b.dart';
|
||||
import 'a1.dart';
|
||||
export 'src/a2.dart';
|
||||
'''
|
||||
},
|
||||
{
|
||||
'uri': 'package:b/b.dart',
|
||||
'public_uri': 'package:b/b.dart',
|
||||
'path': path.join('b', 'lib', 'b.dart'),
|
||||
'content': '''
|
||||
import 'package:flutter/flutter.dart';
|
||||
'''
|
||||
},
|
||||
{
|
||||
'uri': 'package:a/a1.dart',
|
||||
'public_uri': 'package:a/a1.dart',
|
||||
'path': path.join('a', 'lib', 'a1.dart'),
|
||||
'content': '''
|
||||
import 'dart:io';
|
||||
'''
|
||||
},
|
||||
{
|
||||
'uri': 'package:a/src/a2.dart',
|
||||
'public_uri': 'package:a/a.dart',
|
||||
'path': path.join('a', 'lib', 'src', 'a2.dart'),
|
||||
'content': '''
|
||||
import 'a3.dart';
|
||||
export 'a4.dart';
|
||||
'''
|
||||
},
|
||||
{
|
||||
'uri': 'package:a/src/a3.dart',
|
||||
'public_uri': 'package:a/src/a3.dart',
|
||||
'path': path.join('a', 'lib', 'src', 'a3.dart'),
|
||||
'content': '''
|
||||
import 'package:flutter/flutter.dart';
|
||||
'''
|
||||
},
|
||||
{
|
||||
'uri': 'package:a/src/a4.dart',
|
||||
'public_uri': 'package:a/a.dart',
|
||||
'path': path.join('a', 'lib', 'src', 'a4.dart'),
|
||||
'content': '''
|
||||
import 'package:flutter/flutter.dart';
|
||||
'''
|
||||
}
|
||||
];
|
||||
|
||||
var _dotPackages = ['a:a/lib/', 'b:b/lib/'];
|
||||
|
||||
class FileReaderMock implements FileReader {
|
||||
@override
|
||||
List<String> readAsLines(String filePath, {Encoding encoding: utf8}) {
|
||||
if (filePath == '.packages') return _dotPackages;
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String readAsString(String filePath, {Encoding encoding: utf8}) {
|
||||
for (var file in _files) {
|
||||
if (file['path'] == filePath) return file['content'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
// 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:ngflutter/src/visitors/binding_helper.dart';
|
||||
import 'package:ngflutter/src/visitors/binding_info.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('TestBedBindingVisitor', () {
|
||||
_BindingVisitorForTest visitor;
|
||||
|
||||
setUp(() {
|
||||
visitor = new _BindingVisitorForTest();
|
||||
});
|
||||
|
||||
parse(String contents) {
|
||||
parseCompilationUnit(contents).accept(visitor);
|
||||
}
|
||||
|
||||
test('should parse simple binding', () {
|
||||
parse('const x = A;');
|
||||
|
||||
expect(visitor.modules['x'], isNotNull);
|
||||
expect(visitor.modules['x'].directChildren, equals(['A']));
|
||||
});
|
||||
|
||||
test('should parse list bindings', () {
|
||||
parse('const x = [p.A, B];');
|
||||
|
||||
expect(visitor.modules['x'], isNotNull);
|
||||
expect(visitor.modules['x'].directChildren, equals(['B']));
|
||||
});
|
||||
|
||||
test('should parse "const Provider(A)"', () {
|
||||
parse('const x = const Provider(A);');
|
||||
|
||||
expect(visitor.modules['x'], isNotNull);
|
||||
expect(visitor.modules['x'].directChildren.length, 1);
|
||||
var binding = visitor.modules['x'].directChildren[0] as BindingInstance;
|
||||
expect(binding.className, 'A');
|
||||
expect(binding.referencedClasses, isEmpty);
|
||||
});
|
||||
|
||||
test('should parse "const Provider(const A())"', () {
|
||||
parse('const x = const Provider(const A());');
|
||||
|
||||
expect(visitor.modules['x'], isNotNull);
|
||||
expect(visitor.modules['x'].directChildren.length, 1);
|
||||
var binding = visitor.modules['x'].directChildren[0] as BindingInstance;
|
||||
expect(binding.className, 'A');
|
||||
expect(binding.referencedClasses, isEmpty);
|
||||
});
|
||||
|
||||
test('should throw error for "const Provider([A])"', () {
|
||||
expect(() => parse('const x = const Provider([A]);'),
|
||||
throwsUnsupportedError);
|
||||
});
|
||||
|
||||
test('should parse "provide(A, useClass: B)"', () {
|
||||
parse('dynamic x = provide(A, useClass: B);');
|
||||
|
||||
expect(visitor.modules['x'], isNotNull);
|
||||
expect(visitor.modules['x'].directChildren.length, 1);
|
||||
var binding = visitor.modules['x'].directChildren[0] as BindingInstance;
|
||||
expect(binding.className, 'A');
|
||||
expect(binding.referencedClasses, equals(new Set.from(['B'])));
|
||||
});
|
||||
|
||||
test('should parse "provide(const A(), useExisting: B)"', () {
|
||||
parse('dynamic x = provide(const A(), useExisting: B);');
|
||||
|
||||
expect(visitor.modules['x'], isNotNull);
|
||||
expect(visitor.modules['x'].directChildren.length, 1);
|
||||
var binding = visitor.modules['x'].directChildren[0] as BindingInstance;
|
||||
expect(binding.className, 'A');
|
||||
expect(binding.referencedClasses, equals(new Set.from(['B'])));
|
||||
});
|
||||
|
||||
// May need to add support for this scenario.
|
||||
test('should throw error for "provide(A(), toAlias: B)"', () {
|
||||
expect(() => parse('dynamic x = provide(A(), toAlias: B);'),
|
||||
throwsUnsupportedError);
|
||||
});
|
||||
|
||||
test('should parse const Provider(A, useClass: B)', () {
|
||||
var bindingStr = 'const Provider(A, useClass: B)';
|
||||
parse('const x = $bindingStr;');
|
||||
|
||||
expect(visitor.modules['x'], isNotNull);
|
||||
expect(visitor.modules['x'].directChildren.length, 1);
|
||||
var binding = visitor.modules['x'].directChildren[0] as BindingInstance;
|
||||
expect(binding.className, 'A');
|
||||
expect(binding.referencedClasses, equals(new Set.from(['B'])));
|
||||
expect(binding.creationExpression, bindingStr);
|
||||
});
|
||||
|
||||
test('should parse const Provider(A, useFactory: f, deps: const [B, C])',
|
||||
() {
|
||||
var bindingStr = 'const Provider(A, useFactory: f, deps: const [B, C])';
|
||||
parse('const x = $bindingStr;');
|
||||
|
||||
expect(visitor.modules['x'], isNotNull);
|
||||
expect(visitor.modules['x'].directChildren.length, 1);
|
||||
var binding = visitor.modules['x'].directChildren[0] as BindingInstance;
|
||||
expect(binding.className, 'A');
|
||||
expect(binding.referencedClasses, equals(new Set.from(['B', 'C'])));
|
||||
expect(binding.creationExpression, bindingStr);
|
||||
});
|
||||
|
||||
test('should parse const Provider(A, useValue: new B())', () {
|
||||
parse('const x = const Provider(A, useValue: new B());');
|
||||
|
||||
expect(visitor.modules['x'], isNotNull);
|
||||
expect(visitor.modules['x'].directChildren.length, 1);
|
||||
var binding = visitor.modules['x'].directChildren[0] as BindingInstance;
|
||||
expect(binding.className, 'A');
|
||||
expect(binding.referencedClasses, equals(new Set.from(['B'])));
|
||||
});
|
||||
|
||||
test("should parse const Provider('someThing', useValue: new B())", () {
|
||||
parse("const x = const Provider('someThing', useValue: new B());");
|
||||
|
||||
expect(visitor.modules['x'], isNotNull);
|
||||
expect(visitor.modules['x'].directChildren.length, 1);
|
||||
var binding = visitor.modules['x'].directChildren[0] as BindingInstance;
|
||||
expect(binding.className, 'someThing');
|
||||
expect(binding.referencedClasses, equals(new Set.from(['B'])));
|
||||
});
|
||||
|
||||
test('should parse list in deps', () {
|
||||
parse('''
|
||||
const x = const Provider(A,
|
||||
useFactory: f,
|
||||
deps: const [ const [B, const C()]]);
|
||||
''');
|
||||
|
||||
expect(visitor.modules['x'], isNotNull);
|
||||
expect(visitor.modules['x'].directChildren.length, 1);
|
||||
var binding = visitor.modules['x'].directChildren[0] as BindingInstance;
|
||||
expect(binding.className, 'A');
|
||||
expect(binding.referencedClasses, equals(new Set.from(['B'])));
|
||||
});
|
||||
});
|
||||
|
||||
group('ModuleInfo', () {
|
||||
_BindingVisitorForTest visitor;
|
||||
|
||||
setUp(() {
|
||||
visitor = new _BindingVisitorForTest();
|
||||
});
|
||||
|
||||
parse(String contents) {
|
||||
parseCompilationUnit(contents).accept(visitor);
|
||||
}
|
||||
|
||||
void checkExpandedModule(
|
||||
List<BindingInstance> allBindingInstances, List<String> expected) {
|
||||
expect(allBindingInstances, isNotNull);
|
||||
var actual = [];
|
||||
for (var binding in allBindingInstances) {
|
||||
actual.add(binding.className);
|
||||
}
|
||||
|
||||
expect(actual, equals(expected));
|
||||
}
|
||||
|
||||
test('should expand bindings', () {
|
||||
parse('''
|
||||
const a = A;
|
||||
const b = [
|
||||
a,
|
||||
B1,
|
||||
const Provider(B2, useClass: X)
|
||||
];
|
||||
|
||||
dynamic c = [
|
||||
provide(C, useClass: X),
|
||||
b
|
||||
];
|
||||
''');
|
||||
|
||||
expect(visitor.modules['a'], isNotNull);
|
||||
expect(visitor.modules['b'], isNotNull);
|
||||
expect(visitor.modules['c'], isNotNull);
|
||||
|
||||
checkExpandedModule(
|
||||
visitor.modules['a'].getAllBindingInstances(visitor.modules), ['A']);
|
||||
checkExpandedModule(
|
||||
visitor.modules['b'].getAllBindingInstances(visitor.modules),
|
||||
['A', 'B1', 'B2']);
|
||||
checkExpandedModule(
|
||||
visitor.modules['c'].getAllBindingInstances(visitor.modules),
|
||||
['C', 'A', 'B1', 'B2']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _BindingVisitorForTest extends RecursiveAstVisitor {
|
||||
Map<String, ModuleInfo> modules = {};
|
||||
|
||||
_BindingVisitorForTest();
|
||||
|
||||
@override
|
||||
visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
|
||||
var variable = node.variables.variables[0];
|
||||
var name = variable.name.name;
|
||||
var initializer = variable.initializer;
|
||||
|
||||
var module = modules.putIfAbsent(name, () => new ModuleInfo());
|
||||
module.name = name;
|
||||
|
||||
if (initializer is ListLiteral) {
|
||||
extractBindingInfo(initializer, module);
|
||||
} else {
|
||||
processBindingElement(initializer, module);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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:ngflutter/src/visitors/binding_info.dart';
|
||||
import 'package:ngflutter/src/visitors/binding_visitor.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('BindingVisitor', () {
|
||||
Map<String, ModuleInfo> visit(String content) {
|
||||
var compilationUnit = parseCompilationUnit(content);
|
||||
var out = <String, ModuleInfo>{};
|
||||
var visitor = new BindingVisitor('', out, {}, new Set<String>());
|
||||
compilationUnit.accept(visitor);
|
||||
return out;
|
||||
}
|
||||
|
||||
test('should skip misc variable', () {
|
||||
var module = visit('''
|
||||
library a;
|
||||
const xyz = const [A, B, C];
|
||||
''')['xyz'];
|
||||
expect(module, isNull);
|
||||
});
|
||||
|
||||
test('should skip empty initializer', () {
|
||||
var module = visit('''
|
||||
library a;
|
||||
dynamic aModule;
|
||||
''')['aModule'];
|
||||
|
||||
expect(module, isNull);
|
||||
});
|
||||
|
||||
test('should parse list bindings', () {
|
||||
var results = visit('''
|
||||
library a;
|
||||
const testBinding = const [A, B, C];
|
||||
const testModule = D;
|
||||
const someBindings = testModule;
|
||||
''');
|
||||
|
||||
ModuleInfo module = results['testBinding'];
|
||||
expect(module, isNotNull);
|
||||
expect(module.directChildren, equals(['A', 'B', 'C']));
|
||||
module = results['testModule'];
|
||||
expect(module, isNotNull);
|
||||
expect(module.directChildren, equals(['D']));
|
||||
module = results['someBindings'];
|
||||
expect(module, isNotNull);
|
||||
expect(module.directChildren, equals(['testModule']));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
// 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:ngflutter/src/visitors/dart_class_info.dart';
|
||||
import 'package:ngflutter/src/visitors/dart_class_visitor.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('DartClassVisitor', () {
|
||||
Map<String, DartClassInfo> visit(String content) {
|
||||
var compilationUnit = parseCompilationUnit(content);
|
||||
var out = <String, DartClassInfo>{};
|
||||
var visitor = new DartClassVisitor('', out, {});
|
||||
compilationUnit.accept(visitor);
|
||||
return out;
|
||||
}
|
||||
|
||||
test('should collect OpaqueToken', () {
|
||||
var classInfo =
|
||||
visit("const token = const OpaqueToken('token');")['token'];
|
||||
|
||||
expect(classInfo, isNotNull);
|
||||
});
|
||||
|
||||
test('should collect the components constructor types', () {
|
||||
var classInfo = visit("""
|
||||
library x;
|
||||
|
||||
class Cons {
|
||||
Cons(String x, Exotic y);
|
||||
}
|
||||
""")['Cons'];
|
||||
|
||||
expect(
|
||||
classInfo.constructorParameters
|
||||
.map((parameter) => parameter.dependency),
|
||||
equals(['String', 'Exotic']));
|
||||
expect(classInfo.constructorParameters.map((parameter) => parameter.name),
|
||||
equals(['x', 'y']));
|
||||
});
|
||||
|
||||
test('should collect constructor types which reference this', () {
|
||||
var classInfo = visit("""
|
||||
library x;
|
||||
|
||||
class Cons {
|
||||
final String y;
|
||||
Cons(this.y, Exotic z);
|
||||
}
|
||||
""")['Cons'];
|
||||
|
||||
expect(
|
||||
classInfo.constructorParameters
|
||||
.map((parameter) => parameter.dependency),
|
||||
equals(['String', 'Exotic']));
|
||||
expect(classInfo.constructorParameters.map((parameter) => parameter.name),
|
||||
equals(['y', 'z']));
|
||||
});
|
||||
|
||||
test('should collect member type for field declaration', () {
|
||||
var classInfo = visit("""
|
||||
class Cons {
|
||||
final x = new Clock.fixed();
|
||||
List<SomeThing> y;
|
||||
}
|
||||
""")['Cons'];
|
||||
|
||||
expect(classInfo.memberTypes['x'].className, equals('Clock'));
|
||||
expect(classInfo.memberTypes['y'].className, equals('List<SomeThing>'));
|
||||
});
|
||||
|
||||
test(
|
||||
'should collect constructor types which reference this'
|
||||
' and defined after constructor', () {
|
||||
var classInfo = visit("""
|
||||
library x;
|
||||
|
||||
class Cons {
|
||||
final String y;
|
||||
Cons(this.x, this.y, List<Exotic> z);
|
||||
SomeClass x;
|
||||
}
|
||||
""")['Cons'];
|
||||
|
||||
expect(
|
||||
classInfo.constructorParameters
|
||||
.map((parameter) => parameter.dependency),
|
||||
equals(['SomeClass', 'String', 'List<Exotic>']));
|
||||
expect(classInfo.constructorParameters.map((parameter) => parameter.name),
|
||||
equals(['x', 'y', 'z']));
|
||||
});
|
||||
|
||||
test('should collect classes types with implicit constructors', () {
|
||||
var classInfo = visit("""
|
||||
library x;
|
||||
|
||||
class Cons {
|
||||
final String y;
|
||||
}
|
||||
""")['Cons'];
|
||||
|
||||
expect(classInfo.constructorParameters, equals([]));
|
||||
});
|
||||
|
||||
test('shoulde collect extends clauses', () {
|
||||
var classInfo = visit("""
|
||||
library x;
|
||||
|
||||
class Cons extends SuperAwesomeBase {
|
||||
}
|
||||
""")['Cons'];
|
||||
|
||||
expect(classInfo.extendsType, equals('SuperAwesomeBase'));
|
||||
});
|
||||
|
||||
test('should collect implements clauses', () {
|
||||
var classInfo = visit("""
|
||||
library x;
|
||||
|
||||
class Cons implements dull.DullInterface, AwesomeInterface {
|
||||
}
|
||||
""")['Cons'];
|
||||
|
||||
expect(classInfo.implementsTypes,
|
||||
equals(['DullInterface', 'AwesomeInterface']));
|
||||
});
|
||||
|
||||
test('should skip optional parameter', () {
|
||||
var classInfo = visit("""
|
||||
library x;
|
||||
|
||||
class Cons {
|
||||
Cons(@Optional() String x, @SkipSelf() Exotic y);
|
||||
}
|
||||
""")['Cons'];
|
||||
|
||||
expect(classInfo.constructorParameters.isEmpty, true);
|
||||
});
|
||||
|
||||
test('should get type from @Inject(MyString)', () {
|
||||
var classInfo = visit("""
|
||||
library x;
|
||||
|
||||
class Cons {
|
||||
final String y;
|
||||
Cons(@Inject(MyString) String x, @Inject(YString) this.y);
|
||||
}
|
||||
""")['Cons'];
|
||||
|
||||
expect(
|
||||
classInfo.constructorParameters
|
||||
.map((parameter) => parameter.dependency),
|
||||
equals(['MyString', 'YString']));
|
||||
expect(classInfo.constructorParameters.map((parameter) => parameter.name),
|
||||
equals(['x', 'y']));
|
||||
});
|
||||
|
||||
test("should get type from @Inject('someString')", () {
|
||||
var classInfo = visit("""
|
||||
library x;
|
||||
|
||||
class Cons {
|
||||
final String y;
|
||||
Cons(@Inject('someString') String x, @Inject(YString) this.y);
|
||||
}
|
||||
""")['Cons'];
|
||||
|
||||
expect(
|
||||
classInfo.constructorParameters
|
||||
.map((parameter) => parameter.dependency),
|
||||
equals(['someString', 'YString']));
|
||||
expect(classInfo.constructorParameters.map((parameter) => parameter.name),
|
||||
equals(['x', 'y']));
|
||||
});
|
||||
|
||||
test('should get type from @Inject(const MyString())', () {
|
||||
var classInfo = visit("""
|
||||
library x;
|
||||
|
||||
class Cons {
|
||||
final String y;
|
||||
Cons(@Inject(const MyString()) String x,
|
||||
@Inject(const YString()) this.y);
|
||||
}
|
||||
""")['Cons'];
|
||||
|
||||
expect(
|
||||
classInfo.constructorParameters
|
||||
.map((parameter) => parameter.dependency),
|
||||
equals(['MyString', 'YString']));
|
||||
expect(classInfo.constructorParameters.map((parameter) => parameter.name),
|
||||
equals(['x', 'y']));
|
||||
});
|
||||
|
||||
test('should get type from @MyString()', () {
|
||||
var classInfo = visit("""
|
||||
library x;
|
||||
|
||||
class Cons {
|
||||
final String y;
|
||||
Cons(@MyString String x,
|
||||
@YString this.y);
|
||||
}
|
||||
""")['Cons'];
|
||||
|
||||
expect(
|
||||
classInfo.constructorParameters
|
||||
.map((parameter) => parameter.dependency),
|
||||
equals(['MyString', 'YString']));
|
||||
expect(classInfo.constructorParameters.map((parameter) => parameter.name),
|
||||
equals(['x', 'y']));
|
||||
});
|
||||
|
||||
test('should get member type for setter', () {
|
||||
var classInfo = visit("""
|
||||
class Cons {
|
||||
var x;
|
||||
var y;
|
||||
var z;
|
||||
Cons();
|
||||
|
||||
set x(String value){x = value;}
|
||||
set y(value){y = value;}
|
||||
set z(List<String> value){z = value;}
|
||||
}
|
||||
""")['Cons'];
|
||||
|
||||
expect(classInfo.memberTypes['x'].className, equals('String'));
|
||||
expect(classInfo.memberTypes['y'].className, equals('dynamic'));
|
||||
expect(classInfo.memberTypes['z'].className, equals('List<String>'));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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 'package:recase/recase.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('Entity name', () {
|
||||
test('should produce correct formats of names', () {
|
||||
final name = new ReCase('abc_bcd_cde');
|
||||
expect(name.titleCase, 'Abc Bcd Cde');
|
||||
expect(name.camelCase, 'AbcBcdCde');
|
||||
expect(name.camelCase.toLowerCase(), 'abcBcdCde');
|
||||
expect(name.paramCase, 'abc-bcd-cde');
|
||||
expect(name.snakeCase, 'abc_bcd_cde');
|
||||
});
|
||||
test('should be handle to handle different types of input', () {
|
||||
final camelCasedName1 = new ReCase('AbcBcdCde');
|
||||
expect(camelCasedName1.snakeCase, 'abc_bcd_cde');
|
||||
final camelCasedName2 = new ReCase('abcBcdCde');
|
||||
expect(camelCasedName2.snakeCase, 'abc_bcd_cde');
|
||||
final dashedName = new ReCase('abc-bcd-cde');
|
||||
expect(dashedName.snakeCase, 'abc_bcd_cde');
|
||||
});
|
||||
test('should throw for incorrect formats', () {
|
||||
expect(() => new ReCase('Abc-bcd'), throwsArgumentError);
|
||||
expect(() => new ReCase('abc-bcd_cde'), throwsArgumentError);
|
||||
expect(() => new ReCase('_abc'), throwsArgumentError);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// 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:ngflutter/src/visitors/flutter_component_visitor.dart';
|
||||
import 'package:ngflutter/src/visitors/component_info.dart';
|
||||
import 'package:ngflutter/src/visitors/dart_class_info.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('FlutterComponentVisitor', () {
|
||||
Map<String, ComponentInfo> visit(
|
||||
Map<String, DartClassInfo> classes, String content) {
|
||||
var compilationUnit = parseCompilationUnit(content);
|
||||
var out = <String, ComponentInfo>{};
|
||||
var visitor = new FlutterComponentVisitor(classes, out);
|
||||
compilationUnit.accept(visitor);
|
||||
return out;
|
||||
}
|
||||
|
||||
test('should parse an Flutter component', () {
|
||||
final classes = <String, DartClassInfo>{
|
||||
'A': new DartClassInfo('A'),
|
||||
'B': new DartClassInfo('B'),
|
||||
'TestComponent': new DartClassInfo('TestComponent')
|
||||
};
|
||||
|
||||
var component = visit(classes, '''
|
||||
library a;
|
||||
|
||||
@Component(
|
||||
selector: 'test',
|
||||
directives: const [A, B],
|
||||
templateUrl: 'test.html'
|
||||
)
|
||||
class TestComponent {}
|
||||
''')['TestComponent'];
|
||||
|
||||
expect(component, isNotNull);
|
||||
expect(component.selectorName, equals('test'));
|
||||
expect(component.templatePath, equals('test.html'));
|
||||
expect(
|
||||
component.templateTypes
|
||||
.map((templateType) => templateType.classInfo.className),
|
||||
equals(['A', 'B']));
|
||||
});
|
||||
|
||||
test('should collect inline template', () {
|
||||
final classes = <String, DartClassInfo>{
|
||||
'TestComponent': new DartClassInfo('TestComponent')
|
||||
};
|
||||
|
||||
var component = visit(classes, '''
|
||||
library a;
|
||||
|
||||
@Component(
|
||||
selector: 'test',
|
||||
template: '<div></div>'
|
||||
)
|
||||
class TestComponent {}
|
||||
''')['TestComponent'];
|
||||
|
||||
expect(component, isNotNull);
|
||||
expect(component.inlineTemplate, equals('<div></div>'));
|
||||
});
|
||||
|
||||
test('can combine component and view tags values', () {
|
||||
final classes = <String, DartClassInfo>{
|
||||
'A': new DartClassInfo('A'),
|
||||
'B': new DartClassInfo('B'),
|
||||
'TestComponent': new DartClassInfo('TestComponent')
|
||||
};
|
||||
var component = visit(classes, '''
|
||||
library a;
|
||||
|
||||
@Component(
|
||||
selector: 'test'
|
||||
)
|
||||
@View(
|
||||
directives: const [A, B],
|
||||
templateUrl: 'test.html'
|
||||
)
|
||||
class TestComponent {}
|
||||
''')['TestComponent'];
|
||||
|
||||
expect(component, isNotNull);
|
||||
expect(component.selectorName, equals('test'));
|
||||
expect(component.templatePath, equals('test.html'));
|
||||
expect(
|
||||
component.templateTypes
|
||||
.map((templateType) => templateType.classInfo.className),
|
||||
equals(['A', 'B']));
|
||||
});
|
||||
|
||||
test('can parse directives which value is a variable', () {
|
||||
final classes = <String, DartClassInfo>{
|
||||
'A': new DartClassInfo('A'),
|
||||
'B': new DartClassInfo('B'),
|
||||
'GtTestComponent': new DartClassInfo('TestComponent')
|
||||
};
|
||||
var component = visit(classes, '''
|
||||
const myDirectives = const [A, B];
|
||||
@Component(
|
||||
selector: 'gt-test',
|
||||
directives: myDirectives,
|
||||
templateUrl: 'gt_test.html'
|
||||
)
|
||||
class GtTestComponent {}
|
||||
''')['TestComponent'];
|
||||
|
||||
expect(
|
||||
component.templateTypes
|
||||
.map((templateType) => templateType.classInfo.className),
|
||||
equals(['A', 'B']));
|
||||
});
|
||||
|
||||
test('should collect component binding list', () {
|
||||
final classes = <String, DartClassInfo>{
|
||||
'A': new DartClassInfo('A'),
|
||||
'B': new DartClassInfo('B'),
|
||||
'TestComponent': new DartClassInfo('TestComponent')
|
||||
};
|
||||
|
||||
var component = visit(classes, '''
|
||||
library a;
|
||||
|
||||
@Component(
|
||||
providers: const [A, B],
|
||||
selector: 'test'
|
||||
)
|
||||
class TestComponent {}
|
||||
''')['TestComponent'];
|
||||
|
||||
expect(component, isNotNull);
|
||||
expect(component.module, isNotNull);
|
||||
expect(component.module.directChildren, equals(['A', 'B']));
|
||||
});
|
||||
|
||||
test('should collect component binding variable', () {
|
||||
final classes = <String, DartClassInfo>{
|
||||
'A': new DartClassInfo('A'),
|
||||
'TestComponent': new DartClassInfo('TestComponent')
|
||||
};
|
||||
var component = visit(classes, '''
|
||||
library a;
|
||||
|
||||
@Component(
|
||||
providers: A,
|
||||
selector: 'test'
|
||||
)
|
||||
class TestComponent {}
|
||||
''')['TestComponent'];
|
||||
expect(component, isNotNull);
|
||||
expect(component.module, isNotNull);
|
||||
expect(component.module.directChildren, equals(['A']));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// 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 'package:ngflutter/src/app_logger.dart';
|
||||
import 'package:ngflutter/src/command_runner.dart';
|
||||
import 'package:ngflutter/src/file_reader.dart';
|
||||
import 'package:ngflutter/src/file_writer.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('ngflutter', () {
|
||||
AppLoggerMock logger;
|
||||
FileWriterMock writer;
|
||||
FileReader.reader = new FileReaderMock();
|
||||
NgDartCommanderRunner runner;
|
||||
|
||||
setUp(() {
|
||||
AppLogger.log = logger = new AppLoggerMock();
|
||||
FileWriter.writer = writer = new FileWriterMock();
|
||||
runner = new NgDartCommanderRunner();
|
||||
});
|
||||
|
||||
test('should generate test with default path', () async {
|
||||
await runner
|
||||
.run(['generate', 'test', path.join('lib', 'app_component.dart')]);
|
||||
|
||||
expect(writer.filesWritten.length, 2);
|
||||
expect(writer.filesWritten[0].startsWith('test'), isTrue);
|
||||
expect(logger.warningCount, 0);
|
||||
expect(logger.severeCount, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class AppLoggerMock implements AppLogger {
|
||||
int severeCount = 0;
|
||||
int warningCount = 0;
|
||||
bool verbose = false;
|
||||
|
||||
@override
|
||||
void fine(message, [Object error, StackTrace stackTrace]) {}
|
||||
|
||||
@override
|
||||
void info(message, [Object error, StackTrace stackTrace]) {}
|
||||
|
||||
@override
|
||||
void severe(message, [Object error, StackTrace stackTrace]) {
|
||||
++severeCount;
|
||||
}
|
||||
|
||||
@override
|
||||
void warning(message, [Object error, StackTrace stackTrace]) {
|
||||
++warningCount;
|
||||
}
|
||||
|
||||
@override
|
||||
set isVerbose(bool value) {
|
||||
verbose = value;
|
||||
}
|
||||
}
|
||||
|
||||
class FileWriterMock implements FileWriter {
|
||||
List<String> filesWritten = [];
|
||||
FileWriterMock();
|
||||
@override
|
||||
void write(String destination, String content) {
|
||||
filesWritten.add(destination);
|
||||
}
|
||||
}
|
||||
|
||||
var _files = [
|
||||
{
|
||||
'path': path.join('hello_flutter', 'lib', 'app_component.dart'),
|
||||
'content': '''
|
||||
import 'package:flutter/flutter.dart';
|
||||
|
||||
@Component(
|
||||
selector: 'app-component',
|
||||
templateUrl: 'app_component.html')
|
||||
class AppComponent {
|
||||
var name = 'Flutter';
|
||||
}
|
||||
'''
|
||||
},
|
||||
{
|
||||
'path': path.join('lib', 'app_component.html'),
|
||||
'content': '''
|
||||
<h1>Hello Flutter</h1>
|
||||
'''
|
||||
}
|
||||
];
|
||||
|
||||
var _dotPackages = ['hello_flutter:hello_flutter/lib/'];
|
||||
var _pubSpec = ['name: hello_flutter'];
|
||||
|
||||
class FileReaderMock implements FileReader {
|
||||
@override
|
||||
List<String> readAsLines(String filePath, {Encoding encoding: utf8}) {
|
||||
if (filePath == '.packages') {
|
||||
return _dotPackages;
|
||||
} else if (filePath == 'pubspec.yaml') {
|
||||
return _pubSpec;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String readAsString(String filePath, {Encoding encoding: utf8}) {
|
||||
for (var file in _files) {
|
||||
if (file['path'] == filePath) return file['content'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// 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:ngflutter/src/app_logger.dart';
|
||||
import 'package:ngflutter/src/command_runner.dart';
|
||||
import 'package:ngflutter/src/file_writer.dart';
|
||||
import 'package:ngflutter/src/path_util.dart';
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('ngflutter', () {
|
||||
AppLoggerMock logger;
|
||||
FileWriterMock writer;
|
||||
NgDartCommanderRunner runner;
|
||||
|
||||
setUp(() {
|
||||
AppLogger.log = logger = new AppLoggerMock();
|
||||
FileWriter.writer = writer = new FileWriterMock();
|
||||
runner = new NgDartCommanderRunner();
|
||||
});
|
||||
|
||||
test('should fix invalid path', () {
|
||||
expect(getNormalizedPath(r'path/to\some/folder'),
|
||||
path.join('path', 'to', 'some', 'folder'));
|
||||
});
|
||||
|
||||
test('should generate component with default path', () async {
|
||||
await runner.run(['generate', 'component', 'HelloWorldComponent']);
|
||||
|
||||
expect(writer.filesWritten.length, 2);
|
||||
expect(writer.filesWritten[0].startsWith('lib'), isTrue);
|
||||
expect(logger.warningCount, 0);
|
||||
expect(logger.severeCount, 0);
|
||||
});
|
||||
|
||||
test('should generate component with specified path', () async {
|
||||
final componentPath = path.join('some', 'path');
|
||||
await runner.run([
|
||||
'generate',
|
||||
'component',
|
||||
'--path=$componentPath',
|
||||
'HelloWorldComponent'
|
||||
]);
|
||||
expect(writer.filesWritten.length, 2);
|
||||
expect(writer.filesWritten[0].startsWith(componentPath), isTrue);
|
||||
expect(logger.warningCount, 0);
|
||||
expect(logger.severeCount, 0);
|
||||
});
|
||||
|
||||
test('should generate project with default path', () async {
|
||||
final projectPath = path.join('.', 'hello_flutter');
|
||||
await runner.run(['-v', 'new', 'HelloFlutter']);
|
||||
expect(logger.verbose, isTrue);
|
||||
expect(writer.filesWritten.length, 8);
|
||||
expect(writer.filesWritten[0].startsWith(projectPath), isTrue);
|
||||
expect(logger.warningCount, 0);
|
||||
expect(logger.severeCount, 0);
|
||||
});
|
||||
|
||||
test('should generate project with specified path', () async {
|
||||
final projectPath = path.join('some', 'path');
|
||||
await runner.run(['new', 'HelloFlutter', '-p $projectPath']);
|
||||
expect(logger.verbose, isFalse);
|
||||
expect(writer.filesWritten.length, 8);
|
||||
expect(
|
||||
writer.filesWritten[0]
|
||||
.startsWith(path.join(projectPath, 'hello_flutter')),
|
||||
isTrue);
|
||||
expect(logger.warningCount, 0);
|
||||
expect(logger.severeCount, 0);
|
||||
});
|
||||
|
||||
test('should generate directive with default path', () async {
|
||||
await runner.run(['generate', 'directive', 'HelloWorldDirective']);
|
||||
|
||||
expect(writer.filesWritten.length, 1);
|
||||
expect(writer.filesWritten[0].startsWith('lib'), isTrue);
|
||||
expect(logger.warningCount, 0);
|
||||
expect(logger.severeCount, 0);
|
||||
});
|
||||
|
||||
test('should generate directive with specified path', () async {
|
||||
final directivePath = path.join('some', 'path');
|
||||
await runner.run([
|
||||
'generate',
|
||||
'directive',
|
||||
'--path=$directivePath',
|
||||
'HelloWorldDirective'
|
||||
]);
|
||||
expect(writer.filesWritten.length, 1);
|
||||
expect(writer.filesWritten[0].startsWith(directivePath), isTrue);
|
||||
expect(logger.warningCount, 0);
|
||||
expect(logger.severeCount, 0);
|
||||
});
|
||||
|
||||
test('should generate pipe with default path', () async {
|
||||
await runner.run(['generate', 'pipe', 'HelloWorldPipe']);
|
||||
|
||||
expect(writer.filesWritten.length, 1);
|
||||
expect(writer.filesWritten[0].startsWith('lib'), isTrue);
|
||||
expect(logger.warningCount, 0);
|
||||
expect(logger.severeCount, 0);
|
||||
});
|
||||
|
||||
test('should generate pipe with specified path', () async {
|
||||
final directivePath = path.join('some', 'path');
|
||||
await runner
|
||||
.run(['generate', 'pipe', '--path=$directivePath', 'HelloWorldPipe']);
|
||||
expect(writer.filesWritten.length, 1);
|
||||
expect(writer.filesWritten[0].startsWith(directivePath), isTrue);
|
||||
expect(logger.warningCount, 0);
|
||||
expect(logger.severeCount, 0);
|
||||
});
|
||||
|
||||
test('should throw UsageException for missing project name', () {
|
||||
expect(runner.run(['new']), throwsA(const TypeMatcher<UsageException>()));
|
||||
});
|
||||
|
||||
test('should throw UsageException for missing component name', () {
|
||||
expect(runner.run(['generate', 'component']),
|
||||
throwsA(const TypeMatcher<UsageException>()));
|
||||
});
|
||||
|
||||
test('should throw UsageException for missing directive name', () {
|
||||
expect(runner.run(['generate', 'directive']),
|
||||
throwsA(const TypeMatcher<UsageException>()));
|
||||
});
|
||||
|
||||
test('should throw UsageException for missing pipe name', () {
|
||||
expect(runner.run(['generate', 'pipe']),
|
||||
throwsA(const TypeMatcher<UsageException>()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class AppLoggerMock implements AppLogger {
|
||||
int severeCount = 0;
|
||||
int warningCount = 0;
|
||||
bool verbose = false;
|
||||
|
||||
@override
|
||||
void fine(message, [Object error, StackTrace stackTrace]) {}
|
||||
|
||||
@override
|
||||
void info(message, [Object error, StackTrace stackTrace]) {}
|
||||
|
||||
@override
|
||||
void severe(message, [Object error, StackTrace stackTrace]) {
|
||||
++severeCount;
|
||||
}
|
||||
|
||||
@override
|
||||
void warning(message, [Object error, StackTrace stackTrace]) {
|
||||
++warningCount;
|
||||
}
|
||||
|
||||
@override
|
||||
set isVerbose(bool value) {
|
||||
verbose = value;
|
||||
}
|
||||
}
|
||||
|
||||
class FileWriterMock implements FileWriter {
|
||||
List<String> filesWritten = [];
|
||||
FileWriterMock();
|
||||
@override
|
||||
void write(String destination, String content) {
|
||||
filesWritten.add(destination);
|
||||
}
|
||||
}
|
||||
@@ -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:convert';
|
||||
|
||||
import 'package:ngflutter/src/exceptions.dart';
|
||||
import 'package:ngflutter/src/file_reader.dart';
|
||||
import 'package:ngflutter/src/package_uri_resolver.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('PackageUriResolver', () {
|
||||
FileReader.reader = new FileReaderMock();
|
||||
|
||||
PackageUriResolver resolver;
|
||||
|
||||
setUp(() {
|
||||
resolver = new PackageUriResolver('.packages');
|
||||
});
|
||||
|
||||
test('should parse dependent package URI', () {
|
||||
var filePath = resolver.resolve('package:some_package/some_file.dart');
|
||||
expect(
|
||||
filePath,
|
||||
equals(path.join(
|
||||
path.separator,
|
||||
path.join(
|
||||
'home',
|
||||
'someone',
|
||||
'.pub-cache',
|
||||
'hosted',
|
||||
'pub.dartlang.org',
|
||||
'some_package-1.0.0',
|
||||
'lib',
|
||||
'some_file.dart'))));
|
||||
});
|
||||
|
||||
test('should parse current project URI', () {
|
||||
var filePath = resolver.resolve('package:ngflutter/some_file.dart');
|
||||
expect(filePath, equals(path.join('lib', 'some_file.dart')));
|
||||
});
|
||||
|
||||
test('should throw for unknow package', () {
|
||||
expect(() => resolver.resolve('package:unknown/some_file.dart'),
|
||||
throwsA(const TypeMatcher<UsageException>()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class FileReaderMock implements FileReader {
|
||||
static const List<String> _dotPackages = const [
|
||||
'# Generated by pub on 2017-05-01 00:00:00.00001.',
|
||||
'some_package:file:///home/someone/.pub-cache/hosted/'
|
||||
'pub.dartlang.org/some_package-1.0.0/lib/',
|
||||
'ngflutter:lib/'
|
||||
];
|
||||
|
||||
@override
|
||||
List<String> readAsLines(Object uri, {Encoding encoding: utf8}) {
|
||||
if (uri is String && uri == '.packages') return _dotPackages;
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String readAsString(Object uri, {Encoding encoding: utf8}) => null;
|
||||
}
|
||||
@@ -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 'package:ngflutter/src/page_object_data.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('PageObjectData', () {
|
||||
test('should generate items.', () {
|
||||
var po = new PageObjectData(
|
||||
'<action-button class="good"></action-button>',
|
||||
);
|
||||
expect(po.variables.first.getterString,
|
||||
'Future<PageLoaderElement> get good => _getGood();');
|
||||
expect(po.variables.first.internalString,
|
||||
"@ByClass('good')\n Lazy<PageLoaderElement> _getGood;");
|
||||
expect(po.variables.first.type.uri, 'package:pageloader/objects.dart');
|
||||
});
|
||||
|
||||
test('should generate items in list', () {
|
||||
var po = new PageObjectData(
|
||||
'<action-button class="good" *ngFor="xxx"></action-button>',
|
||||
);
|
||||
expect(po.variables.first.getterString,
|
||||
'Future<List<PageLoaderElement>> get good => _getGood();');
|
||||
expect(po.variables.first.internalString,
|
||||
"@ByClass('good')\n Lazy<List<PageLoaderElement>> _getGood;");
|
||||
});
|
||||
|
||||
test('should generate items in parents list', () {
|
||||
var po = new PageObjectData(
|
||||
'<p *ngFor="xxx"><action-button class="good">'
|
||||
'</action-button></p>',
|
||||
);
|
||||
expect(po.variables.first.getterString,
|
||||
'Future<List<PageLoaderElement>> get good => _getGood();');
|
||||
expect(po.variables.first.internalString,
|
||||
"@ByClass('good')\n Lazy<List<PageLoaderElement>> _getGood;");
|
||||
});
|
||||
|
||||
test('should generate items with default type', () {
|
||||
var po = new PageObjectData('<some-widget class="cool"></some-widget>');
|
||||
expect(po.variables.first.getterString,
|
||||
'Future<PageLoaderElement> get cool => _getCool();');
|
||||
expect(po.variables.first.internalString,
|
||||
"@ByClass('cool')\n Lazy<PageLoaderElement> _getCool;");
|
||||
expect(po.variables.first.type.uri, 'package:pageloader/objects.dart');
|
||||
});
|
||||
|
||||
test('should sort generated items', () {
|
||||
var po1 = new PageObjectData(
|
||||
'<action-button class="good"></action-button>'
|
||||
'<action-button class="bad"></action-button>',
|
||||
);
|
||||
expect(po1.variables.first.name, 'Bad');
|
||||
expect(po1.variables.last.name, 'Good');
|
||||
var po2 = new PageObjectData(
|
||||
'<action-button class="good"></action-button>'
|
||||
'<action-button class="bad"></action-button>',
|
||||
);
|
||||
expect(po2.variables.first.name, 'Bad');
|
||||
expect(po2.variables.last.name, 'Good');
|
||||
});
|
||||
|
||||
test('should ignore some tags.', () {
|
||||
var po = new PageObjectData('<p>123</p>');
|
||||
expect(po.variables.isEmpty, true);
|
||||
});
|
||||
|
||||
test('should add optional annotation.', () {
|
||||
var po = new PageObjectData('<some-widget *ngIf="1"></some-widget>');
|
||||
expect(po.variables[0].internalString, startsWith('@optional'));
|
||||
});
|
||||
|
||||
test('should add optional annotation when parent is optional.', () {
|
||||
var po = new PageObjectData(
|
||||
'<div *ngIf="1"><some-widget></some-widget></div>');
|
||||
expect(po.variables[0].internalString, startsWith('@optional'));
|
||||
});
|
||||
|
||||
test('should add optional annotation when in <template [ngIf]>', () {
|
||||
var po = new PageObjectData(
|
||||
'<template [ngIf]="1"><some-widget></some-widget></template>',
|
||||
);
|
||||
expect(po.variables[0].internalString, startsWith('@optional'));
|
||||
});
|
||||
|
||||
test('should choose correct selector.', () {
|
||||
var po = new PageObjectData(
|
||||
'<some-widget class="cool"></some-widget>'
|
||||
'<some-widget class="cool" id="cooler"></some-widget>'
|
||||
'<some-widget></some-widget>',
|
||||
);
|
||||
expect(po.variables.length, 3);
|
||||
expect(po.variables[0].selector.toString(), "@ByClass('cool')");
|
||||
expect(po.variables[1].selector.toString(), "@ById('cooler')");
|
||||
expect(po.variables[2].selector.toString(), "@ByTagName('some-widget')");
|
||||
});
|
||||
|
||||
test('should work with selectors with attributes', () {
|
||||
var po = new PageObjectData(
|
||||
'<some-cell class="field-class"></some-cell>'
|
||||
'<some-cell id="fieldWithId"></some-cell>',
|
||||
);
|
||||
expect(po.variables[0].selector.toString(), "@ByClass('field-class')");
|
||||
expect(po.variables[1].selector.toString(), "@ById('fieldWithId')");
|
||||
|
||||
expect(po.variables[0].name, 'FieldClass');
|
||||
expect(po.variables[1].name, 'FieldWithId');
|
||||
});
|
||||
|
||||
test('should produce correct commonDependencies.', () {
|
||||
var po1 = new PageObjectData('');
|
||||
expect(po1.commonDependencies, [PageObjectData.pageLoaderDependency]);
|
||||
var po3 = new PageObjectData('<some-widget></some-widget>');
|
||||
expect(po3.commonDependencies, [
|
||||
PageObjectData.pageLoaderDependency,
|
||||
PageObjectData.asyncDependency
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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 'package:ngflutter/src/file_reader.dart';
|
||||
import 'package:ngflutter/src/project_model.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('ProjectModel', () {
|
||||
FileReader.reader = new FileReaderMock();
|
||||
ProjectModel projectModel;
|
||||
setUp(() {
|
||||
projectModel = new ProjectModel(
|
||||
'.packages', 'pubspec.yaml', path.join('lib', 'a.dart'), null);
|
||||
});
|
||||
|
||||
test('shoud export project name', () {
|
||||
expect(projectModel.projectName, equals('a'));
|
||||
});
|
||||
|
||||
test('should export component class URI', () {
|
||||
expect(projectModel.componentClassUri, equals('package:a/a.dart'));
|
||||
});
|
||||
|
||||
test('should export component class name', () {
|
||||
expect(projectModel.componentClassName, equals('TestComponent'));
|
||||
});
|
||||
|
||||
test('should export service classes used', () {
|
||||
expect(projectModel.serviceClasses, equals(['D']));
|
||||
expect(projectModel.needProviders, isTrue);
|
||||
expect(projectModel.referencedUris, equals(['package:a/a.dart']));
|
||||
});
|
||||
|
||||
test('should export dart classes.', () {
|
||||
expect(projectModel.dartClasses.keys.length, equals(5));
|
||||
expect(projectModel.dartClasses.keys.toList(),
|
||||
equals(['TestComponent', 'A', 'C', 'D', 'E']));
|
||||
});
|
||||
|
||||
test('should export component classes.', () {
|
||||
expect(projectModel.components.keys.length, equals(1));
|
||||
expect(projectModel.components.keys.first, equals('TestComponent'));
|
||||
});
|
||||
|
||||
test('should export binding modules.', () {
|
||||
expect(projectModel.modules.length, equals(1));
|
||||
expect(projectModel.modules.keys.first, equals('someThing'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var _files = [
|
||||
{
|
||||
'path': path.join('a', 'lib', 'a.dart'),
|
||||
'content': '''
|
||||
library test_a;
|
||||
|
||||
import 'package:flutter/flutter.dart';
|
||||
import 'a1.dart';
|
||||
|
||||
part 'src/d.dart';
|
||||
|
||||
@Component(
|
||||
selector: 'test-component',
|
||||
providers: const [
|
||||
someThing,
|
||||
const Provider(A, useClass: B)
|
||||
],
|
||||
templateUrl: 'test.html')
|
||||
class TestComponent {
|
||||
A _a;
|
||||
C _c;
|
||||
D _d;
|
||||
E _e;
|
||||
TestComponent(this._a, this._c, this._d, this._e);
|
||||
}
|
||||
'''
|
||||
},
|
||||
{
|
||||
'path': path.join('a', 'lib', 'src', 'd.dart'),
|
||||
'content': '''
|
||||
part of test_a;
|
||||
|
||||
class D{}
|
||||
'''
|
||||
},
|
||||
{
|
||||
'path': path.join('a', 'lib', 'a1.dart'),
|
||||
'content': '''
|
||||
import 'package:flutter/flutter.dart';
|
||||
|
||||
const someThing = const [
|
||||
const Provider(C, useValue: 'test')
|
||||
];
|
||||
'''
|
||||
}
|
||||
];
|
||||
|
||||
var _dotPackages = ['a:a/lib/'];
|
||||
var _pubSpec = ['name: a'];
|
||||
|
||||
class FileReaderMock implements FileReader {
|
||||
@override
|
||||
List<String> readAsLines(String filePath, {Encoding encoding: utf8}) {
|
||||
if (filePath == '.packages') {
|
||||
return _dotPackages;
|
||||
} else if (filePath == 'pubspec.yaml') {
|
||||
return _pubSpec;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String readAsString(String filePath, {Encoding encoding: utf8}) {
|
||||
for (var file in _files) {
|
||||
if (file['path'] == filePath) return file['content'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user