dart-sdk/pkg/nnbd_migration
Paul Berry 466494f5be Shared type analysis: API adjustments for analyzer and CFE
I've begun prototyping what it might look like to integrate the
current shared type analysis functionality with the analyzer and CFE,
and I've discovered some API improvements that are needed:

- The shared logic now handles the possibility that switch cases that
  share a body have been merged prior to type analysis (because the
  CFE merges them during parsing), in addition to the pre-existing
  functionality which assumed that switch case merging had to be done
  in the shared logic.

- The shared logic now returns several pieces of information as the
  result of a call to `analyzeSwitchStatement`: whether the switch
  statement had a `default` clause, whether it was exhaustive, whether
  the last case body terminates, and the type of the scrutinee.  These
  are all needed by the CFE.

- The shared logic now allows `TypeAnalyzer.errors` to be `null`,
  indicating that no errors should be reported.  This reflects how
  errors are suppressed during top level inference in the CFE.

- If a switch case lacks a `when` clause, this is reported by calling
  `handleNoWhen` rather than passing a boolean to `handleCaseHead`.

- The shared logic now reports the appropriate error when a case
  constant doesn't properly match the scrutinee's static type.

- Information about case labels is now delivered to flow analysis via
  `switchStatement_endAlternatives` rather than
  `switchStatement_beginCase`.  This made it possible to rewrite the
  shared `analyzeSwitchStatement` method in a way that requires less
  bookkeeping, because it no longer has to peek ahead to look for
  labels associated with a given case body.

- `TypeAnalyzer.analyzeExpression` is now responsible for
  understanding that "no context" and a context of `dynamic` should
  both be coalesced to `?`.  The analyzer does this (although it's not
  100% why), and it's definitely "business logic" that eventually
  belongs in the shared type analyzer.

- `TypeAnalyzer.analyzeSwitchExpression` and
  `TypeAnalyzer.analyzeSwitchStatement` no longer receive a list of
  ExpressionCaseInfo / StatementCaseInfo objects describing the cases;
  instead they query for them using a callback.  This reduces the
  lifetime of the ExpressionCaseInfo / StatementCaseInfo objects.  In
  the future, when we have record support, we could replace these
  objects with records, which would then be passed on the stack,
  avoiding any allocations.

- A new hook, `handleSwitchScrutinee`, is called right after visiting
  the "scrutinee" expression of a switch expression or switch
  statement.  This hook is needed by the analyzer to compute
  exhaustiveness.  In a future CL, I hope to move exhaustiveness
  analysis into the shared code as well, which should make this hook
  unnecessary.

- `TypeAnalyzer.analyzeSwitchStatement` now reports an error if a
  switch case completes normally and pattern support is not enabled.

- The test class `_MiniAstTypeAnalyzer` no longer overrides
  `analyzeExpression` to provide a default context type; instead,
  every call to `analyzeExpression` that didn't previously provide a
  context now provides a context of `?`.  Note that not all of these
  are correct, but they are close enough for the unit tests we have
  today.  I plan to fix them in future CLs as I replace this logic
  with shared logic.

- The hook `handleVariablePattern` is now always provided with a
  static type.  Previously, it was only provided with a static type if
  this was the first time the variable was bound in the pattern.

Change-Id: I70e3c5468312a9329fcf4ad2e13749a32d2418e7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/257487
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
2022-09-12 13:55:37 +00:00
..
bin Run the migration tool on itself. 2021-06-23 23:35:47 +00:00
lib Shared type analysis: API adjustments for analyzer and CFE 2022-09-12 13:55:37 +00:00
test Breaking changes for analyzer version 5.0.0 2022-09-07 16:27:18 +00:00
tool Reland "Migration: fix generate_resources.dart to use dart compile js." 2022-08-18 20:21:17 +00:00
.gitignore Move much of migration tool UI code into pkg/nnbd_migration 2020-04-23 23:43:42 +00:00
analysis_options.yaml Migration: enable the use of super parameters 2022-08-07 12:28:39 +00:00
CHANGELOG.md Prepare to publish package nnbd_migration version 0.1.1. 2020-10-13 22:53:04 +00:00
LICENSE Update LICENSE 2021-04-07 10:28:38 +00:00
OWNERS [infra] Add OWNERS to the Dart SDK 2022-02-14 14:06:34 +00:00
pubspec.yaml Migration: enable the use of super parameters 2022-08-07 12:28:39 +00:00
README.md Add a notice to the nnbd_migration package README. 2020-10-13 16:29:08 +00:00
TRIAGE.md Add a triage document for the dart migrate tool. 2020-11-17 21:39:48 +00:00

Null safety migration tooling

Note:

  • This migration tool is now available through the SDK, using the dart migrate command. Support for running it via pub activate is deprecated.
  • The null safety migration tooling is in an early state and may have bugs and other issues.
  • As null safety is still in preview, we recommend only doing trial migrations. The final migration of apps and packages should not be done until the feature is more complete.
  • For best results, use SDK version 2.9.0-10.0.dev or higher.

How migration works

The migration uses a new interactive algorithm designed specifically for Dart null safety.

