Merge the null_safety branch into master  (#56)

diff --git a/.travis.yml b/.travis.yml
index 9738ed5..8361c62 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,24 +1,38 @@
 language: dart
 
 dart:
-  - dev
-  - 2.6.0
+- dev
 
-dart_task:
-  - test: --platform vm,chrome
-
-matrix:
+jobs:
   include:
-    # Only validate formatting using the dev release
-    - dart: dev
-      dart_task: dartfmt
-    - dart: dev
-      dart_task:
-        dartanalyzer: --fatal-warnings --fatal-infos .
+    - stage: analyze_and_format
+      name: "Analyze test/"
+      dart: dev
+      os: linux
+      script: dartanalyzer --enable-experiment=non-nullable --fatal-warnings --fatal-infos .
+    - stage: analyze_and_format
+      name: "Format"
+      dart: dev
+      os: linux
+      script: dartfmt -n --set-exit-if-changed .
+    - stage: test
+      name: "Vm Tests"
+      dart: dev
+      os: linux
+      script: pub run --enable-experiment=non-nullable test -p vm
+    - stage: test
+      name: "Web Tests"
+      dart: dev
+      os: linux
+      script: pub run --enable-experiment=non-nullable test -p chrome
+
+stages:
+  - analyze_and_format
+  - test
 
 # Only building master means that we don't run two builds for each pull request.
 branches:
-  only: [master]
+  only: [master, null_safety]
 
 cache:
  directories:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5da330e..1723b0f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+# 1.8.0-nullsafety
+
+* Migrate to null safety.
+  * Apis have been migrated to reflect the existing assumptions in the code
+    and are not expected to be breaking.
+    
 # 1.7.0
 
 * Add a `SourceSpan.subspan()` extension method which returns a slice of an
diff --git a/analysis_options.yaml b/analysis_options.yaml
index ab5a4f2..ced2e21 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -2,6 +2,8 @@
 analyzer:
   strong-mode:
     implicit-casts: false
+  enable-experiment:
+    - non-nullable
 linter:
   rules:
   - always_declare_return_types
diff --git a/lib/src/file.dart b/lib/src/file.dart
index 9ad595c..6fbcd41 100644
--- a/lib/src/file.dart
+++ b/lib/src/file.dart
@@ -23,7 +23,7 @@
   /// The URL where the source file is located.
   ///
   /// This may be null, indicating that the URL is unknown or unavailable.
-  final Uri url;
+  final Uri? url;
 
   /// An array of offsets for each line beginning in the file.
   ///
@@ -47,7 +47,7 @@
   /// increasing offsets. In that case, we can find the line for an offset
   /// quickly by first checking to see if the offset is on the same line as the
   /// previous result.
-  int _cachedLine;
+  int? _cachedLine;
 
   /// This constructor is deprecated.
   ///
@@ -71,7 +71,7 @@
   /// forwards-compatibility, callers should only pass in characters less than
   /// or equal to `0xFFFF`.
   SourceFile.decoded(Iterable<int> decodedChars, {url})
-      : url = url is String ? Uri.parse(url) : url as Uri,
+      : url = url is String ? Uri.parse(url) : url as Uri?,
         _decodedChars = Uint32List.fromList(decodedChars.toList()) {
     for (var i = 0; i < _decodedChars.length; i++) {
       var c = _decodedChars[i];
@@ -87,7 +87,7 @@
   /// Returns a span from [start] to [end] (exclusive).
   ///
   /// If [end] isn't passed, it defaults to the end of the file.
-  FileSpan span(int start, [int end]) {
+  FileSpan span(int start, [int? end]) {
     end ??= length;
     return _FileSpan(this, start, end);
   }
@@ -107,10 +107,10 @@
     if (offset < _lineStarts.first) return -1;
     if (offset >= _lineStarts.last) return _lineStarts.length - 1;
 
-    if (_isNearCachedLine(offset)) return _cachedLine;
+    if (_isNearCachedLine(offset)) return _cachedLine!;
 
     _cachedLine = _binarySearch(offset) - 1;
-    return _cachedLine;
+    return _cachedLine!;
   }
 
   /// Returns `true` if [offset] is near [_cachedLine].
@@ -119,20 +119,21 @@
   /// updates [_cachedLine] to point to that.
   bool _isNearCachedLine(int offset) {
     if (_cachedLine == null) return false;
+    final cachedLine = _cachedLine!;
 
     // See if it's before the cached line.
-    if (offset < _lineStarts[_cachedLine]) return false;
+    if (offset < _lineStarts[cachedLine]) return false;
 
     // See if it's on the cached line.
-    if (_cachedLine >= _lineStarts.length - 1 ||
-        offset < _lineStarts[_cachedLine + 1]) {
+    if (cachedLine >= _lineStarts.length - 1 ||
+        offset < _lineStarts[cachedLine + 1]) {
       return true;
     }
 
     // See if it's on the next line.
-    if (_cachedLine >= _lineStarts.length - 2 ||
-        offset < _lineStarts[_cachedLine + 2]) {
-      _cachedLine++;
+    if (cachedLine >= _lineStarts.length - 2 ||
+        offset < _lineStarts[cachedLine + 2]) {
+      _cachedLine = cachedLine + 1;
       return true;
     }
 
@@ -161,7 +162,7 @@
   ///
   /// If [line] is passed, it's assumed to be the line containing [offset] and
   /// is used to more efficiently compute the column.
-  int getColumn(int offset, {int line}) {
+  int getColumn(int offset, {int? line}) {
     if (offset < 0) {
       throw RangeError('Offset may not be negative, was $offset.');
     } else if (offset > length) {
@@ -189,7 +190,7 @@
   /// Gets the offset for a [line] and [column].
   ///
   /// [column] defaults to 0.
-  int getOffset(int line, [int column]) {
+  int getOffset(int line, [int? column]) {
     column ??= 0;
 
     if (line < 0) {
@@ -213,7 +214,7 @@
   /// Returns the text of the file from [start] to [end] (exclusive).
   ///
   /// If [end] isn't passed, it defaults to the end of the file.
-  String getText(int start, [int end]) =>
+  String getText(int start, [int? end]) =>
       String.fromCharCodes(_decodedChars.sublist(start, end));
 }
 
@@ -231,7 +232,7 @@
   final int offset;
 
   @override
-  Uri get sourceUrl => file.url;
+  Uri? get sourceUrl => file.url;
 
   @override
   int get line => file.getLine(offset);
@@ -299,7 +300,7 @@
   final int _end;
 
   @override
-  Uri get sourceUrl => file.url;
+  Uri? get sourceUrl => file.url;
 
   @override
   int get length => _end - _start;
@@ -318,7 +319,7 @@
     final endLine = file.getLine(_end);
     final endColumn = file.getColumn(_end);
 
-    int endOffset;
+    int? endOffset;
     if (endColumn == 0 && endLine != 0) {
       // If [end] is at the very beginning of the line, the span covers the
       // previous newline, so we only want to include the previous line in the
@@ -362,16 +363,15 @@
   int compareTo(SourceSpan other) {
     if (other is! _FileSpan) return super.compareTo(other);
 
-    final otherFile = other as _FileSpan;
-    final result = _start.compareTo(otherFile._start);
-    return result == 0 ? _end.compareTo(otherFile._end) : result;
+    final result = _start.compareTo(other._start);
+    return result == 0 ? _end.compareTo(other._end) : result;
   }
 
   @override
   SourceSpan union(SourceSpan other) {
     if (other is! FileSpan) return super.union(other);
 
-    final span = expand(other as _FileSpan);
+    final span = expand(other);
 
     if (other is _FileSpan) {
       if (_start > other._end || other._start > _end) {
@@ -425,7 +425,7 @@
   }
 
   /// See `SourceSpanExtension.subspan`.
-  FileSpan subspan(int start, [int end]) {
+  FileSpan subspan(int start, [int? end]) {
     RangeError.checkValidRange(start, end, length);
     if (start == 0 && (end == null || end == length)) return this;
     return file.span(_start + start, end == null ? _end : _start + end);
@@ -436,7 +436,7 @@
 /// Extension methods on the [FileSpan] API.
 extension FileSpanExtension on FileSpan {
   /// See `SourceSpanExtension.subspan`.
-  FileSpan subspan(int start, [int end]) {
+  FileSpan subspan(int start, [int? end]) {
     RangeError.checkValidRange(start, end, length);
     if (start == 0 && (end == null || end == length)) return this;
 
diff --git a/lib/src/highlighter.dart b/lib/src/highlighter.dart
index cef6f79..491dae5 100644
--- a/lib/src/highlighter.dart
+++ b/lib/src/highlighter.dart
@@ -6,7 +6,6 @@
 
 import 'package:charcode/charcode.dart';
 import 'package:collection/collection.dart';
-import 'package:meta/meta.dart';
 import 'package:path/path.dart' as p;
 import 'package:term_glyph/term_glyph.dart' as glyph;
 
@@ -23,11 +22,11 @@
 
   /// The color to highlight the primary [_Highlight] within its context, or
   /// `null` if it should not be colored.
-  final String _primaryColor;
+  final String? _primaryColor;
 
   /// The color to highlight the secondary [_Highlight]s within their context,
   /// or `null` if they should not be colored.
-  final String _secondaryColor;
+  final String? _secondaryColor;
 
   /// The number of characters before the bar in the sidebar.
   final int _paddingBeforeSidebar;
@@ -63,7 +62,7 @@
       : this._(_collateLines([_Highlight(span, primary: true)]), () {
           if (color == true) return colors.red;
           if (color == false) return null;
-          return color as String;
+          return color as String?;
         }(), null);
 
   /// Creates a [Highlighter] that will return a string highlighting
@@ -83,7 +82,7 @@
   /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
   Highlighter.multiple(SourceSpan primarySpan, String primaryLabel,
       Map<SourceSpan, String> secondarySpans,
-      {bool color = false, String primaryColor, String secondaryColor})
+      {bool color = false, String? primaryColor, String? secondaryColor})
       : this._(
             _collateLines([
               _Highlight(primarySpan, label: primaryLabel, primary: true),
@@ -108,7 +107,8 @@
                 .where((highlight) => isMultiline(highlight.span))
                 .length)
             .reduce(math.max),
-        _multipleFiles = !isAllTheSame(_lines.map((line) => line.url));
+        _multipleFiles =
+            !isAllTheSame(_lines.map((line) => line.url).whereType<Uri>());
 
   /// Returns whether [lines] contains any adjacent lines from the same source
   /// file that aren't adjacent in the original file.
@@ -127,8 +127,8 @@
   /// Collect all the source lines from the contexts of all spans in
   /// [highlights], and associates them with the highlights that cover them.
   static List<_Line> _collateLines(List<_Highlight> highlights) {
-    final highlightsByUrl =
-        groupBy(highlights, (highlight) => highlight.span.sourceUrl);
+    final highlightsByUrl = groupBy<_Highlight, Uri?>(
+        highlights, (highlight) => highlight.span.sourceUrl);
     for (var list in highlightsByUrl.values) {
       list.sort((highlight1, highlight2) =>
           highlight1.span.compareTo(highlight2.span));
@@ -143,8 +143,7 @@
         // If [highlight.span.context] contains lines prior to the one
         // [highlight.span.text] appears on, write those first.
         final lineStart = findLineStart(
-            context, highlight.span.text, highlight.span.start.column);
-        assert(lineStart != null); // enforced by [_normalizeContext]
+            context, highlight.span.text, highlight.span.start.column)!;
 
         final linesBeforeSpan =
             '\n'.allMatches(context.substring(0, lineStart)).length;
@@ -192,7 +191,8 @@
     // Each index of this list represents a column after the sidebar that could
     // contain a line indicating an active highlight. If it's `null`, that
     // column is empty; if it contains a highlight, it should be drawn for that column.
-    final highlightsByColumn = List<_Highlight>(_maxMultilineSpans);
+    final highlightsByColumn =
+        List<_Highlight?>.filled(_maxMultilineSpans, null);
 
     for (var i = 0; i < _lines.length; i++) {
       final line = _lines[i];
@@ -226,8 +226,10 @@
       _writeMultilineHighlights(line, highlightsByColumn);
       if (highlightsByColumn.isNotEmpty) _buffer.write(' ');
 
-      final primary = line.highlights
-          .firstWhere((highlight) => highlight.isPrimary, orElse: () => null);
+      final primaryIdx =
+          line.highlights.indexWhere((highlight) => highlight.isPrimary);
+      final primary = primaryIdx == -1 ? null : line.highlights[primaryIdx];
+
       if (primary != null) {
         _writeHighlightedText(
             line.text,
@@ -258,7 +260,7 @@
 
   /// Writes the beginning of the file highlight for the file with the given
   /// [url].
-  void _writeFileStart(Uri url) {
+  void _writeFileStart(Uri? url) {
     if (!_multipleFiles || url == null) {
       _writeSidebar(end: glyph.downEnd);
     } else {
@@ -277,20 +279,20 @@
   /// written. If it appears in [highlightsByColumn], a horizontal line is
   /// written from its column to the rightmost column.
   void _writeMultilineHighlights(
-      _Line line, List<_Highlight> highlightsByColumn,
-      {_Highlight current}) {
+      _Line line, List<_Highlight?> highlightsByColumn,
+      {_Highlight? current}) {
     // Whether we've written a sidebar indicator for opening a new span on this
     // line, and which color should be used for that indicator's rightward line.
     var openedOnThisLine = false;
-    String openedOnThisLineColor;
+    String? openedOnThisLineColor;
 
     final currentColor = current == null
         ? null
         : current.isPrimary ? _primaryColor : _secondaryColor;
     var foundCurrent = false;
     for (var highlight in highlightsByColumn) {
-      final startLine = highlight?.span?.start?.line;
-      final endLine = highlight?.span?.end?.line;
+      final startLine = highlight?.span.start.line;
+      final endLine = highlight?.span.end.line;
       if (current != null && highlight == current) {
         foundCurrent = true;
         assert(startLine == line.number || endLine == line.number);
@@ -322,9 +324,9 @@
             }, color: openedOnThisLineColor);
             openedOnThisLine = true;
             openedOnThisLineColor ??=
-                highlight.isPrimary ? _primaryColor : _secondaryColor;
+                highlight!.isPrimary ? _primaryColor : _secondaryColor;
           } else if (endLine == line.number &&
-              highlight.span.end.column == line.text.length) {
+              highlight!.span.end.column == line.text.length) {
             _buffer.write(highlight.label == null
                 ? glyph.glyphOrAscii('└', '\\')
                 : vertical);
@@ -341,7 +343,7 @@
   // Writes [text], with text between [startColumn] and [endColumn] colorized in
   // the same way as [_colorize].
   void _writeHighlightedText(String text, int startColumn, int endColumn,
-      {@required String color}) {
+      {required String? color}) {
     _writeText(text.substring(0, startColumn));
     _colorize(() => _writeText(text.substring(startColumn, endColumn)),
         color: color);
@@ -353,7 +355,7 @@
   ///
   /// This may either add or remove [highlight] from [highlightsByColumn].
   void _writeIndicator(
-      _Line line, _Highlight highlight, List<_Highlight> highlightsByColumn) {
+      _Line line, _Highlight highlight, List<_Highlight?> highlightsByColumn) {
     final color = highlight.isPrimary ? _primaryColor : _secondaryColor;
     if (!isMultiline(highlight.span)) {
       _writeSidebar();
@@ -436,7 +438,7 @@
   }
 
   /// Writes a space followed by [label] if [label] isn't `null`.
-  void _writeLabel(String label) {
+  void _writeLabel(String? label) {
     if (label != null) _buffer.write(' $label');
   }
 
@@ -457,7 +459,7 @@
   //
   // If [text] is given, it's used in place of the line number. It can't be
   // passed at the same time as [line].
-  void _writeSidebar({int line, String text, String end}) {
+  void _writeSidebar({int? line, String? text, String? end}) {
     assert(line == null || text == null);
 
     // Add 1 to line to convert from computer-friendly 0-indexed line numbers to
@@ -489,7 +491,7 @@
 
   /// Colors all text written to [_buffer] during [callback], if colorization is
   /// enabled and [color] is not `null`.
-  void _colorize(void Function() callback, {@required String color}) {
+  void _colorize(void Function() callback, {required String? color}) {
     if (_primaryColor != null && color != null) _buffer.write(color);
     callback();
     if (_primaryColor != null && color != null) _buffer.write(colors.none);
@@ -513,7 +515,7 @@
   ///
   /// This helps distinguish clarify what each highlight means when multiple are
   /// used in the same message.
-  final String label;
+  final String? label;
 
   _Highlight(SourceSpan span, {this.label, bool primary = false})
       : span = (() {
@@ -636,7 +638,7 @@
 
   /// Returns whether [span]'s text runs all the way to the end of its context.
   static bool _isTextAtEndOfContext(SourceSpanWithContext span) =>
-      findLineStart(span.context, span.text, span.start.column) +
+      findLineStart(span.context, span.text, span.start.column)! +
           span.start.column +
           span.length ==
       span.context.length;
@@ -661,7 +663,7 @@
   final int number;
 
   /// The URL of the source file in which this line appears.
-  final Uri url;
+  final Uri? url;
 
   /// All highlights that cover any portion of this line, in source span order.
   ///
diff --git a/lib/src/location.dart b/lib/src/location.dart
index 93942f0..0979618 100644
--- a/lib/src/location.dart
+++ b/lib/src/location.dart
@@ -16,7 +16,7 @@
   ///
   /// This may be null, indicating that the source URL is unknown or
   /// unavailable.
-  final Uri sourceUrl;
+  final Uri? sourceUrl;
 
   /// The 0-based offset of this location in the source.
   final int offset;
@@ -42,9 +42,9 @@
   /// means that [line] defaults to 0 and [column] defaults to [offset].
   ///
   /// [sourceUrl] may be either a [String], a [Uri], or `null`.
-  SourceLocation(this.offset, {sourceUrl, int line, int column})
+  SourceLocation(this.offset, {sourceUrl, int? line, int? column})
       : sourceUrl =
-            sourceUrl is String ? Uri.parse(sourceUrl) : sourceUrl as Uri,
+            sourceUrl is String ? Uri.parse(sourceUrl) : sourceUrl as Uri?,
         line = line ?? 0,
         column = column ?? offset {
     if (offset < 0) {
@@ -89,7 +89,7 @@
       offset == other.offset;
 
   @override
-  int get hashCode => sourceUrl.hashCode + offset;
+  int get hashCode => (sourceUrl?.hashCode ?? 0) + offset;
 
   @override
   String toString() => '<$runtimeType: $offset $toolString>';
@@ -98,6 +98,6 @@
 /// A base class for source locations with [offset], [line], and [column] known
 /// at construction time.
 class SourceLocationBase extends SourceLocation {
-  SourceLocationBase(int offset, {sourceUrl, int line, int column})
+  SourceLocationBase(int offset, {sourceUrl, int? line, int? column})
       : super(offset, sourceUrl: sourceUrl, line: line, column: column);
 }
diff --git a/lib/src/location_mixin.dart b/lib/src/location_mixin.dart
index a54e363..9a1f930 100644
--- a/lib/src/location_mixin.dart
+++ b/lib/src/location_mixin.dart
@@ -48,7 +48,7 @@
       offset == other.offset;
 
   @override
-  int get hashCode => sourceUrl.hashCode + offset;
+  int get hashCode => (sourceUrl?.hashCode ?? 0) + offset;
 
   @override
   String toString() => '<$runtimeType: $offset $toolString>';
diff --git a/lib/src/span.dart b/lib/src/span.dart
index 15f0d34..94c05ba 100644
--- a/lib/src/span.dart
+++ b/lib/src/span.dart
@@ -27,7 +27,7 @@
   ///
   /// This may be null, indicating that the source URL is unknown or
   /// unavailable.
-  Uri get sourceUrl;
+  Uri? get sourceUrl;
 
   /// The length of this span, in characters.
   int get length;
@@ -139,7 +139,7 @@
   /// [FileSpan]s).
   String messageMultiple(
       String message, String label, Map<SourceSpan, String> secondarySpans,
-      {bool color = false, String primaryColor, String secondaryColor}) {
+      {bool color = false, String? primaryColor, String? secondaryColor}) {
     final buffer = StringBuffer()
       ..write('line ${start.line + 1}, column ${start.column + 1}');
     if (sourceUrl != null) buffer.write(' of ${p.prettyUri(sourceUrl)}');
@@ -174,7 +174,7 @@
   /// much more useful output with [SourceSpanWithContext]s (including
   /// [FileSpan]s).
   String highlightMultiple(String label, Map<SourceSpan, String> secondarySpans,
-          {bool color = false, String primaryColor, String secondaryColor}) =>
+          {bool color = false, String? primaryColor, String? secondaryColor}) =>
       Highlighter.multiple(this, label, secondarySpans,
               color: color,
               primaryColor: primaryColor,
@@ -183,7 +183,7 @@
 
   /// Returns a span from [start] code units (inclusive) to [end] code units
   /// (exclusive) after the beginning of this span.
-  SourceSpan subspan(int start, [int end]) {
+  SourceSpan subspan(int start, [int? end]) {
     RangeError.checkValidRange(start, end, length);
     if (start == 0 && (end == null || end == length)) return this;
 
@@ -220,7 +220,7 @@
       newEndLocation = this.end;
     } else if (end == start) {
       newEndLocation = newStartLocation;
-    } else if (end != null && end != length) {
+    } else {
       for (var i = start; i < end; i++) {
         consumeCodePoint(i);
       }
diff --git a/lib/src/span_exception.dart b/lib/src/span_exception.dart
index 32aaa4e..c6db03b 100644
--- a/lib/src/span_exception.dart
+++ b/lib/src/span_exception.dart
@@ -15,8 +15,8 @@
   /// The span associated with this exception.
   ///
   /// This may be `null` if the source location can't be determined.
-  SourceSpan get span => _span;
-  final SourceSpan _span;
+  SourceSpan? get span => _span;
+  final SourceSpan? _span;
 
   SourceSpanException(this._message, this._span);
 
@@ -30,7 +30,7 @@
   @override
   String toString({color}) {
     if (span == null) return message;
-    return 'Error on ${span.message(message, color: color)}';
+    return 'Error on ${span!.message(message, color: color)}';
   }
 }
 
@@ -41,9 +41,9 @@
   final dynamic source;
 
   @override
-  int get offset => span?.start?.offset;
+  int? get offset => span?.start.offset;
 
-  SourceSpanFormatException(String message, SourceSpan span, [this.source])
+  SourceSpanFormatException(String message, SourceSpan? span, [this.source])
       : super(message, span);
 }
 
@@ -64,7 +64,7 @@
   /// additional information and helps distinguish it from [secondarySpans].
   final Map<SourceSpan, String> secondarySpans;
 
-  MultiSourceSpanException(String message, SourceSpan span, this.primaryLabel,
+  MultiSourceSpanException(String message, SourceSpan? span, this.primaryLabel,
       Map<SourceSpan, String> secondarySpans)
       : secondarySpans = Map.unmodifiable(secondarySpans),
         super(message, span);
@@ -80,11 +80,11 @@
   /// If [color] is `true` or a string, [secondaryColor] is used to highlight
   /// [secondarySpans].
   @override
-  String toString({color, String secondaryColor}) {
+  String toString({color, String? secondaryColor}) {
     if (span == null) return message;
 
     var useColor = false;
-    String primaryColor;
+    String? primaryColor;
     if (color is String) {
       useColor = true;
       primaryColor = color;
@@ -92,7 +92,7 @@
       useColor = true;
     }
 
-    final formatted = span.messageMultiple(
+    final formatted = span!.messageMultiple(
         message, primaryLabel, secondarySpans,
         color: useColor,
         primaryColor: primaryColor,
@@ -108,9 +108,9 @@
   final dynamic source;
 
   @override
-  int get offset => span?.start?.offset;
+  int? get offset => span?.start.offset;
 
-  MultiSourceSpanFormatException(String message, SourceSpan span,
+  MultiSourceSpanFormatException(String message, SourceSpan? span,
       String primaryLabel, Map<SourceSpan, String> secondarySpans,
       [this.source])
       : super(message, span, primaryLabel, secondarySpans);
diff --git a/lib/src/span_mixin.dart b/lib/src/span_mixin.dart
index 55eae94..0f751ea 100644
--- a/lib/src/span_mixin.dart
+++ b/lib/src/span_mixin.dart
@@ -17,7 +17,7 @@
 /// to the distance between [start] and [end].
 abstract class SourceSpanMixin implements SourceSpan {
   @override
-  Uri get sourceUrl => start.sourceUrl;
+  Uri? get sourceUrl => start.sourceUrl;
 
   @override
   int get length => end.offset - start.offset;
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 5cb5e90..72c6173 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -16,10 +16,8 @@
 
 /// Returns whether all elements of [iter] are the same value, according to
 /// `==`.
-///
-/// Assumes [iter] doesn't contain any `null` values.
 bool isAllTheSame(Iterable<Object> iter) {
-  Object lastValue;
+  Object? lastValue;
   for (var value in iter) {
     if (lastValue == null) {
       lastValue = value;
@@ -34,14 +32,14 @@
 bool isMultiline(SourceSpan span) => span.start.line != span.end.line;
 
 /// Sets the first `null` element of [list] to [element].
-void replaceFirstNull<E>(List<E> list, E element) {
+void replaceFirstNull<E>(List<E?> list, E element) {
   final index = list.indexOf(null);
   if (index < 0) throw ArgumentError('$list contains no null elements.');
   list[index] = element;
 }
 
 /// Sets the element of [list] that currently contains [element] to `null`.
-void replaceWithNull<E>(List<E> list, E element) {
+void replaceWithNull<E>(List<E?> list, E element) {
   final index = list.indexOf(element);
   if (index < 0) {
     throw ArgumentError('$list contains no elements matching $element.');
@@ -63,7 +61,7 @@
 ///
 /// Returns the index in [context] where that line begins, or null if none
 /// exists.
-int findLineStart(String context, String text, int column) {
+int? findLineStart(String context, String text, int column) {
   // If the text is empty, we just want to find the first line that has at least
   // [column] characters.
   if (text.isEmpty) {
diff --git a/pubspec.yaml b/pubspec.yaml
index 126d9c0..89f40c8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,18 +1,92 @@
 name: source_span
-version: 1.7.0
+version: 1.8.0-nullsafety
 
 description: A library for identifying source spans and locations.
 homepage: https://github.com/dart-lang/source_span
 
 environment:
-  sdk: '>=2.6.0 <3.0.0'
+  sdk: '>=2.9.0-18.0 <2.9.0'
 
 dependencies:
-  charcode: ^1.0.0
-  collection: ^1.8.0
-  meta: '>=0.9.0 <2.0.0'
-  path: '>=1.2.0 <2.0.0'
-  term_glyph: ^1.0.0
+  charcode: '>=1.2.0-nullsafety <1.2.0'
+  collection: '>=1.15.0-nullsafety <1.15.0'
+  path: '>=1.8.0-nullsafety <1.8.0'
+  term_glyph: '>=1.2.0-nullsafety <1.2.0'
 
 dev_dependencies:
   test: ^1.6.0
+
+dependency_overrides:
+  async:
+    git:
+      url: git://github.com/dart-lang/async.git
+      ref: null_safety
+  boolean_selector:
+    git:
+      url: git://github.com/dart-lang/boolean_selector.git
+      ref: null_safety
+  charcode:
+    git:
+      url: git://github.com/dart-lang/charcode.git
+      ref: null_safety
+  collection: 1.15.0-nullsafety
+  js:
+    git:
+      url: git://github.com/dart-lang/sdk.git
+      path: pkg/js
+  matcher:
+    git:
+      url: git://github.com/dart-lang/matcher.git
+      ref: null_safety
+  meta: 1.3.0-nullsafety
+  path:
+    git:
+      url: git://github.com/dart-lang/path.git
+      ref: null_safety
+  pedantic:
+    git:
+      url: git://github.com/dart-lang/pedantic.git
+      ref: null_safety
+  pool:
+    git:
+      url: git://github.com/dart-lang/pool.git
+      ref: null_safety
+  source_maps:
+    git:
+      url: git://github.com/dart-lang/source_maps.git
+      ref: null_safety
+  source_map_stack_trace:
+    git:
+      url: git://github.com/dart-lang/source_map_stack_trace.git
+      ref: null_safety
+  stack_trace:
+    git:
+      url: git://github.com/dart-lang/stack_trace.git
+      ref: null_safety
+  stream_channel:
+    git:
+      url: git://github.com/dart-lang/stream_channel.git
+      ref: null_safety
+  string_scanner:
+    git:
+      url: git://github.com/dart-lang/string_scanner.git
+      ref: null_safety
+  term_glyph:
+    git:
+      url: git://github.com/dart-lang/term_glyph.git
+      ref: null_safety
+  test_api:
+    git:
+      url: git://github.com/dart-lang/test.git
+      ref: null_safety
+      path: pkgs/test_api
+  test_core:
+    git:
+      url: git://github.com/dart-lang/test.git
+      ref: null_safety
+      path: pkgs/test_core
+  test:
+    git:
+      url: git://github.com/dart-lang/test.git
+      ref: null_safety
+      path: pkgs/test
diff --git a/test/file_test.dart b/test/file_test.dart
index 63b523f..581f03f 100644
--- a/test/file_test.dart
+++ b/test/file_test.dart
@@ -6,7 +6,7 @@
 import 'package:source_span/source_span.dart';
 
 void main() {
-  SourceFile file;
+  late SourceFile file;
   setUp(() {
     file = SourceFile.fromString('''
 foo bar baz
@@ -295,7 +295,7 @@
     });
 
     group('union()', () {
-      FileSpan span;
+      late FileSpan span;
       setUp(() {
         span = file.span(5, 12);
       });
@@ -358,7 +358,7 @@
     });
 
     group('expand()', () {
-      FileSpan span;
+      late FileSpan span;
       setUp(() {
         span = file.span(5, 12);
       });
@@ -407,7 +407,7 @@
     });
 
     group('subspan()', () {
-      FileSpan span;
+      late FileSpan span;
       setUp(() {
         span = file.span(5, 11); // "ar baz"
       });
diff --git a/test/highlight_test.dart b/test/highlight_test.dart
index 6313108..7754d00 100644
--- a/test/highlight_test.dart
+++ b/test/highlight_test.dart
@@ -10,7 +10,7 @@
 import 'package:test/test.dart';
 
 void main() {
-  bool oldAscii;
+  late bool oldAscii;
   setUpAll(() {
     oldAscii = glyph.ascii;
     glyph.ascii = true;
@@ -20,7 +20,7 @@
     glyph.ascii = oldAscii;
   });
 
-  SourceFile file;
+  late SourceFile file;
   setUp(() {
     file = SourceFile.fromString('''
 foo bar baz
diff --git a/test/location_test.dart b/test/location_test.dart
index c22a148..bbe259b 100644
--- a/test/location_test.dart
+++ b/test/location_test.dart
@@ -6,7 +6,7 @@
 import 'package:test/test.dart';
 
 void main() {
-  SourceLocation location;
+  late SourceLocation location;
   setUp(() {
     location = SourceLocation(15, line: 2, column: 6, sourceUrl: 'foo.dart');
   });
diff --git a/test/multiple_highlight_test.dart b/test/multiple_highlight_test.dart
index d9d1b30..6d5ec9d 100644
--- a/test/multiple_highlight_test.dart
+++ b/test/multiple_highlight_test.dart
@@ -7,7 +7,7 @@
 import 'package:test/test.dart';
 
 void main() {
-  bool oldAscii;
+  late bool oldAscii;
   setUpAll(() {
     oldAscii = glyph.ascii;
     glyph.ascii = true;
@@ -17,7 +17,7 @@
     glyph.ascii = oldAscii;
   });
 
-  SourceFile file;
+  late SourceFile file;
   setUp(() {
     file = SourceFile.fromString('''
 foo bar baz
diff --git a/test/span_test.dart b/test/span_test.dart
index 838d6b7..c27048e 100644
--- a/test/span_test.dart
+++ b/test/span_test.dart
@@ -9,7 +9,8 @@
 import 'package:source_span/src/colors.dart' as colors;
 
 void main() {
-  bool oldAscii;
+  late bool oldAscii;
+
   setUpAll(() {
     oldAscii = glyph.ascii;
     glyph.ascii = true;
@@ -19,7 +20,7 @@
     glyph.ascii = oldAscii;
   });
 
-  SourceSpan span;
+  late SourceSpan span;
   setUp(() {
     span = SourceSpan(SourceLocation(5, sourceUrl: 'foo.dart'),
         SourceLocation(12, sourceUrl: 'foo.dart'), 'foo bar');
diff --git a/test/utils_test.dart b/test/utils_test.dart
index a4a372e..02888df 100644
--- a/test/utils_test.dart
+++ b/test/utils_test.dart
@@ -9,26 +9,26 @@
   group('find line start', () {
     test('skip entries in wrong column', () {
       final context = '0_bb\n1_bbb\n2b____\n3bbb\n';
-      final index = findLineStart(context, 'b', 1);
+      final index = findLineStart(context, 'b', 1)!;
       expect(index, 11);
       expect(context.substring(index - 1, index + 3), '\n2b_');
     });
 
     test('end of line column for empty text', () {
       final context = '0123\n56789\nabcdefgh\n';
-      final index = findLineStart(context, '', 5);
+      final index = findLineStart(context, '', 5)!;
       expect(index, 5);
       expect(context[index], '5');
     });
 
     test('column at the end of the file for empty text', () {
       var context = '0\n2\n45\n';
-      var index = findLineStart(context, '', 2);
+      var index = findLineStart(context, '', 2)!;
       expect(index, 4);
       expect(context[index], '4');
 
       context = '0\n2\n45';
-      index = findLineStart(context, '', 2);
+      index = findLineStart(context, '', 2)!;
       expect(index, 4);
     });