diff --git a/CHANGELOG.md b/CHANGELOG.md index 2299714c..171bf0b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,36 @@ } ``` +* Added `ConstantReader`, a high-level API for reading from constant (static) + values from Dart source code (usually represented by `DartObject` from the + `analyzer` package): + + ```dart + abstract class ConstantReader { + factory ConstantReader(DartObject object) => ... + + // Other methods and properties also exist. + + /// Reads[ field] from the constant as another constant value. + ConstantReader read(String field); + + /// Reads [field] from the constant as a boolean. + /// + /// If the resulting value is `null`, uses [defaultTo] if defined. + bool readBool(String field, {bool defaultTo()}); + + /// Reads [field] from the constant as an int. + /// + /// If the resulting value is `null`, uses [defaultTo] if defined. + int readInt(String field, {int defaultTo()}); + + /// Reads [field] from the constant as a string. + /// + /// If the resulting value is `null`, uses [defaultTo] if defined. + String readString(String field, {String defaultTo()}); + } + ``` + ## 0.5.8 * Add `formatOutput` optional parameter to the `GeneratorBuilder` constructor. diff --git a/lib/source_gen.dart b/lib/source_gen.dart index 966bef4a..219a7405 100644 --- a/lib/source_gen.dart +++ b/lib/source_gen.dart @@ -5,6 +5,7 @@ library source_gen; export 'src/builder.dart'; +export 'src/constants.dart' show ConstantReader; export 'src/find_type.dart' show findType; export 'src/generator.dart'; export 'src/generator_for_annotation.dart'; diff --git a/lib/src/constants.dart b/lib/src/constants.dart new file mode 100644 index 00000000..b1b1fae5 --- /dev/null +++ b/lib/src/constants.dart @@ -0,0 +1,128 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/constant/value.dart'; + +/// Returns whether or not [object] is or represents a `null` value. +bool _isNull(DartObject object) => object?.isNull != false; + +/// Similar to [DartObject.getField], but traverses super classes. +/// +/// Returns `null` if ultimately [field] is never found. +DartObject _getFieldRecursive(DartObject object, String field) { + if (_isNull(object)) { + return null; + } + final result = object.getField(field); + if (_isNull(result)) { + return _getFieldRecursive(object.getField('(super)'), field); + } + return result; +} + +/// A wrapper for analyzer's [DartObject] with a predictable high-level API. +/// +/// Unlike [DartObject.getField], all `readX` methods attempt to access super +/// classes for the field value if not found. +abstract class ConstantReader { + factory ConstantReader(DartObject object) => + _isNull(object) ? const _NullConstant() : new _Constant(object); + + /// Returns whether this constant represents a `bool` literal. + bool get isBool; + + /// Returns this constant as a `bool` value. + bool get boolValue; + + /// Returns whether this constant represents an `int` literal. + bool get isInt; + + /// Returns this constant as an `int` value. + /// + /// Throws [FormatException] if [isInt] is `false`. + int get intValue; + + /// Returns whether this constant represents a `String` literal. + /// + /// If `true`, [stringValue] will return a `String` (not throw). + bool get isString; + + /// Returns this constant as an `String` value. + /// + /// Throws [FormatException] if [isString] is `false`. + String get stringValue; + + /// Returns whether this constant represents `null`. + bool get isNull; + + /// Reads[ field] from the constant as another constant value. + ConstantReader read(String field); +} + +/// Implements a [ConstantReader] representing a `null` value. +class _NullConstant implements ConstantReader { + const _NullConstant(); + + @override + bool get boolValue => throw new FormatException('Not a bool', 'null'); + + @override + int get intValue => throw new FormatException('Not an int', 'null'); + + @override + String get stringValue => throw new FormatException('Not a String', 'null'); + + @override + bool get isBool => false; + + @override + bool get isInt => false; + + @override + bool get isNull => true; + + @override + bool get isString => false; + + @override + ConstantReader read(_) => this; +} + +/// Default implementation of [ConstantReader]. +class _Constant implements ConstantReader { + final DartObject _object; + + const _Constant(this._object); + + @override + bool get boolValue => isBool + ? _object.toBoolValue() + : throw new FormatException('Not a bool', _object); + + @override + int get intValue => isInt + ? _object.toIntValue() + : throw new FormatException('Not an int', _object); + + @override + String get stringValue => isString + ? _object.toStringValue() + : throw new FormatException('Not a String', _object); + + @override + bool get isBool => _object.toBoolValue() != null; + + @override + bool get isInt => _object.toIntValue() != null; + + @override + bool get isNull => _isNull(_object); + + @override + bool get isString => _object.toStringValue() != null; + + @override + ConstantReader read(String field) => + new ConstantReader(_getFieldRecursive(_object, field)); +} diff --git a/test/constants_test.dart b/test/constants_test.dart new file mode 100644 index 00000000..81d7511c --- /dev/null +++ b/test/constants_test.dart @@ -0,0 +1,92 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:build_test/build_test.dart'; +import 'package:source_gen/source_gen.dart'; +import 'package:test/test.dart'; + +void main() { + group('Constant', () { + List constants; + + setUpAll(() async { + final resolver = await resolveSource(r''' + library test_lib; + + const aString = 'Hello'; + const aInt = 1234; + const aBool = true; + const aNull = null; + + @aString // [0] + @aInt // [1] + @aBool // [2] + @aNull // [3] + @Example( // [4] + aString: aString, + aInt: aInt, + aBool: aBool, + aNull: aNull, + nested: const Exampe(), + ) + @Super() // [5] + class Example { + final String aString; + final int aInt; + final bool aBool; + final Example nested; + + const Example({this.aString, this.aInt, this.aBool, this.nested}); + } + + class Super extends Example { + const Super() : super(aString: 'Super Hello'); + } + '''); + constants = resolver + .getLibraryByName('test_lib') + .getType('Example') + .metadata + .map((e) => new ConstantReader(e.computeConstantValue())) + .toList(); + }); + + test('should read a String', () { + expect(constants[0].isString, isTrue); + expect(constants[0].stringValue, 'Hello'); + }); + + test('should read an Int', () { + expect(constants[1].isInt, isTrue); + expect(constants[1].intValue, 1234); + }); + + test('should read a Bool', () { + expect(constants[2].isBool, isTrue); + expect(constants[2].boolValue, true); + }); + + test('should read a Null', () { + expect(constants[3].isNull, isTrue); + }); + + test('should read an arbitrary object', () { + final constant = constants[4]; + expect(constant.read('aString').stringValue, 'Hello'); + expect(constant.read('aInt').intValue, 1234); + expect(constant.read('aBool').boolValue, true); + expect(constant.read('aNull').isNull, isTrue); + + final nested = constant.read('nested'); + expect(nested.read('aString').isNull, isTrue); + expect(nested.read('aInt').isNull, isTrue); + expect(nested.read('aBool').isNull, isTrue); + }); + + test('should read from a super object', () { + final constant = constants[5]; + expect(constant.read('aString').stringValue, 'Super Hello'); + }); + }); +}