Tuesday, January 1, 2013

I Love Dart Annotations

‹prev | My Chain | next›

When I first played with annotations in Dart, they seemed neat and all, but I could think of no practical uses. I realize now that I use annotation-like conventions all the time in Rails code—the dreaded FIXME (and its TODO and OPTIMIZE cousins). In fact, in Rails, these are more than conventions, Rails includes rake machinery to extract these comments:
$ rake notes:fixme

app/models/user.rb:
  * [1140] There should be fewer than 1140 lines in this class
  ...
It did not occur to me at first, but, thanks to annotations, "FIXME" notes in Dart can be more than conventions as well. To demonstrate, I create a FixMe constant constructor:
class FixMe {
  final String note, author, date;
  const FixMe(this.note, {this.author, this.date});
}
Using them is a simple matter of including the annotation syntax in my code:
main() {
  @FixMe("Moar better code")
  var cookie = new Cookie();
  print("cookie has ${cookie.number_of_chips} chips");
}
That does not do much of anything just yet. Presumably, in the future, Dart will include mechanisms to interact with the AST in order to extract those easily.

There is a way that I can use annotations today. At times in the code for Dart for Hipsters I include intentionally broken code to demonstrate errors:
class ComicsView {
  // Private because it starts with underscore
  ComicsCollection _collection;
}

main() {
  group("[private]", (){
    test('has no setter', (){
      var view = new ComicsView();

      assign_non_existent_setter() {
        view.collection = new ComicsCollection();
      };

      expect(
        assign_non_existent_setter,
        throwsNoSuchMethodError
      );
    });
  });
}
This unit test passes just fine thanks to the throwsNoSuchMethodError matcher. But when I run dart_analyzer against my code, it catches this as an error:
classes git:(master) ✗ dart_analyzer test.dart
file:/home/chris/repos/csdart/Book/code/classes/private.dart:25:31: Field 'collection' has no setter
    24:       assign_non_existent_setter() {
    25:         view.collection = new ComicsCollection();
                     ~~~~~~~~~~
That is not a huge deal, but when I have hundreds of tests that help me identify parts of the book that are out of sync with the Dart language, I want a way to ignore intentional errors at a glance. Enter my very own annotations.dart library for internal use in my book:
library annotations;

/// Use when intentionally causing an error.
///
///     @IntentionalError('Demo when semi-colon is omitted')
///     var foo = "bar"
class IntentionalError {
  final String note;
  const IntentionalError([this.note]);
}
Now, I can import that library and put it to some use:
library private_snippet;

import 'package:unittest/unittest.dart';
import '../annotations.dart';

// ...

main() {
  group("[private]", (){
    test('has no setter', (){
      // ...
      assign_non_existent_setter() {
        @IntentionalError('Demonstrate non-existent setter call')
        var collection = view.collection = new ComicsCollection();
      };
      // ...
    });
  });
}
Now, when I run my code, the tests still pass:
classes git:(master) ✗ dart test.dart    
PASS: [private] has no setter
And I can identify that this was intentional with a glance as the dart_analyzer output:
➜  classes git:(master) ✗ dart_analyzer test.dart
file:/home/chris/repos/csdart/Book/code/classes/private.dart:26:31: Field 'collection' has no setter
    25:         @IntentionalError('Demonstrate non-existent setter call')
    26:         var collection = view.collection = new ComicsCollection();
                                      ~~~~~~~~~~
The only drawback was that, due to a limitation in Dart annotations, I had to introduce an unnecessary variable assignment. Still, this seems worth the minor inconvenience if it helps me produce a better book.


Day #617

5 comments:

  1. Also note that there are two 'official' annotations in the package meta (http://pub.dartlang.org/packages/meta, source code is at http://code.google.com/p/dart/source/browse/branches/bleeding_edge/dart/pkg/meta/lib/meta.dart) that are already supported by the Dart Editor, @deprecated and @override.

    ReplyDelete
    Replies
    1. Ooh nice. Even though it's not Emacs, that Dart Editor is pretty darn cool. I may fire it up again and give those annotations a try :)

      Delete
  2. First, without annotation, you showed the following listing:

    classes git:(master) ? dart_analyzer test.dart
    file:/home/chris/repos/csdart/Book/code/classes/private.dart:25:31: Field 'collection' has no setter
    24: assign_non_existent_setter() {
    25: view.collection = new ComicsCollection();


    Then, WITH annotation, you showed the following listing:
    classes git:(master) ? dart_analyzer test.dart
    file:/home/chris/repos/csdart/Book/code/classes/private.dart:26:31: Field 'collection' has no setter
    25: @IntentionalError('Demonstrate non-existent setter call')
    26: var collection = view.collection = new ComicsCollection();

    So I do not see the advantage of an annotation here.
    It seems that the analyzer always shows the respective line plus the line before. Thus it seems that a simple comment would have done too, wouldn't it?

    ReplyDelete
    Replies
    1. It would and it wouldn't. I think annotation > comment for a couple of reasons.

      First, I still hold out hope that someday dart_analyzer will support hooks to identify which custom-built annotations signify ignorable warnings. When/if that day comes, I'll be set.

      Second, the annotation makes it more official. It is possible that I will have comments above bad code in the future. By making it an annotation, I am going out of my way to say that no, this is something different. I can quickly scan warnings for the @ and ignore. But if it's a comment, I'll always have to read it.

      Third, I am formalizing the format. When I come back to the book in 3 months, I will be forced to use the exact same format for these intentional errors. I won't have to guess or grep through code to remember what format that I had used. I will never have to go back and get comments in sync. These annotations will always have exactly the same format. That also makes it easy to grep-exclude warnings should I like, or find-grep to change.

      So in the end, yes, I could, with some extra and ongoing effort, achieve the same thing with comments. But I'm sold on annotations as being better in this case.

      Delete
    2. " I still hold out hope that someday dart_analyzer will support hooks to identify which custom-built annotations signify ignorable warnings."
      ... or will at least recognise a standard set of annotations.

      Ok, great reply. Thanks.

      Delete