Typical code migration tools are designed to be run once, handle most cases, and let the developer do manual cleanup on the result. This does not work well for null safety and attempting this workflow will result in a lot more manual work. Similarly, after your migration has been applied, the migration cannot be rerun without first reverting it.

Why does the interactive approach save so much time?

Remember that Dart already has nullable types. Every type in old Dart code is nullable! What old Dart lacks is non-null types.

And like most migrations, our tool tries to preserve your code's current behavior. In the case of null safety, we may mark a lot of your types as nullable -- because they really were nullable before.

Nulls are traced through your program as far as they can go, and types are marked nullable in this process. If the tool makes a single mistake or choice you disagree with, it can lead to many excess nullable types.

Interactive feedback to the tool

Unintentional null is the top cause of crashes in Dart programs. By marking your intention with comments like /*?*/ and /*!*/, we can stop these unintentional nulls from spreading through your program in your migrated code. Adding a small number of these hints will have a huge impact on migration quality.

The high level workflow of the tool is therefore driven through an interactive web UI. After starting the tool with dart migrate, open the presented URL in a browser. Scan through the changes, use the "nullability trace" feature to find the best place to add a nullability hint (adding a hint in the best place can prevent dozens of types from being made nullable). Rerun the migration and repeat, committing the hints as you go. When the output is correct and acceptable, apply the migration.

For example,

List<int> ints = const [0, null];
int zero = ints[0];
int one = zero + 1;
List<int> zeroOne = [zero, one];

The default migration will be backwards compatible, but not ideal.

List<int?> ints = const [0, null];
int? zero = ints[0];
int one = zero! + 1;
List<int?> zeroOne = <int?>[zero, one];

zero should not be marked nullable, but it is. We then have cascading quality issues, such as null-checking a value that shouldn't have been marked null, and marking other variables as null due to deep null tracing. We can fix this all by adding a single /*!*/ hint.

List<int?> ints = const [0, null];
int/*!*/ zero = ints[0]!; // Just add /*!*/ here, the migration tool does the rest!
int one = zero + 1;
List<int> zeroOne = <int>[zero, one];

If you add one hint before migrating, you have done the equivalent of making five manual edits after migrating. To find the best place to put your hints, use the preview tool's nullability trace feature. This lets you trace back up to the root cause of any type's inferred nullability. Add hints as close to the original source of null as possible to have the biggest impact to the migration.

Note: The migration tool cannot be rerun on a migrated codebase. At that point in time, every nullable and non-nullable type is indistinguishable from an intentionally nullable or non-nullable type. The opportunity to change large numbers of types for you at once without also accidentally changing your intent has been lost. A long migration effort (such as one on a large project) can be done incrementally, by committing these hints over time.

Migrating a package

  1. Select a package to work on, and open a command terminal in the top-level of the package directory.

  2. Run pub get in order to make available all dependencies of the package.

  3. It is best to migrate a package to null safety after the package's dependencies have migrated to null safety. Run pub outdated --mode=null-safety to learn the migration status of the package's dependencies. See the pub outdated documentation for more information.

  4. It is best to migrate a package starting from a clean code repository state (git status, for example), in case you must revert the migration. Ensure there are no pending changes in the package's code repository.

  5. Run the migration tool from the top-level of the package directory:

    dart migrate
    

The migration tool will display a URL for the web interface. Open that URL in a browser to view, analyze, and improve the proposed null-safe migration.

Using the tool

  1. Run the tool (see above).
  2. Once analysis and migration is complete, open the indicated URL in a browser.
  3. Start with an important or interesting file in your package on the left side by clicking on it.
  4. Look at the proposed edits in the upper right, and click on them in turn.
  5. If you see an edit that looks wrong:
    1. Use the "trace view" in the bottom right to find the root cause
    2. Either click on an "add hint" button to correct it at the root, or open your editor and make the change manually.
      • Some changes are as simple as adding a /*!*/ hint on a type. The tool has buttons to do this for you.
      • Others may require larger refactors. These changes can be made in your editor.
      • Changes may even be committed to source code management before finally applying the migration. In this way, a migration of a large package can be carried out over multiple sessions, or between multiple engineers. Committing hints and other adjustments along the way helps to separate the concerns of describing user intent vs committing to the migration result.
    3. Periodically rerun the migration and repeat.
  6. Once you are satisfied with the proposed migration:
    1. Save your work using git or other means. Applying the migration will overwrite the existing files on disk.
      • Note: In addition to making edits to the Dart source code in the package, applying the migration edits the package's pubspec.yaml file, in order to change the Dart SDK version constraints, under the environment field, and the "Package Config" file, located in the package's .dart_tool directory, named package_config.json.
    2. Apply the migration by clicking the Apply Migration button in the interface.
    3. Tip: leaving the web UI open may help you if you later have test failures or analysis errors.
  7. Rerun pub get, then analyze and test your package.
    1. If there are new static analysis issues, or if a test fails, you may still use the preview to help you figure out what went wrong.
    2. If large changes are required, revert the migration, and go back to step one. The tool does not provide any revert capability; this must be done via source code management (for example, git checkout).

Providing feedback

Please file issues at https://github.com/dart-lang/sdk/issues, and reference the analyzer-nnbd-migration label (you may not be able to apply the label yourself).