I'm writing a code generator for Dart using the build_runner, but my builder is not being called for annotations at fields, although it does work for annotations at classes.
Is it possible to also call the generator for annotations at fields (or at any place for that matter)?
For example, the builder is called for the following file:
import 'package:my_annotation/my_annotation.dart';
part 'example.g.dart';
#MyAnnotation()
class Fruit {
int number;
}
But not for this one:
import 'package:my_annotation/my_annotation.dart';
part 'example.g.dart';
class Fruit {
#MyAnnotation()
int number;
}
Here's the definition of the annotation:
class MyAnnotation {
const MyAnnotation();
}
And this is how the generator is defined. For now, it just aborts whenever it's called, causing an error message to be printed.
library my_annotation_generator;
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:my_annotation/my_annotation.dart';
import 'package:source_gen/source_gen.dart';
Builder generateAnnotation(BuilderOptions options) =>
SharedPartBuilder([MyAnnotationGenerator()], 'my_annotation');
class MyAnnotationGenerator extends GeneratorForAnnotation<MyAnnotation> {
#override
generateForAnnotatedElement(Element element, ConstantReader annotation, _) {
throw CodeGenError('Generating code for annotation is not implemented yet.');
}
Here's the build.yaml configuration:
targets:
$default:
builders:
my_annotation_generator|my_annotation:
enabled: true
builders:
my_annotation:
target: ":my_annotation_generator"
import: "package:my_annotation/my_annotation.dart"
builder_factories: ["generateAnnotation"]
build_extensions: { ".dart": [".my_annotation.g.part"] }
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]
At least from my experience, your file 'example.dart' would need at least one annotation above the class definition to be parsed by GeneratorForAnnotation.
example.dart:
import 'package:my_annotation/my_annotation.dart';
part 'example.g.dart';
#MyAnnotation()
class Fruit {
#MyFieldAnnotation()
int number;
}
To access annotations above class fields or class methods you could use a visitor to "visit" each child element and extract the source code information.
For example, to get information about the class fields you could override the method visitFieldElement and then access any annotations using the getter: element.metadata.
builder.dart:
import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/visitor.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:build/src/builder/build_step.dart';
import 'package:source_gen/source_gen.dart';
import 'package:my_annotation/my_annotation.dart';
class MyAnnotationGenerator extends
GeneratorForAnnotation<MyAnnotation> {
#override
FutureOr<String> generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,){
return _generateSource(element);
}
String _generateSource(Element element) {
var visitor = ModelVisitor();
element.visitChildren(visitor);
return '''
// ${visitor.className}
// ${visitor.fields}
// ${visitor.metaData}
''';
}
}
class ModelVisitor extends SimpleElementVisitor {
DartType className;
Map<String, DartType> fields = {};
Map<String, dynamic> metaData = {};
#override
visitConstructorElement(ConstructorElement element) {
className = element.type.returnType;
}
#override
visitFieldElement(FieldElement element) {
fields[element.name] = element.type;
metaData[element.name] = element.metadata;
}
}
Note: In this example, _generateSource returns a commented statement. Without comments you would need to return well-formed dart source code, otherwise, the builder will terminate with an error.
For more information see:
Source Generation and Writing Your Own Package (The Boring Flutter Development Show, Ep. 22) https://www.youtube.com/watch?v=mYDFOdl-aWM&t=459s
The built-in GeneratorForAnnotation uses the LibraryElement's annotatedWith(...) method, which only checks for top-level annotations.
To also detect annotations on fields, you'll need to write something custom.
Here's the Generator I wrote for my project:
abstract class GeneratorForAnnotatedField<AnnotationType> extends Generator {
/// Returns the annotation of type [AnnotationType] of the given [element],
/// or [null] if it doesn't have any.
DartObject getAnnotation(Element element) {
final annotations =
TypeChecker.fromRuntime(AnnotationType).annotationsOf(element);
if (annotations.isEmpty) {
return null;
}
if (annotations.length > 1) {
throw Exception(
"You tried to add multiple #$AnnotationType() annotations to the "
"same element (${element.name}), but that's not possible.");
}
return annotations.single;
}
#override
String generate(LibraryReader library, BuildStep buildStep) {
final values = <String>{};
for (final element in library.allElements) {
if (element is ClassElement && !element.isEnum) {
for (final field in element.fields) {
final annotation = getAnnotation(field);
if (annotation != null) {
values.add(generateForAnnotatedField(
field,
ConstantReader(annotation),
));
}
}
}
}
return values.join('\n\n');
}
String generateForAnnotatedField(
FieldElement field, ConstantReader annotation);
}
I had a very similar issue trying to target specific methods within my annotated classes. Inspired by your answers I slightly modified the class annotation model_visitor to check the method annotation before selecting elements.
class ClassAnnotationModelVisitor extends SimpleElementVisitor<dynamic> {
String className;
Map<String, String> methods = <String, String>{};
Map<String, String> parameters = <String, String>{};
#override
dynamic visitConstructorElement(ConstructorElement element) {
final elementReturnType = element.type.returnType.toString();
className = elementReturnType.replaceFirst('*', '');
}
#override
dynamic visitMethodElement(MethodElement element) {
if (methodHasAnnotation(MethodAnnotation, element)) {
final functionReturnType = element.type.returnType.toString();
methods[element.name] = functionReturnType.replaceFirst('*', '');
parameters[element.name] = element.parameters.map((e) => e.name).join(' ,');
}
}
bool methodHasAnnotation(Type annotationType, MethodElement element) {
final annotations = TypeChecker.fromRuntime(annotationType).annotationsOf(element);
return !annotations.isEmpty;
}
}
Then, I can use the basic GeneratorForAnnotation class and generate for class and methodsArray.
Related
I want to get TypeAnnotation source to get annotations defined on that type
// file1.dart
#Annoation(name :"hello")
class RType { }
// file2.dart
#Selectors()
class Example {
static Rtype hello() => null;
}
by using ast visitor i am able to get RType ( TypeAnnoation) , but i want to get actual RType and its annotations ..
class SelectorsGenerator extends GeneratorForAnnotation<Selectors> {
AstNode getAstNodeFromElement(Element element) {
AnalysisSession session = element.session;
ParsedLibraryResult parsedLibResult =
session.getParsedLibraryByElement(element.library);
ElementDeclarationResult elDeclarationResult =
parsedLibResult.getElementDeclaration(element);
return elDeclarationResult.node;
}
#override
generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
if (!(element is ClassElement)) {
throw Exception("Selectors should be applied on class only");
}
element = element as ClassElement;
final visitor = ExampleVisitor();
final astNode = getAstNodeFromElement(element);
astNode.visitChildren(visitor);
return """
// Selector
""";
}
}
class ExampleVisitor extends SimpleAstVisitor {
#override
visitMethodDeclaration(MethodDeclaration node) {
final t= node.returnType; //TypeAnnonation
t.type // DartType is null here :(
//TODO i want to get annotations defined on this type
}
}
You shouldn't need to switch to the AST model for this, it should be possible to get the annotation with the Element model.
var methods = classElement.methods;
for (var method in methods) {
var returnType = method.returnType;
var metadata = returnType.element.metadata;
// Do something with the annotation.
}
I try to localize a String in Flutter with the localization package. The problem is the location where my translation is needed. It is not related to the UI, rather it is somewhere deep in my model, where I don't have access to a BuildContext. Is there any other possibility to still make use of the translation function?
// I don't have a context variable here
MyLocalizations.of(context).trans("foo")
Yes there is. You don't need BuildContext to access strings. Here is my solution:
class Strings {
Strings._(Locale locale) : _localeName = locale.toString() {
current = this;
}
final String _localeName;
static Strings current;
static Future<Strings> load(Locale locale) async {
await initializeMessages(locale.toString());
final result = Strings._(locale);
return result;
}
static Strings of(BuildContext context) {
return Localizations.of<Strings>(context, Strings);
}
String get title {
return Intl.message(
'Hello World',
name: 'title',
desc: 'Title for the Demo application',
);
}
}
Future<Null> main() async {
final Locale myLocale = Locale(window.locale);
await Strings.load(myLocale);
runApp(MyApplication());
}
Now you can reference a string as follows:
final title = Strings.current.title;
I know this question is dated way back. But I came across this issue when implementing my application, and I dont see any "nice" way to handle it.
So here is my approach
class LanguageService {
static String defaultLanguage = 'en';
static Map<String, Map<String, String>> _localizedValues = {
'en': {
'title': 'Storefront',
'language': 'Language',
'googleLogin': 'Login with Google'
},
'vn': {
'title': 'Cửa hàng',
'language': 'Ngôn ngữ',
'googleLogin': 'Đăng Nhập với Google'
}
};
static set language(String lang) {
defaultLanguage = lang;
}
static String get title {
return _localizedValues[defaultLanguage]['title'];
}
static String get language {
return _localizedValues[defaultLanguage]['language'];
}
static String get googleLogin {
return _localizedValues[defaultLanguage]['googleLogin'];
}
}
Now you can reference a string as follows:
String title = LanguageService.title;
You can find the detailed tutorial here
AppLocalitzations needs the context.
You can create a class (e.g., Localization) to encapsulate AppLocalizations initialization and initialize it from the home widget using its context. After, can be used with a mixin:
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class Localization {
static AppLocalizations _loc;
AppLocalizations get loc => Localization._loc;
static void init(BuildContext context) => _loc = AppLocalizations.of(context);
}
In the home widget:
...
#override
Widget build(BuildContext context) {
Localization.init(context);
return Scaffold(
...
Access to loc in some class (it isn't necessary to be a Widget) using mixins:
class XXXWidget extends StatelessWidget with Localization {
...
Text(loc.xxxx)
...
}
class _XXXXWidgetState extends State<XXXWidget> with Localization {
...
Text(loc.xxxx)
...
}
class XXXXController with Localization {
...
cardNumberValidator = RequiredValidator(errorText: loc.commons_Required);
...
}
Null safety version:
class Localization {
static AppLocalizations? _l;
AppLocalizations get loc => Localization._l!;
static void init(BuildContext context) => _l = AppLocalizations.of(context)!;
}
No, there is no other way because it is stored using an InheritedWidget, which is a part of the build tree and thus can only be accessed with a reference to it (the BuildContext).
You will need to pass your context to somewhere deep in your model.
I am not sure if i did it right (from performance point of view) and maybe someone can comment on this but i have rx BehaviorSubject in my AppLocalization and fire event once new locales are loaded. I am listening to it in my main.dart and doing setState on receiving an event.
I checked performance tab but did not noticed any big changes in it once comparing my method vs accessing translations through context (inherited widget).
I did the following test, but it doesn't work:
//main.dart
class Test
{
static const a = 10;
final b = 20;
final c = a+1;
}
//part.dart
part of 'main.dart';
class Test
{
final d = a +1; //<---undefined name 'a'
}
I would like to split the class in flutter tutorial into multiple files. For example: _buildSuggestions in a separate file, _buildRow in a separate file, etc.
update:
my solution:
before:
//main.dart
class RandomWordsState extends State<RandomWords> {
{
final _var1;
final _var2;
#override
Widget build(BuildContext context) {
...
body: _buildList(),
);
Widget _buildList() { ... }
Widget _buildRow() { ... }
}
after:
//main.dart
import 'buildlist.dart';
class RandomWordsState extends State<RandomWords> {
{
final var1;
final var2;
#override
Widget build(BuildContext context) {
...
body: buildList(this),
);
}
//buildlist.dart
import 'main.dart';
Widget buildList(RandomWordsState obj) {
... obj.var1 ...
}
I am faced with same problem. My variant based on extensions:
page.dart
part 'section.dart';
class _PageState extends State<Page> {
build(BuildContext context) {
// ...
_buildSection(context);
// ...
}
}
section.dart
part of 'page.dart';
extension Section on _PageState {
_buildSection(BuildContext context) {
// ...
}
}
Dart doesn't support partial classes. part and part of are to split a library into multiple files, not a class.
Private (identifiers starting with _) in Dart is per library which is usually a *.dart file.
main.dart
part 'part.dart';
class Test {
/// When someone tries to create an instance of this class
/// Create an instance of _Test instead
factory Test() = _Test;
/// private constructor that can only be accessed within the same library
Test._();
static const a = 10;
final b = 20;
final c = a+1;
}
part.dart
part of 'main.dart';
class _Test extends Test {
/// private constructor can only be called from within the same library
/// Call the private constructor of the super class
_Test() : super._();
/// static members of other classes need to be prefixed with
/// the class name, even when it is the super class
final d = Test.a +1; //<---undefined name 'a'
}
A similar pattern is used in many code-generation scenarios in Dart like in
https://pub.dartlang.org/packages/built_value
https://pub.dartlang.org/packages/built_redux
https://pub.dartlang.org/packages/json_serializable
and many others.
I just extend it with extension keyword like Swift.
// class_a.dart
class ClassA {}
// class_a+feature_a.dart
import 'class_a.dart';
extension ClassA_FeatureA on ClassA {
String separatedFeatureA() {
// do your job here
}
}
Please ignore the coding conventions, it's just a sample.
I have a list of models that I need to create a mini reflective system.
I analyzed the Serializable package and understood how to create one generated file per file, however, I couldn't find how can I create one file for a bulk of files.
So, how to dynamically generate one file, using source_gen, for a list of files?
Example:
Files
user.dart
category.dart
Generated:
info.dart (containg information from user.dart and category.dart)
Found out how to do it with the help of people in Gitter.
You must have one file, even if empty, to call the generator. In my example, it is lib/batch.dart.
source_gen: ^0.5.8
Here is the working code:
The tool/build.dart
import 'package:build_runner/build_runner.dart';
import 'package:raoni_global/phase.dart';
main() async {
PhaseGroup pg = new PhaseGroup()
..addPhase(batchModelablePhase(const ['lib/batch.dart']));
await build(pg,
deleteFilesByDefault: true);
}
The phase:
batchModelablePhase([Iterable<String> globs =
const ['bin/**.dart', 'web/**.dart', 'lib/**.dart']]) {
return new Phase()
..addAction(
new GeneratorBuilder(const
[const BatchGenerator()], isStandalone: true
),
new InputSet(new PackageGraph.forThisPackage().root.name, globs));
}
The generator:
import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'package:glob/glob.dart';
import 'package:build_runner/build_runner.dart';
class BatchGenerator extends Generator {
final String path;
const BatchGenerator({this.path: 'lib/models/*.dart'});
#override
Future<String> generate(Element element, BuildStep buildStep) async {
// this makes sure we parse one time only
if (element is! LibraryElement)
return null;
String libraryName = 'raoni_global', filePath = 'lib/src/model.dart';
String className = 'Modelable';
// find the files at the path designed
var l = buildStep.findAssets(new Glob(path));
// get the type of annotation that we will use to search classes
var resolver = await buildStep.resolver;
var assetWithAnnotationClass = new AssetId(libraryName, filePath);
var annotationLibrary = resolver.getLibrary(assetWithAnnotationClass);
var exposed = annotationLibrary.getType(className).type;
// the caller library' name
String libName = new PackageGraph.forThisPackage().root.name;
await Future.forEach(l.toList(), (AssetId aid) async {
LibraryElement lib;
try {
lib = resolver.getLibrary(aid);
} catch (e) {}
if (lib != null && Utils.isNotEmpty(lib.name)) {
// all objects within the file
lib.units.forEach((CompilationUnitElement unit) {
// only the types, not methods
unit.types.forEach((ClassElement el) {
// only the ones annotated
if (el.metadata.any((ElementAnnotation ea) =>
ea.computeConstantValue().type == exposed)) {
// use it
}
});
});
}
});
return '''
$libName
''';
}
}
It seems what you want is what this issue is about How to generate one output from many inputs (aggregate builder)?
[Günter]'s answer helped me somewhat.
Buried in that thread is another thread which links to a good example of an aggregating builder:
1https://github.com/matanlurey/build/blob/147083da9b6a6c70c46eb910a3e046239a2a0a6e/docs/writing_an_aggregate_builder.md
The gist is this:
import 'package:build/build.dart';
import 'package:glob/glob.dart';
class AggregatingBuilder implements Builder {
/// Glob of all input files
static final inputFiles = new Glob('lib/**');
#override
Map<String, List<String>> get buildExtensions {
/// '$lib$' is a synthetic input that is used to
/// force the builder to build only once.
return const {'\$lib$': const ['all_files.txt']};
}
#override
Future<void> build(BuildStep buildStep) async {
/// Do some operation on the files
final files = <String>[];
await for (final input in buildStep.findAssets(inputFiles)) {
files.add(input.path);
}
String fileContent = files.join('\n');
/// Write to the file
final outputFile = AssetId(buildStep.inputId.package,'lib/all_files.txt');
return buildStep.writeAsString(outputFile, fileContent);
}
}
I try to customize HyperlinkHelper. So I have override HypertextDetector
package org.xtext.example.mydsl.ui;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.xtext.ui.editor.hyperlinking.DefaultHyperlinkDetector;
import org.eclipse.xtext.ui.editor.hyperlinking.IHyperlinkHelper;
public class MyHyperlinkDetector extends DefaultHyperlinkDetector {
private static final String PREFERENCES = ".hyper";
#Override
public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) {
IDocument document = textViewer.getDocument();
int offset = region.getOffset();
// extract relevant characters
IRegion lineRegion;
String candidate;
try {
lineRegion = document.getLineInformationOfOffset(offset);
candidate = document.get(lineRegion.getOffset(), lineRegion.getLength());
} catch (BadLocationException ex) {
return null;
}
// look for keyword
int index = candidate.indexOf(PREFERENCES);
if (index != -1) {
// detect region containing keyword
IRegion targetRegion = new Region(lineRegion.getOffset() + index, PREFERENCES.length());
if ((targetRegion.getOffset() <= offset)
&& ((targetRegion.getOffset() + targetRegion.getLength()) > offset))
// create link
return new IHyperlink[] { new PreferencesHyperlink(targetRegion, candidate) };
}
return null;
}
#Override
public IHyperlinkHelper getHelper() {
// TODO Auto-generated method stub
return new MyHyperlinkHelper();
}
}
Hyperlink detector is worked, but MyHyperlinkHelper is never created. Even if I comment method detectHyperlinks.
My goal is to open file with name what I have click in my edited dsl grammar. That's why I need HyperlinkHelper. I.e. I need to check does my substring is correct file name.
How to solve it?
Regards,
Vladimir.
dont override the method. simply use guice and call the method from the superclass in your impl
public Class<? extends IHyperlinkHelper> bindIHyperlinkHelper() {
return DomainmodelHyperlinkHelper.class;
}
or in Xtend
def Class<? extends IHyperlinkHelper> bindIHyperlinkHelper() {
return DomainmodelHyperlinkHelper;
}