Over React Codemods

Pub Build Status

Built with dart_codemod.

Codemods to help consumers of over_react automate the migration of UI component code. Currently, the only use cases are around upgrading from Dart 1 to Dart 2.

Installation

Note: this package requires Dart SDK version 2.1.0 to run, but the codemods themselves are designed to run on code that is written for Dart 1.x or 2.x.

pub global activate over_react_codemod

Once you've activated this package, you should be able to run whichever codemods you need via pub global run.

Dart 1 to Dart 2 Codemod

This package provides a dart2_upgrade codemod that will modify existing over_react component code to be compatible with Dart 2 and the over_react builder.

Depending on your needs, you may be able to upgrade directly from Dart 1 to Dart 2, or you may need to take an intermediary step and provide a version of your codebase that is both forwards- and backwards-compatible. Both of these options are supported by this codemod.

  • pub global run over_react_codemod:dart2_upgrade --backwards-compat

    Use this codemod to migrate your over_react code to a format that is both forwards-compatible with Dart 2 and backwards-compatible with Dart 1.

  • pub global run over_react_codemod:dart2_upgrade

    Use this codemod if you want to migrate to Dart 2 compatible code and do not need to maintain backwards-compatability with Dart 1. You can run this to immediately upgrade from Dart 1 to Dart 2, or you can run this on code that has already been run through this codemod with the --backwards-compat flag once you're ready to drop Dart 1 support.

For more information on the transition from Dart 1 to Dart 2 and how it affects over_react, check out the over_react Dart 2 migration guide. It includes sample diffs of the changes that these codemods will introduce.

Checking for Regressions

Especially in the case where a forwards- and backwards-compatible version of your code is necessary, it can be helpful to be able to check automatically for regressions after having initially migrated your code. Doing so will help prevent accidentally merging code that doesn't meet your compatability expectations.

Each of these codemods can be run in a --fail-on-changes mode that will count the number of changes that would have been suggested and exit with a non-zero exit code if that number is greater than 0.

As an example, adding the following command to your CI process or pre-merge checklist will prevent merging code that is not in the form that is compatible with both Dart 1 and Dart 2:

pub global run over_react_codemod:dart2_upgrade --fail-on-changes

Ignoring Codemod Suggestions

Some of the changes provided by the codemods in this package are based on imperfect heuristics (e.g. looking for a specific naming convention) and consequently may be susceptible to false positives. If you hit such a case in your codebase, you can tell the codemod to ignore a certain line by attaching an ignore comment either on the same line or the immediately preceding line.

For example, consider a mixin usage that happens to match the naming convention of ending with StateMixin, but isn't actually an over_react state mixin:

class Foo extends Object with BarStateMixin {}

As is, the dart2_upgrade --backwards-compat codemod would find this code and attempt to change it to:

class Foo extends Object
    with
        BarStateMixin,
        // ignore: mixin_of_non_class, undefined_class
        $BarStateMixin {}

But if BarStateMixin isn't actually an over_react state mixin, then this updated code will fail. To avoid this problem, simply add an // orcm_ignore comment to the mixin type that should be ignored:

class Foo extends Object
    with
        // orcm_ignore
        BarStateMixin {}

This ignore mechanism works with any of the changes that the codemods in this package will try to suggest.

Authoring codemods

Resources

  • Analyzer package
    • AST documentation
    • Element model documentation
      • (the element model is only accessible from "resolved AST")
    • In general, the various AstNode and Element subclasses are well-documented, and are very helpful in describing the relationships between different types of nodes/elements and pointing to other classes. Reading those and clicking through the references is a good way to learn about specific structures, and help get your bearings.
  • codemod package

Best practices

  • Code defensively for edge-cases, and avoid assumptions about the AST or Element model.

    Some codemods process a lot of code, especially when identifying code that should be operated on, and some of that code may have structures you don't expect.

    That doesn't necessarily mean your codemod should handle every single edge-case, but generally it shouldn't break with uncaught exceptions when it encounters certain code.

    Things to avoid assumptions about:

    • The types or nullability of child/parent nodes
      • Prefer type checks over casts
        • tryCast() and ancestorOfType() can be handy in certain cases.
      • Prefer null-checks over !
    • The number of child nodes or other items in collections
      • When using .first/.last/.single on iterables, either check the length of the iterable first, or switch to something more conditional like .firstWhere() or .firstOrNull
  • Avoid using childEntities

    Most AST node classes have getters for their different child nodes. Using these helps make code easier to read, and also provide typing (and nullability) which helps with static analysis and autocomplete.

    If you're manually traversing the AST trying to find a certain descendant, consider whether a visitor-based implementation would be better.

  • Avoid using AstNode.toSource() (or .toString()) outside of tests

    • To identify relevant code. There may be different syntax variations (either current or in future Dart versions) of code that unintentionally wouldn't be a match.

      For example, if we were doing expression.toSource() == 'foo.bar()', we would miss the following cases:

        foo..bar();
        foo?.bar();
        namespaced_import.foo.bar();
        foo.bar(optionalArgAddedInSubclass: true);
      

      Instead, we could check for the method name via:

        expression is MethodInvocation && expression.methodName.name == 'bar'
      

      and change the foo check to something else, depending on the use-case:

        // Check whether the target is a "foo" identifier 
        // (though this doesn't handle prefixed cases)
        expression.realTarget?.tryCast<Identifier>()?.name == 'foo'
        // Check whether it statically points to a `foo` variable
        expression.realTarget?.tryCast<Identifier>()?.staticElement?.name == 'foo');
        // Or, Check whether it's a `Foo` object:
        isFooType(expression.realTarget?.staticType);
        // (Or, something else)
      

      For cases where the syntax seems simple enough where variations shouldn't be a problem, there are usually APIs that provide the value you're looking for.

        final identifier = parseIdentifier('foo');
        identifier.name; // 'foo'
      
        final boolean = parseBooleanLiteral('false');
        boolean.value; // false
      
        final import = parseImportDirective('import "package:foo/foo.dart";');
        import.uriContent; // 'package:foo/foo.dart'
      
    • For building patch strings (such as when moving code from one place to another).

      toSource() provides an approximation of the source, and may be missing comments or have different whitespace. If needed, use the context.sourceFile to get the original source for a given node.

  • Don't make assumptions about existing whitespace and line breaks.

    There are different ways code can be formatted with dartfmt, and some code may not be formatted at all, so it's unsafe to make assumptions.

    For instance, if you'd like to yield a patch that deletes a newline with some code, either check for its existence first by getting the source or line number of that offset in context.sourceFile. Or, instead of deleting from node.offset, delete from the end of the previous token (node.prevToken?.end ?? node.offset) to take any whitespace between those nodes with it.

Libraries