Null safe API (#347)

Migrate API to null safety.

This does not include some files in bin/, test/, and tool/:

* bin/markdown.dart requires package:args, which has not published a null safe release.
* tool/stats.dart requires package:args.
* tool/stats_lib.dart requires package:html, which has not published a null safe release.
* tool/dartdoc-compare.dart requires package:args.
* test/util.dart requires package:io, which has not published a null safe release.
* test/markdown_test.dart, test/vesion.dart import test/util.dart.

Most APIs upgraded to null safety very smoothly. Very clean change.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07693c7..e3c30cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
 ## 4.0.0-dev
 
+* Migrate package to Dart's null safety language feature, requiring Dart
+  2.12 or higher.
 * **Breaking change:** The TagSyntax constructor no longer takes an `end`
   parameter. TagSyntax no longer implements `onMatchEnd`. Instead, TagSyntax
   implements a method called `close` which creates and returns a Node, if a
diff --git a/benchmark/benchmark.dart b/benchmark/benchmark.dart
index e75787f..f3e934c 100644
--- a/benchmark/benchmark.dart
+++ b/benchmark/benchmark.dart
@@ -22,7 +22,7 @@
     var start = DateTime.now();
 
     // For a single benchmark, convert the source multiple times.
-    String result;
+    late String result;
     for (var j = 0; j < runsPerTrial; j++) {
       result = markdownToHtml(source);
     }
diff --git a/bin/markdown.dart b/bin/markdown.dart
index ce6fb82..5844354 100644
--- a/bin/markdown.dart
+++ b/bin/markdown.dart
@@ -1,3 +1,9 @@
+// Copyright (c) 2020, 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.
+
+// @dart=2.9
+
 import 'dart:async';
 import 'dart:io';
 
diff --git a/example/app.dart b/example/app.dart
index 9244692..b735ef8 100644
--- a/example/app.dart
+++ b/example/app.dart
@@ -1,3 +1,7 @@
+// Copyright (c) 2020, 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 'dart:async';
 import 'dart:html';
 import 'package:markdown/markdown.dart' as md;
@@ -20,7 +24,7 @@
 final basicRadio = querySelector('#basic-radio') as HtmlElement;
 final commonmarkRadio = querySelector('#commonmark-radio') as HtmlElement;
 final gfmRadio = querySelector('#gfm-radio') as HtmlElement;
-md.ExtensionSet extensionSet;
+md.ExtensionSet? extensionSet;
 
 final extensionSets = {
   'basic-radio': md.ExtensionSet.none,
@@ -46,7 +50,7 @@
 
   // GitHub is the default extension set.
   gfmRadio.attributes['checked'] = '';
-  gfmRadio.querySelector('.glyph').text = 'radio_button_checked';
+  gfmRadio.querySelector('.glyph')!.text = 'radio_button_checked';
   extensionSet = extensionSets[gfmRadio.id];
   _renderMarkdown();
 
@@ -55,8 +59,8 @@
   gfmRadio.onClick.listen(_switchFlavor);
 }
 
-void _renderMarkdown([Event event]) {
-  var markdown = markdownInput.value;
+void _renderMarkdown([Event? event]) {
+  var markdown = markdownInput.value!;
 
   htmlDiv.setInnerHtml(md.markdownToHtml(markdown, extensionSet: extensionSet),
       treeSanitizer: nullSanitizer);
@@ -77,9 +81,9 @@
 }
 
 void _typeItOut(String msg, int pos) {
-  Timer timer;
+  late Timer timer;
   markdownInput.onKeyUp.listen((_) {
-    timer?.cancel();
+    timer.cancel();
   });
   void addCharacter() {
     if (pos > msg.length) {
@@ -100,19 +104,19 @@
   if (!target.attributes.containsKey('checked')) {
     if (basicRadio != target) {
       basicRadio.attributes.remove('checked');
-      basicRadio.querySelector('.glyph').text = 'radio_button_unchecked';
+      basicRadio.querySelector('.glyph')!.text = 'radio_button_unchecked';
     }
     if (commonmarkRadio != target) {
       commonmarkRadio.attributes.remove('checked');
-      commonmarkRadio.querySelector('.glyph').text = 'radio_button_unchecked';
+      commonmarkRadio.querySelector('.glyph')!.text = 'radio_button_unchecked';
     }
     if (gfmRadio != target) {
       gfmRadio.attributes.remove('checked');
-      gfmRadio.querySelector('.glyph').text = 'radio_button_unchecked';
+      gfmRadio.querySelector('.glyph')!.text = 'radio_button_unchecked';
     }
 
     target.attributes['checked'] = '';
-    target.querySelector('.glyph').text = 'radio_button_checked';
+    target.querySelector('.glyph')!.text = 'radio_button_checked';
     extensionSet = extensionSets[target.id];
     _renderMarkdown();
   }
diff --git a/lib/src/ast.dart b/lib/src/ast.dart
index 5755226..8488a44 100644
--- a/lib/src/ast.dart
+++ b/lib/src/ast.dart
@@ -2,7 +2,7 @@
 // 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.
 
-typedef Resolver = Node Function(String name, [String title]);
+typedef Resolver = Node? Function(String name, [String? title]);
 
 /// Base class for any AST item.
 ///
@@ -16,9 +16,9 @@
 /// A named tag that can contain other nodes.
 class Element implements Node {
   final String tag;
-  final List<Node> children;
+  final List<Node>? children;
   final Map<String, String> attributes;
-  String generatedId;
+  String? generatedId;
 
   /// Instantiates a [tag] Element with [children].
   Element(this.tag, this.children) : attributes = <String, String>{};
@@ -45,7 +45,7 @@
   void accept(NodeVisitor visitor) {
     if (visitor.visitElementBefore(this)) {
       if (children != null) {
-        for (var child in children) {
+        for (var child in children!) {
           child.accept(visitor);
         }
       }
@@ -55,7 +55,7 @@
 
   @override
   String get textContent {
-    return (children ?? []).map((Node child) => child.textContent).join('');
+    return (children ?? []).map((Node? child) => child!.textContent).join('');
   }
 }
 
diff --git a/lib/src/block_parser.dart b/lib/src/block_parser.dart
index 004c02f..49ee387 100644
--- a/lib/src/block_parser.dart
+++ b/lib/src/block_parser.dart
@@ -107,7 +107,7 @@
   String get current => lines[_pos];
 
   /// Gets the line after the current one or `null` if there is none.
-  String get next {
+  String? get next {
     // Don't read past the end.
     if (_pos >= lines.length - 1) return null;
     return lines[_pos + 1];
@@ -119,7 +119,7 @@
   /// `peek(0)` is equivalent to [current].
   ///
   /// `peek(1)` is equivalent to [next].
-  String peek(int linesAhead) {
+  String? peek(int linesAhead) {
     if (linesAhead < 0) {
       throw ArgumentError('Invalid linesAhead: $linesAhead; must be >= 0.');
     }
@@ -143,7 +143,7 @@
   /// Gets whether or not the next line matches the given pattern.
   bool matchesNext(RegExp regex) {
     if (next == null) return false;
-    return regex.hasMatch(next);
+    return regex.hasMatch(next!);
   }
 
   List<Node> parseLines() {
@@ -174,11 +174,11 @@
     return pattern.hasMatch(parser.current);
   }
 
-  Node parse(BlockParser parser);
+  Node? parse(BlockParser parser);
 
-  List<String> parseChildLines(BlockParser parser) {
+  List<String?> parseChildLines(BlockParser parser) {
     // Grab all of the lines that form the block element.
-    var childLines = <String>[];
+    var childLines = <String?>[];
 
     while (!parser.isDone) {
       var match = pattern.firstMatch(parser.current);
@@ -199,7 +199,7 @@
 
   /// Generates a valid HTML anchor from the inner text of [element].
   static String generateAnchorHash(Element element) =>
-      element.children.first.textContent
+      element.children!.first.textContent
           .toLowerCase()
           .trim()
           .replaceAll(RegExp(r'[^a-z0-9 _-]'), '')
@@ -213,7 +213,7 @@
   const EmptyBlockSyntax();
 
   @override
-  Node parse(BlockParser parser) {
+  Node? parse(BlockParser parser) {
     parser.encounteredBlankLine = true;
     parser.advance();
 
@@ -253,7 +253,7 @@
   @override
   Node parse(BlockParser parser) {
     var lines = <String>[];
-    String tag;
+    String? tag;
     while (!parser.isDone) {
       var match = _setextPattern.firstMatch(parser.current);
       if (match == null) {
@@ -263,7 +263,7 @@
         continue;
       } else {
         // The underline.
-        tag = (match[1][0] == '=') ? 'h1' : 'h2';
+        tag = (match[1]![0] == '=') ? 'h1' : 'h2';
         parser.advance();
         break;
       }
@@ -271,7 +271,7 @@
 
     var contents = UnparsedContent(lines.join('\n').trimRight());
 
-    return Element(tag /*!*/, [contents]);
+    return Element(tag!, [contents]);
   }
 
   bool _interperableAsParagraph(String line) =>
@@ -307,10 +307,10 @@
 
   @override
   Node parse(BlockParser parser) {
-    var match = pattern.firstMatch(parser.current);
+    var match = pattern.firstMatch(parser.current)!;
     parser.advance();
-    var level = match[1].length;
-    var contents = UnparsedContent(match[2].trim());
+    var level = match[1]!.length;
+    var contents = UnparsedContent(match[2]!.trim());
     return Element('h$level', [contents]);
   }
 }
@@ -342,7 +342,7 @@
     while (!parser.isDone) {
       var match = pattern.firstMatch(parser.current);
       if (match != null) {
-        childLines.add(match[1] /*!*/);
+        childLines.add(match[1]!);
         parser.advance();
         continue;
       }
@@ -384,8 +384,8 @@
   const CodeBlockSyntax();
 
   @override
-  List<String> parseChildLines(BlockParser parser) {
-    var childLines = <String>[];
+  List<String?> parseChildLines(BlockParser parser) {
+    var childLines = <String?>[];
 
     while (!parser.isDone) {
       var match = pattern.firstMatch(parser.current);
@@ -396,7 +396,7 @@
         // If there's a codeblock, then a newline, then a codeblock, keep the
         // code blocks together.
         var nextMatch =
-            parser.next != null ? pattern.firstMatch(parser.next) : null;
+            parser.next != null ? pattern.firstMatch(parser.next!) : null;
         if (parser.current.trim() == '' && nextMatch != null) {
           childLines.add('');
           childLines.add(nextMatch[1]);
@@ -439,18 +439,18 @@
   bool canParse(BlockParser parser) {
     final match = pattern.firstMatch(parser.current);
     if (match == null) return false;
-    final codeFence = match.group(1);
+    final codeFence = match.group(1)!;
     final infoString = match.group(2);
     // From the CommonMark spec:
     //
     // > If the info string comes after a backtick fence, it may not contain
     // > any backtick characters.
     return (codeFence.codeUnitAt(0) != $backquote ||
-        !infoString.codeUnits.contains($backquote));
+        !infoString!.codeUnits.contains($backquote));
   }
 
   @override
-  List<String> parseChildLines(BlockParser parser, [String endBlock]) {
+  List<String> parseChildLines(BlockParser parser, [String? endBlock]) {
     endBlock ??= '';
 
     var childLines = <String>[];
@@ -458,7 +458,7 @@
 
     while (!parser.isDone) {
       var match = pattern.firstMatch(parser.current);
-      if (match == null || !match[1].startsWith(endBlock)) {
+      if (match == null || !match[1]!.startsWith(endBlock)) {
         childLines.add(parser.current);
         parser.advance();
       } else {
@@ -473,9 +473,9 @@
   @override
   Node parse(BlockParser parser) {
     // Get the syntax identifier, if there is one.
-    var match = pattern.firstMatch(parser.current);
+    var match = pattern.firstMatch(parser.current)!;
     var endBlock = match.group(1);
-    var infoString = match.group(2);
+    var infoString = match.group(2)!;
 
     var childLines = parseChildLines(parser, endBlock);
 
@@ -641,7 +641,7 @@
     // Ideally, [BlockSyntax.canEndBlock] should be changed to be a method
     // which accepts a [BlockParser], but this would be a breaking change,
     // so we're going with this temporarily.
-    var match = pattern.firstMatch(parser.current);
+    var match = pattern.firstMatch(parser.current)!;
     // The seventh group, in both [_olPattern] and [_ulPattern] is the text
     // after the delimiter.
     return match[7]?.isNotEmpty ?? false;
@@ -675,20 +675,20 @@
       }
     }
 
-    /*late*/ Match match;
+    late Match? match;
     bool tryMatch(RegExp pattern) {
       match = pattern.firstMatch(parser.current);
       return match != null;
     }
 
-    String listMarker;
-    String indent;
+    String? listMarker;
+    String? indent;
     // In case the first number in an ordered list is not 1, use it as the
     // "start".
-    int startNumber;
+    int? startNumber;
 
     while (!parser.isDone) {
-      var leadingSpace = _whitespaceRe.matchAsPrefix(parser.current).group(0);
+      var leadingSpace = _whitespaceRe.matchAsPrefix(parser.current)!.group(0)!;
       var leadingExpandedTabLength = _expandedTabLength(leadingSpace);
       if (tryMatch(_emptyPattern)) {
         if (_emptyPattern.hasMatch(parser.next ?? '')) {
@@ -707,15 +707,15 @@
         // Horizontal rule takes precedence to a new list item.
         break;
       } else if (tryMatch(_ulPattern) || tryMatch(_olPattern)) {
-        var precedingWhitespace = match[1] /*!*/;
-        var digits = match[2] ?? '';
+        var precedingWhitespace = match![1]!;
+        var digits = match![2] ?? '';
         if (startNumber == null && digits.isNotEmpty) {
           startNumber = int.parse(digits);
         }
-        var marker = match[3] /*!*/;
-        var firstWhitespace = match[5] ?? '';
-        var restWhitespace = match[6] ?? '';
-        var content = match[7] ?? '';
+        var marker = match![3]!;
+        var firstWhitespace = match![5] ?? '';
+        var restWhitespace = match![6] ?? '';
+        var content = match![7] ?? '';
         var isBlank = content.isEmpty;
         if (listMarker != null && listMarker != marker) {
           // Changing the bullet or ordered list delimiter starts a new list.
@@ -792,7 +792,7 @@
             var child = children[i];
             if (child is Element && child.tag == 'p') {
               children.removeAt(i);
-              children.insertAll(i, child.children);
+              children.insertAll(i, child.children!);
             }
           }
         }
@@ -883,11 +883,11 @@
   /// * a divider of hyphens and pipes (not rendered)
   /// * many body rows of body cells (`<td>` cells)
   @override
-  Node parse(BlockParser parser) {
-    var alignments = _parseAlignments(parser.next);
+  Node? parse(BlockParser parser) {
+    var alignments = _parseAlignments(parser.next!);
     var columnCount = alignments.length;
     var headRow = _parseRow(parser, alignments, 'th');
-    if (headRow.children.length != columnCount) {
+    if (headRow.children!.length != columnCount) {
       return null;
     }
     var head = Element('thead', [headRow]);
@@ -908,8 +908,8 @@
           children.removeLast();
         }
       }
-      while (row.children.length > columnCount) {
-        row.children.removeLast();
+      while (row.children!.length > columnCount) {
+        row.children!.removeLast();
       }
       rows.add(row);
     }
@@ -922,7 +922,7 @@
     }
   }
 
-  List<String> _parseAlignments(String line) {
+  List<String?> _parseAlignments(String line) {
     var startIndex = _walkPastOpeningPipe(line);
 
     var endIndex = line.length - 1;
@@ -954,7 +954,7 @@
   /// [alignments] is used to annotate an alignment on each cell, and
   /// [cellType] is used to declare either "td" or "th" cells.
   Element _parseRow(
-      BlockParser parser, List<String> alignments, String cellType) {
+      BlockParser parser, List<String?> alignments, String cellType) {
     var line = parser.current;
     var cells = <String>[];
     var index = _walkPastOpeningPipe(line);
@@ -1098,7 +1098,7 @@
 
   /// Extract reference link definitions from the front of the paragraph, and
   /// return the remaining paragraph lines.
-  List<String> _extractReflinkDefinitions(
+  List<String>? _extractReflinkDefinitions(
       BlockParser parser, List<String> lines) {
     bool lineStartsReflinkDefinition(int i) =>
         lines[i].startsWith(_reflinkDefinitionStart);
@@ -1196,13 +1196,13 @@
       // Not a reference link definition.
       return false;
     }
-    if (match[0].length < contents.length) {
+    if (match.match.length < contents.length) {
       // Trailing text. No good.
       return false;
     }
 
-    var label = match[1];
-    var destination = (match[2] ?? match[3]) /*!*/;
+    var label = match[1]!;
+    var destination = match[2] ?? match[3]!;
     var title = match[4];
 
     // The label must contain at least one non-whitespace character.
@@ -1215,7 +1215,7 @@
       title = null;
     } else {
       // Remove "", '', or ().
-      title = title.substring(1, title.length - 1);
+      title = title!.substring(1, title.length - 1);
     }
 
     // References are case-insensitive, and internal whitespace is compressed.
diff --git a/lib/src/document.dart b/lib/src/document.dart
index 2d57a46..550331a 100644
--- a/lib/src/document.dart
+++ b/lib/src/document.dart
@@ -11,8 +11,8 @@
 class Document {
   final Map<String, LinkReference> linkReferences = <String, LinkReference>{};
   final ExtensionSet extensionSet;
-  final Resolver linkResolver;
-  final Resolver imageLinkResolver;
+  final Resolver? linkResolver;
+  final Resolver? imageLinkResolver;
   final bool encodeHtml;
   final _blockSyntaxes = <BlockSyntax>{};
   final _inlineSyntaxes = <InlineSyntax>{};
@@ -22,9 +22,9 @@
   Iterable<InlineSyntax> get inlineSyntaxes => _inlineSyntaxes;
 
   Document({
-    Iterable<BlockSyntax> blockSyntaxes,
-    Iterable<InlineSyntax> inlineSyntaxes,
-    ExtensionSet extensionSet,
+    Iterable<BlockSyntax>? blockSyntaxes,
+    Iterable<InlineSyntax>? inlineSyntaxes,
+    ExtensionSet? extensionSet,
     this.linkResolver,
     this.imageLinkResolver,
     this.encodeHtml = true,
@@ -45,7 +45,7 @@
   }
 
   /// Parses the given inline Markdown [text] to a series of AST nodes.
-  List<Node> parseInline(String text) => InlineParser(text, this).parse();
+  List<Node> parseInline(String? text) => InlineParser(text, this).parse();
 
   void _parseInlineContent(List<Node> nodes) {
     for (var i = 0; i < nodes.length; i++) {
@@ -56,7 +56,7 @@
         nodes.insertAll(i, inlineNodes);
         i += inlineNodes.length - 1;
       } else if (node is Element && node.children != null) {
-        _parseInlineContent(node.children);
+        _parseInlineContent(node.children!);
       }
     }
   }
@@ -76,7 +76,7 @@
   final String destination;
 
   /// The [link title](http://spec.commonmark.org/0.28/#link-title).
-  final String title;
+  final String? title;
 
   /// Construct a new [LinkReference], with all necessary fields.
   ///
diff --git a/lib/src/html_renderer.dart b/lib/src/html_renderer.dart
index 77dabed..78ce279 100644
--- a/lib/src/html_renderer.dart
+++ b/lib/src/html_renderer.dart
@@ -13,11 +13,11 @@
 /// Converts the given string of Markdown to HTML.
 String markdownToHtml(
   String markdown, {
-  Iterable<BlockSyntax> blockSyntaxes,
-  Iterable<InlineSyntax> inlineSyntaxes,
-  ExtensionSet extensionSet,
-  Resolver linkResolver,
-  Resolver imageLinkResolver,
+  Iterable<BlockSyntax> blockSyntaxes = const [],
+  Iterable<InlineSyntax> inlineSyntaxes = const [],
+  ExtensionSet? extensionSet,
+  Resolver? linkResolver,
+  Resolver? imageLinkResolver,
   bool inlineOnly = false,
 }) {
   var document = Document(
@@ -37,7 +37,7 @@
 }
 
 /// Renders [nodes] to HTML.
-String renderToHtml(List<Node> nodes) => HtmlRenderer().render(nodes);
+String renderToHtml(List<Node?> nodes) => HtmlRenderer().render(nodes);
 
 const _blockTags = [
   'blockquote',
@@ -74,20 +74,20 @@
 
 /// Translates a parsed AST to HTML.
 class HtmlRenderer implements NodeVisitor {
-  StringBuffer buffer;
-  Set<String> uniqueIds;
+  late StringBuffer buffer;
+  late Set<String> uniqueIds;
 
   final _elementStack = <Element>[];
-  String _lastVisitedTag;
+  String? _lastVisitedTag;
 
   HtmlRenderer();
 
-  String render(List<Node> nodes) {
+  String render(List<Node?> nodes) {
     buffer = StringBuffer();
     uniqueIds = <String>{};
 
     for (final node in nodes) {
-      node.accept(this);
+      node!.accept(this);
     }
 
     return buffer.toString();
@@ -123,9 +123,11 @@
       buffer.write(' ${entry.key}="${entry.value}"');
     }
 
+    var generatedId = element.generatedId;
+
     // attach header anchor ids generated from text
-    if (element.generatedId != null) {
-      buffer.write(' id="${uniquifyId(element.generatedId)}"');
+    if (generatedId != null) {
+      buffer.write(' id="${uniquifyId(generatedId)}"');
     }
 
     _lastVisitedTag = element.tag;
@@ -151,7 +153,7 @@
     assert(identical(_elementStack.last, element));
 
     if (element.children != null &&
-        element.children.isNotEmpty &&
+        element.children!.isNotEmpty &&
         _blockTags.contains(_lastVisitedTag) &&
         _blockTags.contains(element.tag)) {
       buffer.writeln();
diff --git a/lib/src/inline_parser.dart b/lib/src/inline_parser.dart
index b7797f3..ccdb3d2 100644
--- a/lib/src/inline_parser.dart
+++ b/lib/src/inline_parser.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:charcode/charcode.dart';
-import 'package:meta/meta.dart';
 
 import 'ast.dart';
 import 'document.dart';
@@ -47,7 +46,7 @@
   ]);
 
   /// The string of Markdown being parsed.
-  final String source;
+  final String? source;
 
   /// The Markdown document this parser is parsing.
   final Document document;
@@ -214,7 +213,7 @@
         continue;
       }
       openersBottom.putIfAbsent(closer.char, () => List.filled(3, bottomIndex));
-      var openersBottomPerCloserLength = openersBottom[closer.char];
+      var openersBottomPerCloserLength = openersBottom[closer.char]!;
       var openerBottom = openersBottomPerCloserLength[closer.length % 3];
       var openerIndex = _delimiterStack.lastIndexWhere(
           (d) =>
@@ -233,8 +232,8 @@
                 _tree.sublist(openerTextNodeIndex + 1, closerTextNodeIndex));
         // Replace all of the nodes between the opener and the closer (which
         // are now the new emphasis node's children) with the emphasis node.
-        _tree
-            .replaceRange(openerTextNodeIndex + 1, closerTextNodeIndex, [node]);
+        _tree.replaceRange(
+            openerTextNodeIndex + 1, closerTextNodeIndex, [node!]);
         // Slide [closerTextNodeIndex] back accordingly.
         closerTextNodeIndex = openerTextNodeIndex + 2;
 
@@ -288,19 +287,19 @@
 
   // Combine any remaining adjacent Text nodes. This is important to produce
   // correct output across newlines, where whitespace is sometimes compressed.
-  void _combineAdjacentText(List<Node> nodes) {
+  void _combineAdjacentText(List<Node?> nodes) {
     for (var i = 0; i < nodes.length - 1; i++) {
       var node = nodes[i];
       if (node is Element && node.children != null) {
-        _combineAdjacentText(node.children);
+        _combineAdjacentText(node.children!);
         continue;
       }
       if (node is Text && nodes[i + 1] is Text) {
         var buffer =
-            StringBuffer('${node.textContent}${nodes[i + 1].textContent}');
+            StringBuffer('${node.textContent}${nodes[i + 1]!.textContent}');
         var j = i + 2;
         while (j < nodes.length && nodes[j] is Text) {
-          buffer.write(nodes[j].textContent);
+          buffer.write(nodes[j]!.textContent);
           j++;
         }
         nodes[i] = Text(buffer.toString());
@@ -309,13 +308,13 @@
     }
   }
 
-  int charAt(int index) => source.codeUnitAt(index);
+  int charAt(int index) => source!.codeUnitAt(index);
 
   void writeText() {
     if (pos == start) {
       return;
     }
-    var text = source.substring(start, pos);
+    var text = source!.substring(start, pos);
     _tree.add(Text(text));
     start = pos;
   }
@@ -328,7 +327,7 @@
   /// Push [state] onto the stack of [TagState]s.
   void _pushDelimiter(Delimiter delimiter) => _delimiterStack.add(delimiter);
 
-  bool get isDone => pos == source.length;
+  bool get isDone => pos == source!.length;
 
   void advanceBy(int length) {
     pos += length;
@@ -346,13 +345,13 @@
 
   /// The first character of [pattern], to be used as an efficient first check
   /// that this syntax matches the current parser position.
-  final int _startCharacter;
+  final int? _startCharacter;
 
   /// Create a new [InlineSyntax] which matches text on [pattern].
   ///
   /// If [startCharacter] is passed, it is used as a pre-matching check which
   /// is faster than matching against [pattern].
-  InlineSyntax(String pattern, {int startCharacter})
+  InlineSyntax(String pattern, {int? startCharacter})
       : pattern = RegExp(pattern, multiLine: true),
         _startCharacter = startCharacter;
 
@@ -360,24 +359,24 @@
   ///
   /// The parser's position can be overriden with [startMatchPos].
   /// Returns whether or not the pattern successfully matched.
-  bool tryMatch(InlineParser parser, [int startMatchPos]) {
+  bool tryMatch(InlineParser parser, [int? startMatchPos]) {
     startMatchPos ??= parser.pos;
 
     // Before matching with the regular expression [pattern], which can be
     // expensive on some platforms, check if even the first character matches
     // this syntax.
     if (_startCharacter != null &&
-        parser.source.codeUnitAt(startMatchPos) != _startCharacter) {
+        parser.source!.codeUnitAt(startMatchPos) != _startCharacter) {
       return false;
     }
 
-    final startMatch = pattern.matchAsPrefix(parser.source, startMatchPos);
+    final startMatch = pattern.matchAsPrefix(parser.source!, startMatchPos);
     if (startMatch == null) return false;
 
     // Write any existing plain text up to this point.
     parser.writeText();
 
-    if (onMatch(parser, startMatch)) parser.consume(startMatch[0].length);
+    if (onMatch(parser, startMatch)) parser.consume(startMatch.match.length);
     return true;
   }
 
@@ -409,7 +408,7 @@
   /// If [sub] is passed, it is used as a simple replacement for [pattern]. If
   /// [startCharacter] is passed, it is used as a pre-matching check which is
   /// faster than matching against [pattern].
-  TextSyntax(String pattern, {String sub, int startCharacter})
+  TextSyntax(String pattern, {String sub = '', int? startCharacter})
       : substitute = sub,
         super(pattern, startCharacter: startCharacter);
 
@@ -420,11 +419,11 @@
   /// returned.
   @override
   bool onMatch(InlineParser parser, Match match) {
-    if (substitute == null ||
+    if (substitute.isEmpty ||
         (match.start > 0 &&
             match.input.substring(match.start - 1, match.start) == '/')) {
       // Just use the original matched text.
-      parser.advanceBy(match[0].length);
+      parser.advanceBy(match.match.length);
       return false;
     }
 
@@ -440,7 +439,8 @@
 
   @override
   bool onMatch(InlineParser parser, Match match) {
-    final char = match[0].codeUnitAt(1);
+    var chars = match.match;
+    var char = chars.codeUnitAt(1);
     // Insert the substitution. Why these three charactes are replaced with
     // their equivalent HTML entity referenced appears to be missing from the
     // CommonMark spec, but is very present in all of the examples.
@@ -452,7 +452,7 @@
     } else if (char == $gt) {
       parser.addNode(Text('&gt;'));
     } else {
-      parser.addNode(Text(match[0][1]));
+      parser.addNode(Text(chars[1]));
     }
     return true;
   }
@@ -485,7 +485,7 @@
 
   @override
   bool onMatch(InlineParser parser, Match match) {
-    var url = match[1] /*!*/;
+    var url = match[1]!;
     var text = parser.document.encodeHtml ? escapeHtml(url) : url;
     var anchor = Element.text('a', text);
     anchor.attributes['href'] = Uri.encodeFull('mailto:$url');
@@ -501,7 +501,7 @@
 
   @override
   bool onMatch(InlineParser parser, Match match) {
-    var url = match[1];
+    var url = match[1]!;
     var text = parser.document.encodeHtml ? escapeHtml(url) : url;
     var anchor = Element.text('a', text);
     anchor.attributes['href'] = Uri.encodeFull(url);
@@ -544,13 +544,13 @@
   AutolinkExtensionSyntax() : super('$start(($scheme)($domain)($path))');
 
   @override
-  bool tryMatch(InlineParser parser, [int startMatchPos]) {
+  bool tryMatch(InlineParser parser, [int? startMatchPos]) {
     return super.tryMatch(parser, parser.pos > 0 ? parser.pos - 1 : 0);
   }
 
   @override
   bool onMatch(InlineParser parser, Match match) {
-    var url = match[1];
+    var url = match[1]!;
     var href = url;
     var matchLength = url.length;
 
@@ -562,7 +562,7 @@
     }
 
     // Prevent accidental standard autolink matches
-    if (url.endsWith('>') && parser.source[parser.pos - 1] == '<') {
+    if (url.endsWith('>') && parser.source![parser.pos - 1] == '<') {
       return false;
     }
 
@@ -589,7 +589,7 @@
     // https://github.github.com/gfm/#example-599
     final trailingPunc = regExpTrailingPunc.firstMatch(url);
     if (trailingPunc != null) {
-      var trailingLength = trailingPunc[0].length;
+      var trailingLength = trailingPunc.match.length;
       url = url.substring(0, url.length - trailingLength);
       href = href.substring(0, href.length - trailingLength);
       matchLength -= trailingLength;
@@ -605,7 +605,7 @@
       final entityRef = regExpEndsWithColon.firstMatch(url);
       if (entityRef != null) {
         // Strip out HTML entity reference
-        var entityRefLength = entityRef[0].length;
+        var entityRefLength = entityRef.match.length;
         url = url.substring(0, url.length - entityRefLength);
         href = href.substring(0, href.length - entityRefLength);
         matchLength -= entityRefLength;
@@ -643,7 +643,7 @@
 /// a [TagSyntax].
 abstract class Delimiter {
   /// The [Text] node representing the plain text representing this delimiter.
-  Text node;
+  abstract Text node;
 
   /// The type of delimiter.
   ///
@@ -664,7 +664,7 @@
   /// stack.  It is, by default, active. Once we parse the next possible link,
   /// `[more](links)`, as a real link, we must deactive the pending links (just
   /// the one, in this case).
-  bool isActive;
+  abstract bool isActive;
 
   /// Whether this delimiter can open emphasis or strong emphasis.
   bool get canOpen;
@@ -703,13 +703,13 @@
   final int endPos;
 
   SimpleDelimiter(
-      {@required this.node,
-      @required this.char,
-      @required this.length,
-      @required this.canOpen,
-      @required this.canClose,
-      @required this.syntax,
-      @required this.endPos})
+      {required this.node,
+      required this.char,
+      required this.length,
+      required this.canOpen,
+      required this.canClose,
+      required this.syntax,
+      required this.endPos})
       : isActive = true;
 }
 
@@ -778,15 +778,15 @@
   final bool canClose;
 
   DelimiterRun._({
-    @required this.node,
-    @required this.char,
-    @required this.syntax,
-    @required bool isLeftFlanking,
-    @required bool isRightFlanking,
-    @required bool isPrecededByPunctuation,
-    @required bool isFollowedByPunctuation,
-    @required this.allowIntraWord,
-  })  : canOpen = isLeftFlanking &&
+    required this.node,
+    required this.char,
+    required this.syntax,
+    required bool isLeftFlanking,
+    required bool isRightFlanking,
+    required bool isPrecededByPunctuation,
+    required bool isFollowedByPunctuation,
+    required this.allowIntraWord,
+  })   : canOpen = isLeftFlanking &&
             (char == $asterisk ||
                 !isRightFlanking ||
                 allowIntraWord ||
@@ -800,9 +800,9 @@
 
   /// Tries to parse a delimiter run from [runStart] (inclusive) to [runEnd]
   /// (exclusive).
-  static DelimiterRun tryParse(InlineParser parser, int runStart, int runEnd,
-      {@required TagSyntax syntax,
-      @required Text node,
+  static DelimiterRun? tryParse(InlineParser parser, int runStart, int runEnd,
+      {required TagSyntax syntax,
+      required Text node,
       bool allowIntraWord = false}) {
     bool leftFlanking,
         rightFlanking,
@@ -813,15 +813,15 @@
       rightFlanking = false;
       preceding = '\n';
     } else {
-      preceding = parser.source.substring(runStart - 1, runStart);
+      preceding = parser.source!.substring(runStart - 1, runStart);
     }
     precededByPunctuation = punctuation.hasMatch(preceding);
 
-    if (runEnd == parser.source.length) {
+    if (runEnd == parser.source!.length) {
       leftFlanking = false;
       following = '\n';
     } else {
-      following = parser.source.substring(runEnd, runEnd + 1);
+      following = parser.source!.substring(runEnd, runEnd + 1);
     }
     followedByPunctuation = punctuation.hasMatch(following);
 
@@ -890,21 +890,21 @@
   /// pre-matching check which is faster than matching against [pattern].
   TagSyntax(String pattern,
       {this.requiresDelimiterRun = false,
-      int startCharacter,
+      int? startCharacter,
       this.allowIntraWord = false})
       : super(pattern, startCharacter: startCharacter);
 
   @override
   bool onMatch(InlineParser parser, Match match) {
-    var runLength = match.group(0).length;
+    var runLength = match.group(0)!.length;
     var matchStart = parser.pos;
     var matchEnd = parser.pos + runLength;
-    var text = Text(parser.source.substring(matchStart, matchEnd));
+    var text = Text(parser.source!.substring(matchStart, matchEnd));
     if (!requiresDelimiterRun) {
       parser._pushDelimiter(SimpleDelimiter(
           node: text,
           length: runLength,
-          char: parser.source.codeUnitAt(matchStart),
+          char: parser.source!.codeUnitAt(matchStart),
           canOpen: true,
           canClose: false,
           syntax: this,
@@ -934,8 +934,8 @@
   /// If a tag can be closed at the current position, then this method calls
   /// [getChildren], in which [parser] parses any nested text into child nodes.
   /// The returned [Node] incorpororates these child nodes.
-  Node close(InlineParser parser, Delimiter opener, Delimiter closer,
-      {@required List<Node> Function() getChildren}) {
+  Node? close(InlineParser parser, Delimiter opener, Delimiter closer,
+      {required List<Node> Function() getChildren}) {
     var strong = opener.length >= 2 && closer.length >= 2;
     return Element(strong ? 'strong' : 'em', getChildren());
   }
@@ -948,7 +948,7 @@
 
   @override
   Node close(InlineParser parser, Delimiter opener, Delimiter closer,
-      {@required List<Node> Function() getChildren}) {
+      {required List<Node> Function() getChildren}) {
     return Element('del', getChildren());
   }
 }
@@ -960,21 +960,21 @@
   final Resolver linkResolver;
 
   LinkSyntax(
-      {Resolver linkResolver,
+      {Resolver? linkResolver,
       String pattern = r'\[',
       int startCharacter = $lbracket})
-      : linkResolver = (linkResolver ?? (String _, [String __]) => null),
+      : linkResolver = (linkResolver ?? ((String _, [String? __]) => null)),
         super(pattern, startCharacter: startCharacter);
 
   @override
-  Node close(
-      InlineParser parser, covariant SimpleDelimiter opener, Delimiter closer,
-      {@required List<Node> Function() getChildren}) {
-    var text = parser.source.substring(opener.endPos, parser.pos);
+  Node? close(
+      InlineParser parser, covariant SimpleDelimiter opener, Delimiter? closer,
+      {required List<Node> Function() getChildren}) {
+    var text = parser.source!.substring(opener.endPos, parser.pos);
     // The current character is the `]` that closed the link text. Examine the
     // next character, to determine what type of link we might have (a '('
     // means a possible inline link; otherwise a possible reference link).
-    if (parser.pos + 1 >= parser.source.length) {
+    if (parser.pos + 1 >= parser.source!.length) {
       // The `]` is at the end of the document, but this may still be a valid
       // shortcut reference link.
       return _tryCreateReferenceLink(parser, text, getChildren: getChildren);
@@ -1007,7 +1007,7 @@
       parser.advanceBy(1);
       // At this point, we've matched `[...][`. Maybe a *full* reference link,
       // like `[foo][bar]` or a *collapsed* reference link, like `[foo][]`.
-      if (parser.pos + 1 < parser.source.length &&
+      if (parser.pos + 1 < parser.source!.length &&
           parser.charAt(parser.pos + 1) == $rbracket) {
         // That opening `[` is not actually part of the link. Maybe a
         // *shortcut* reference link (followed by a `[`).
@@ -1036,13 +1036,13 @@
   /// Otherwise, returns `null`.
   ///
   /// [label] does not need to be normalized.
-  Node _resolveReferenceLink(
+  Node? _resolveReferenceLink(
       String label, Map<String, LinkReference> linkReferences,
-      {List<Node> Function() getChildren}) {
+      {List<Node> Function()? getChildren}) {
     var linkReference = linkReferences[normalizeLinkLabel(label)];
     if (linkReference != null) {
       return _createNode(linkReference.destination, linkReference.title,
-          getChildren: getChildren);
+          getChildren: getChildren!);
     } else {
       // This link has no reference definition. But we allow users of the
       // library to specify a custom resolver function ([linkResolver]) that
@@ -1057,15 +1057,15 @@
           .replaceAll(r'\[', '[')
           .replaceAll(r'\]', ']'));
       if (resolved != null) {
-        getChildren();
+        getChildren!();
       }
       return resolved;
     }
   }
 
   /// Create the node represented by a Markdown link.
-  Node _createNode(String destination, String title,
-      {@required List<Node> Function() getChildren}) {
+  Node _createNode(String destination, String? title,
+      {required List<Node> Function() getChildren}) {
     var children = getChildren();
     var element = Element('a', children);
     element.attributes['href'] = escapeAttribute(destination);
@@ -1077,18 +1077,18 @@
 
   /// Tries to create a reference link node.
   ///
-  /// Returns whether the link was created successfully.
-  Node _tryCreateReferenceLink(InlineParser parser, String label,
-      {List<Node> Function() getChildren}) {
+  /// Returns the link if it was successfully created, `null` otherwise.
+  Node? _tryCreateReferenceLink(InlineParser parser, String label,
+      {required List<Node> Function() getChildren}) {
     return _resolveReferenceLink(label, parser.document.linkReferences,
         getChildren: getChildren);
   }
 
   // Tries to create an inline link node.
   //
-  // Returns whether the link was created successfully.
+  /// Returns the link if it was successfully created, `null` otherwise.
   Node _tryCreateInlineLink(InlineParser parser, InlineLink link,
-      {List<Node> Function() getChildren}) {
+      {required List<Node> Function() getChildren}) {
     return _createNode(link.destination, link.title, getChildren: getChildren);
   }
 
@@ -1098,7 +1098,7 @@
   /// opens the link label.
   ///
   /// Returns the label if it could be parsed, or `null` if not.
-  String _parseReferenceLinkLabel(InlineParser parser) {
+  String? _parseReferenceLinkLabel(InlineParser parser) {
     // Walk past the opening `[`.
     parser.advanceBy(1);
     if (parser.isDone) return null;
@@ -1141,7 +1141,7 @@
   /// `(http://url "title")`.
   ///
   /// Returns the [InlineLink] if one was parsed, or `null` if not.
-  InlineLink _parseInlineLink(InlineParser parser) {
+  InlineLink? _parseInlineLink(InlineParser parser) {
     // Start walking to the character just after the opening `(`.
     parser.advanceBy(1);
 
@@ -1159,7 +1159,9 @@
   /// Parse an inline link with a bracketed destination (a destination wrapped
   /// in `<...>`). The current position of the parser must be the first
   /// character of the destination.
-  InlineLink _parseInlineBracketedLink(InlineParser parser) {
+  ///
+  /// Returns the link if it was successfully created, `null` otherwise.
+  InlineLink? _parseInlineBracketedLink(InlineParser parser) {
     parser.advanceBy(1);
 
     var buffer = StringBuffer();
@@ -1212,7 +1214,9 @@
   /// Parse an inline link with a "bare" destination (a destination _not_
   /// wrapped in `<...>`). The current position of the parser must be the first
   /// character of the destination.
-  InlineLink _parseInlineBareDestinationLink(InlineParser parser) {
+  ///
+  /// Returns the link if it was successfully created, `null` otherwise.
+  InlineLink? _parseInlineBareDestinationLink(InlineParser parser) {
     // According to
     // [CommonMark](http://spec.commonmark.org/0.28/#link-destination):
     //
@@ -1301,10 +1305,12 @@
     }
   }
 
-  // Parse a link title in [parser] at it's current position. The parser's
-  // current position should be a whitespace character that followed a link
-  // destination.
-  String _parseTitle(InlineParser parser) {
+  /// Parses a link title in [parser] at it's current position. The parser's
+  /// current position should be a whitespace character that followed a link
+  /// destination.
+  ///
+  /// Returns the title if it was successfully parsed, `null` otherwise.
+  String? _parseTitle(InlineParser parser) {
     _moveThroughWhitespace(parser);
     if (parser.isDone) return null;
 
@@ -1353,15 +1359,15 @@
 /// Matches images like `![alternate text](url "optional title")` and
 /// `![alternate text][label]`.
 class ImageSyntax extends LinkSyntax {
-  ImageSyntax({Resolver linkResolver})
+  ImageSyntax({Resolver? linkResolver})
       : super(
             linkResolver: linkResolver,
             pattern: r'!\[',
             startCharacter: $exclamation);
 
   @override
-  Element _createNode(String destination, String title,
-      {List<Node> Function() getChildren}) {
+  Element _createNode(String destination, String? title,
+      {required List<Node> Function() getChildren}) {
     var element = Element.empty('img');
     var children = getChildren();
     element.attributes['src'] = destination;
@@ -1391,7 +1397,7 @@
   CodeSyntax() : super(_pattern);
 
   @override
-  bool tryMatch(InlineParser parser, [int startMatchPos]) {
+  bool tryMatch(InlineParser parser, [int? startMatchPos]) {
     if (parser.pos > 0 && parser.charAt(parser.pos - 1) == $backquote) {
       // Not really a match! We can't just sneak past one backtick to try the
       // next character. An example of this situation would be:
@@ -1401,18 +1407,18 @@
       return false;
     }
 
-    var match = pattern.matchAsPrefix(parser.source, parser.pos);
+    var match = pattern.matchAsPrefix(parser.source!, parser.pos);
     if (match == null) {
       return false;
     }
     parser.writeText();
-    if (onMatch(parser, match)) parser.consume(match[0].length);
+    if (onMatch(parser, match)) parser.consume(match.match.length);
     return true;
   }
 
   @override
   bool onMatch(InlineParser parser, Match match) {
-    var code = match[2].trim().replaceAll('\n', ' ');
+    var code = match[2]!.trim().replaceAll('\n', ' ');
     if (parser.document.encodeHtml) code = escapeHtml(code);
     parser.addNode(Element.text('code', code));
 
@@ -1432,7 +1438,7 @@
 
   @override
   bool onMatch(InlineParser parser, Match match) {
-    var alias = match[1];
+    var alias = match[1]!;
     var emoji = emojis[alias];
     if (emoji == null) {
       parser.advanceBy(1);
@@ -1446,7 +1452,7 @@
 
 class InlineLink {
   final String destination;
-  final String title;
+  final String? title;
 
   InlineLink(this.destination, {this.title});
 }
diff --git a/lib/src/util.dart b/lib/src/util.dart
index 8a53657..3a8a454 100644
--- a/lib/src/util.dart
+++ b/lib/src/util.dart
@@ -1,3 +1,7 @@
+// Copyright (c) 2012, 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 'dart:convert';
 
 import 'package:charcode/charcode.dart';
@@ -82,3 +86,8 @@
 /// [CommonMark spec] https://spec.commonmark.org/0.29/#link-label
 String normalizeLinkLabel(String label) =>
     label.trim().replaceAll(_oneOrMoreWhitespacePattern, ' ').toLowerCase();
+
+extension MatchExtensions on Match {
+  /// Returns the whole match String
+  String get match => this[0]!;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 325b6a1..3a86167 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -9,12 +9,12 @@
   markdown:
 
 environment:
-  sdk: '>=2.6.0 <3.0.0'
+  sdk: '>=2.12.0-0 <3.0.0'
 
 dependencies:
   args: ^1.0.0
-  charcode: ^1.1.0
-  meta: ^1.0.0
+  charcode: ^1.2.0-nullsafety.3
+  meta: ^1.3.0-nullsafety.6
 
 dev_dependencies:
   build_runner: ^1.0.0
@@ -24,7 +24,7 @@
   html: '>=0.12.2 <0.15.0'
   io: ^0.3.2+1
   js: ^0.6.1
-  path: ^1.3.1
-  pedantic: ^1.9.0
-  test: ^1.2.0
-  yaml: ^2.1.8
+  path: ^1.8.0-nullsafety.3
+  pedantic: ^1.10.0-nullsafety.3
+  test: ^1.16.0-nullsafety.13
+  yaml: ^3.0.0-nullsafety.0
diff --git a/test/markdown_test.dart b/test/markdown_test.dart
index 03785c8..542a601 100644
--- a/test/markdown_test.dart
+++ b/test/markdown_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart=2.9
+
 import 'package:markdown/markdown.dart';
 import 'package:test/test.dart';
 
@@ -125,7 +127,7 @@
 
     validateCore('dart custom links', 'links [are<foo>] awesome',
         '<p>links <a>are&lt;foo></a> awesome</p>\n',
-        linkResolver: (String text, [_]) =>
+        linkResolver: (String text, [String /*?*/ _]) =>
             Element.text('a', text.replaceAll('<', '&lt;')));
 
     // TODO(amouravski): need more tests here for custom syntaxes, as some
diff --git a/test/util.dart b/test/util.dart
index 90f0c74..8700160 100644
--- a/test/util.dart
+++ b/test/util.dart
@@ -2,8 +2,7 @@
 // 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.
 
-// Used by `dataCasesUnder` below to find the current directory.
-library markdown.test.util;
+// @dart=2.9
 
 import 'dart:isolate';
 
@@ -27,13 +26,15 @@
   }
 }
 
+Future<String> get markdownPackageRoot async =>
+    p.dirname(p.dirname((await Isolate.resolvePackageUri(
+            Uri.parse('package:markdown/markdown.dart')))
+        .path));
+
 void testFile(String file,
     {Iterable<BlockSyntax> blockSyntaxes,
     Iterable<InlineSyntax> inlineSyntaxes}) async {
-  var markdownLibRoot = p.dirname((await Isolate.resolvePackageUri(
-          Uri.parse('package:markdown/markdown.dart')))
-      .path);
-  var directory = p.join(p.dirname(markdownLibRoot), 'test');
+  var directory = p.join(await markdownPackageRoot, 'test');
   for (var dataCase in dataCasesInFile(path: p.join(directory, file))) {
     var description =
         '${dataCase.directory}/${dataCase.file}.unit ${dataCase.description}';
diff --git a/test/version_test.dart b/test/version_test.dart
index f9cb207..df2149a 100644
--- a/test/version_test.dart
+++ b/test/version_test.dart
@@ -1,24 +1,31 @@
+// Copyright (c) 2020, 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.
+
+// @dart=2.9
+
 import 'dart:io';
-import 'dart:mirrors';
 
 import 'package:path/path.dart' as p;
 import 'package:test/test.dart';
 import 'package:yaml/yaml.dart';
 
-// Locate the "tool" directory. Use mirrors so that this works with the test
-// package, which loads this suite into an isolate.
-String get _currentDir => p
-    .dirname((reflect(main) as ClosureMirror).function.location.sourceUri.path);
+import 'util.dart';
 
 void main() {
-  test('check versions', () {
-    var binary = p.normalize(p.join(_currentDir, '..', 'bin/markdown.dart'));
-    var result = Process.runSync('dart', [binary, '--version']);
-    expect(result.exitCode, 0);
+  test('check versions', () async {
+    var packageRoot = await markdownPackageRoot;
+    var binary = p.normalize(p.join(packageRoot, 'bin', 'markdown.dart'));
+    var dartBin = Platform.executable;
+    var result = Process.runSync(dartBin, [binary, '--version']);
+    expect(result.exitCode, 0,
+        reason: 'Exit code expected: 0; actual: ${result.exitCode}\n\n'
+            'stdout: ${result.stdout}\n\n'
+            'stderr: ${result.stderr}');
 
     var binVersion = (result.stdout as String).trim();
 
-    var pubspecFile = p.normalize(p.join(_currentDir, '..', 'pubspec.yaml'));
+    var pubspecFile = p.normalize(p.join(packageRoot, 'pubspec.yaml'));
 
     var pubspecContent =
         loadYaml(File(pubspecFile).readAsStringSync()) as YamlMap;
diff --git a/tool/dartdoc-compare.dart b/tool/dartdoc-compare.dart
index 93857e6..dcd2bb0 100644
--- a/tool/dartdoc-compare.dart
+++ b/tool/dartdoc-compare.dart
@@ -1,3 +1,9 @@
+// Copyright (c) 2020, 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.
+
+// @dart=2.9
+
 import 'dart:convert' show jsonEncode, jsonDecode;
 import 'dart:io' show Directory, File, Platform, Process, exitCode;
 
diff --git a/tool/expected_output.dart b/tool/expected_output.dart
index a85ccde..8561c8e 100644
--- a/tool/expected_output.dart
+++ b/tool/expected_output.dart
@@ -5,11 +5,11 @@
 import 'dart:io';
 import 'dart:isolate';
 
-import 'package:meta/meta.dart';
 import 'package:path/path.dart' as p;
 
 /// Parse and yield data cases (each a [DataCase]) from [path].
-Iterable<DataCase> dataCasesInFile({String path, String baseDir}) sync* {
+Iterable<DataCase> dataCasesInFile(
+    {required String path, String? baseDir}) sync* {
   var file = p.basename(path).replaceFirst(RegExp(r'\..+$'), '');
   baseDir ??= p.relative(p.dirname(path), from: p.dirname(p.dirname(path)));
 
@@ -61,7 +61,7 @@
 /// cases are read from files located immediately in [directory], or
 /// recursively, according to [recursive].
 Iterable<DataCase> _dataCases({
-  String directory,
+  required String directory,
   String extension = 'unit',
   bool recursive = true,
 }) {
@@ -113,12 +113,12 @@
 /// }
 /// ```
 Stream<DataCase> dataCasesUnder({
-  @required String testDirectory,
+  required String testDirectory,
   String extension = 'unit',
   bool recursive = true,
 }) async* {
   var markdownLibRoot = p.dirname((await Isolate.resolvePackageUri(
-          Uri.parse('package:markdown/markdown.dart')))
+          Uri.parse('package:markdown/markdown.dart')))!
       .path);
   var directory =
       p.joinAll([p.dirname(markdownLibRoot), 'test', testDirectory]);
@@ -142,14 +142,14 @@
   final String expectedOutput;
 
   DataCase({
-    this.directory,
-    this.file,
+    this.directory = '',
+    this.file = '',
     // ignore: non_constant_identifier_names
-    this.front_matter,
-    this.description,
-    this.skip,
-    this.input,
-    this.expectedOutput,
+    this.front_matter = '',
+    this.description = '',
+    this.skip = false,
+    required this.input,
+    required this.expectedOutput,
   });
 
   /// A good standard description for `test()`, derived from the data directory,
diff --git a/tool/stats.dart b/tool/stats.dart
index a5c9a3f..b8b4e76 100644
--- a/tool/stats.dart
+++ b/tool/stats.dart
@@ -1,3 +1,9 @@
+// Copyright (c) 2020, 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.
+
+// @dart=2.9
+
 import 'dart:async';
 import 'dart:collection';
 import 'dart:convert';
diff --git a/tool/stats_lib.dart b/tool/stats_lib.dart
index 11549df..a4a7e20 100644
--- a/tool/stats_lib.dart
+++ b/tool/stats_lib.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart=2.9
+
 import 'dart:convert';
 import 'dart:io';
 import 'dart:mirrors';