diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 17cea76..ab0899f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v2
- uses: dart-lang/setup-dart@v1
- run: dart --version
- - run: pub get
+ - run: dart pub get
- run: dart analyze --fatal-warnings .
- run: dart format --set-exit-if-changed .
- run: dart run test -x integration
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5cf3c78..a787e31 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,8 +11,8 @@ jobs:
- uses: actions/checkout@v2
- uses: dart-lang/setup-dart@v1
- run: dart --version
- - run: pub get
- - run: pub global activate grinder
+ - run: dart pub get
+ - run: dart pub global activate grinder
- run: echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH
- name: Package and publish
run: grind pkg-github-all
diff --git a/.gitignore b/.gitignore
index 590b0d4..830b79e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,6 @@
# Files and directories created by pub
.buildlog
.dart_tool/
-.packages
.project
.pub/
build/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9da4241..6511af5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 3.0.0-dev
+
+- Require Dart 2.18 and support Dart 3
+- Rename library to `linkcheck` instead of `linkcheck.run`
+- Update to the latest dependencies supporting sound null safety
+- Switch to Dart recommended lints (`package:lints/recommended.yaml`)
+- Use objects instead of maps to communicate between isolates
+
## 2.0.23
- Fix another issue with building artifacts through `grindr`/`cli_pkg`
diff --git a/README.md b/README.md
index 9d5314d..07508e4 100644
--- a/README.md
+++ b/README.md
@@ -330,6 +330,6 @@ building binaries and placing a new release into
[github.com/filiph/linkcheck/releases](https://github.com/filiph/linkcheck/releases).
In order to populate it to the [GitHub Actions Marketplace](https://github.com/marketplace/actions/check-links-with-linkcheck)
-as well, it's currently requiered to manually Edit and hit
+as well, it's currently required to manually Edit and hit
Update release on the release page once. No changes needed.
(Source: [GiHub Community](https://github.community/t/automatically-publish-action-to-marketplace-on-release/17978))
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 833d692..df74cd6 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,11 +1,17 @@
-include: package:pedantic/analysis_options.yaml
+include: package:lints/recommended.yaml
analyzer:
- strong-mode:
- implicit-casts: false
- implicit-dynamic: false
+ language:
+ strict-casts: true
+ strict-inference: true
+ strict-raw-types: true
linter:
rules:
- omit_local_variable_types: false # TODO: remove and fix
- prefer_single_quotes: false
+ - always_declare_return_types
+ - comment_references
+ - prefer_final_fields
+ - type_annotate_public_apis
+ - unawaited_futures
+ - use_enums
+ - use_super_parameters
diff --git a/bin/linkcheck.dart b/bin/linkcheck.dart
index c52a027..1947955 100644
--- a/bin/linkcheck.dart
+++ b/bin/linkcheck.dart
@@ -1,5 +1,3 @@
-library linkcheck.executable;
-
import 'dart:async';
import 'dart:io';
diff --git a/dart_test.yaml b/dart_test.yaml
index 32531de..a308994 100644
--- a/dart_test.yaml
+++ b/dart_test.yaml
@@ -2,5 +2,5 @@ tags:
integration:
skip: >
The integration tests depend on serving the case folders, and for that
- we need to know the path to those (which pub run test doesn't support).
- Use `dart test/e2e_test.dart` to run these.
+ we need to know the path to those (which dart test doesn't support).
+ Use `dart run test/e2e_test.dart` to run these.
diff --git a/lib/linkcheck.dart b/lib/linkcheck.dart
index 3313796..aabf6ad 100644
--- a/lib/linkcheck.dart
+++ b/lib/linkcheck.dart
@@ -1,14 +1,12 @@
-library linkcheck.run;
-
import 'dart:async';
import 'dart:io' hide Link;
import 'package:args/args.dart';
import 'package:console/console.dart';
-import 'package:linkcheck/src/parsers/url_skipper.dart';
import 'src/crawl.dart' show crawl, CrawlResult;
import 'src/link.dart' show Link;
+import 'src/parsers/url_skipper.dart';
import 'src/writer_report.dart' show reportForWriters;
export 'src/crawl.dart' show crawl, CrawlResult;
@@ -28,7 +26,7 @@ const hostsFlag = "hosts";
const inputFlag = "input-file";
const redirectFlag = "show-redirects";
const skipFlag = "skip-file";
-const version = "2.0.23";
+const version = "3.0.0-dev";
const versionFlag = "version";
final _portOnlyRegExp = RegExp(r"^:\d+$");
@@ -224,8 +222,8 @@ Future run(List arguments, Stdout stdout) async {
bool shouldCheckExternal = argResults[externalFlag] == true;
bool showRedirects = argResults[redirectFlag] == true;
bool shouldCheckAnchors = argResults[anchorFlag] == true;
- String inputFile = argResults[inputFlag] as String;
- String skipFile = argResults[skipFlag] as String;
+ String? inputFile = argResults[inputFlag] as String?;
+ String? skipFile = argResults[skipFlag] as String?;
List urls = argResults.rest.toList();
UrlSkipper skipper = UrlSkipper.empty();
@@ -261,7 +259,7 @@ Future run(List arguments, Stdout stdout) async {
}
// TODO: exit gracefully if provided URL isn't a parseable URI
- List uris = urls.map((url) => Uri.parse(url)).toList();
+ List uris = urls.map((url) => Uri.parse(url)).toList(growable: false);
Set hosts;
if ((argResults[hostsFlag] as Iterable).isNotEmpty) {
hosts = Set.from(argResults[hostsFlag] as Iterable);
diff --git a/lib/src/crawl.dart b/lib/src/crawl.dart
index 8597e89..d25d999 100644
--- a/lib/src/crawl.dart
+++ b/lib/src/crawl.dart
@@ -1,18 +1,17 @@
-library linkcheck.crawl;
-
import 'dart:async';
import 'dart:collection';
import 'dart:io' show Stdout;
import 'package:console/console.dart';
+import 'package:meta/meta.dart';
import 'destination.dart';
import 'link.dart';
-import 'package:linkcheck/src/parsers/url_skipper.dart';
-import 'uri_glob.dart';
+import 'parsers/url_skipper.dart';
import 'server_info.dart';
-import 'worker/pool.dart';
+import 'uri_glob.dart';
import 'worker/fetch_results.dart';
+import 'worker/pool.dart';
/// Number of isolates to create by default.
const defaultThreads = 8;
@@ -36,8 +35,8 @@ Future crawl(
// Redirect output to injected [stdout] for better testing.
void print(Object message) => stdout.writeln(message);
- Cursor cursor;
- TextPen pen;
+ Cursor? cursor;
+ TextPen? pen;
if (ansiTerm) {
Console.init();
cursor = Cursor();
@@ -64,7 +63,9 @@ Future crawl(
..isSource = true
..isExternal = false)
.toSet());
- open.forEach((destination) => bin[destination.url] = Bin.open);
+ for (var destination in open) {
+ bin[destination.url] = Bin.open;
+ }
// Queue for the external destinations.
Queue openExternal = Queue();
@@ -106,7 +107,7 @@ Future crawl(
int count = 0;
if (!verbose) {
- if (ansiTerm) {
+ if (cursor != null) {
cursor.write("Crawling: $count");
} else {
print("Crawling...");
@@ -116,12 +117,12 @@ Future crawl(
// TODO:
// - --cache for creating a .linkcheck.cache file
- var allDone = Completer();
+ var allDone = Completer();
// Respond to Ctrl-C
- StreamSubscription stopSignalSubscription;
+ late final StreamSubscription stopSignalSubscription;
stopSignalSubscription = stopSignal.listen((dynamic _) async {
- if (ansiTerm) {
+ if (pen != null) {
pen
.text("\n")
.red()
@@ -148,11 +149,11 @@ Future crawl(
}
}
- bool _serverIsKnown(Destination destination) =>
+ bool serverIsKnown(Destination destination) =>
servers.keys.contains(destination.uri.authority);
Iterable availableDestinations =
- _zip(open.where(_serverIsKnown), openExternal.where(_serverIsKnown));
+ _zip(open.where(serverIsKnown), openExternal.where(serverIsKnown));
// In order not to touch the underlying iterables, we keep track
// of the destinations we want to remove.
@@ -164,8 +165,8 @@ Future crawl(
destinationsToRemove.add(destination);
String host = destination.uri.authority;
- ServerInfo server = servers[host];
- if (server.hasNotConnected) {
+ ServerInfo? server = servers[host];
+ if (server == null || server.hasNotConnected) {
destination.didNotConnect = true;
closed.add(destination);
bin[destination.url] = Bin.closed;
@@ -176,8 +177,9 @@ Future crawl(
continue;
}
- if (server.bouncer != null &&
- !server.bouncer.allows(destination.uri.path)) {
+ var serverBouncer = server.bouncer;
+ if (serverBouncer != null &&
+ !serverBouncer.allows(destination.uri.path)) {
destination.wasDeniedByRobotsTxt = true;
closed.add(destination);
bin[destination.url] = Bin.closed;
@@ -236,7 +238,7 @@ Future crawl(
"${result.didNotConnect ? 'didn\'t connect' : 'connected'}, "
"${result.robotsTxtContents.isEmpty ? 'no robots.txt' : 'robots.txt found'}.");
} else {
- if (ansiTerm) {
+ if (cursor != null) {
cursor.moveLeft(count.toString().length);
count += 1;
cursor.write(count.toString());
@@ -259,7 +261,7 @@ Future crawl(
if (destinations.isEmpty) {
if (verbose) {
print("WARNING: Received result for a destination that isn't in "
- "the inProgress set: ${result.toMap()}");
+ "the inProgress set: $result");
var isInOpen =
open.where((dest) => dest.url == result.checked.url).isNotEmpty;
var isInOpenExternal = openExternal
@@ -287,12 +289,12 @@ Future crawl(
if (verbose) {
count += 1;
print("Done checking: $checked (${checked.statusDescription}) "
- "=> ${result?.links?.length ?? 0} links");
+ "=> ${result.links.length} links");
if (checked.isBroken) {
print("- BROKEN");
}
} else {
- if (ansiTerm) {
+ if (cursor != null) {
cursor.moveLeft(count.toString().length);
count += 1;
cursor.write(count.toString());
@@ -436,11 +438,10 @@ Future crawl(
}
// Fix links (dedupe destinations).
- var urlMap = Map.fromIterable(closed,
- key: (Object dest) => (dest as Destination).url);
+ var urlMap = {for (final destination in closed) destination.url: destination};
for (var link in links) {
var canonical = urlMap[link.destination.url];
- // Note: If it wasn't for the posibility to SIGINT the process, we could
+ // Note: If it wasn't for the possibility to SIGINT the process, we could
// assert there is exactly one Destination per URL. There might not be,
// though.
if (canonical != null) {
@@ -472,9 +473,11 @@ Future crawl(
return CrawlResult(links, closed);
}
+@immutable
class CrawlResult {
final Set links;
final Set destinations;
+
const CrawlResult(this.links, this.destinations);
}
diff --git a/lib/src/destination.dart b/lib/src/destination.dart
index 5eb0af2..ad44587 100644
--- a/lib/src/destination.dart
+++ b/lib/src/destination.dart
@@ -1,8 +1,8 @@
-library linkcheck.destination;
-
import 'dart:io' show ContentType, HttpClientResponse, RedirectInfo;
-import 'package:linkcheck/src/parsers/html.dart';
+import 'package:meta/meta.dart';
+
+import 'parsers/html.dart';
/// RegExp for detecting URI scheme, such as `http:`, `mailto:`, etc.
final _scheme = RegExp(r"$(\w[\w\-]*\w):");
@@ -15,7 +15,7 @@ final _scheme = RegExp(r"$(\w[\w\-]*\w):");
/// component.
bool checkSchemeSupported(String url, Uri source) {
var match = _scheme.firstMatch(url);
- String scheme;
+ String? scheme;
if (match == null) {
// No scheme provided, so the source's scheme is used.
scheme = source.scheme;
@@ -26,20 +26,14 @@ bool checkSchemeSupported(String url, Uri source) {
return Destination.supportedSchemes.contains(scheme);
}
+@immutable
class BasicRedirectInfo {
- String url;
- int statusCode;
-
- BasicRedirectInfo.from(RedirectInfo info) {
- url = info.location.toString();
- statusCode = info.statusCode;
- }
-
- BasicRedirectInfo.fromMap(Map map)
- : url = map["url"] as String,
- statusCode = map["statusCode"] as int;
+ final String url;
+ final int statusCode;
- Map toMap() => {"url": url, "statusCode": statusCode};
+ BasicRedirectInfo.from(RedirectInfo info)
+ : url = info.location.toString(),
+ statusCode = info.statusCode;
}
class Destination {
@@ -49,20 +43,20 @@ class Destination {
final String url;
/// The uri as specified by source file, without the fragment.
- Uri _uri;
+ Uri? _uri;
/// The HTTP status code returned.
- int statusCode;
+ int? statusCode;
/// MimeType of the response.
- ContentType contentType;
+ ContentType? contentType;
- List redirects;
+ List redirects = [];
/// Url after all redirects.
- String finalUrl;
+ String? finalUrl;
- bool isExternal;
+ bool isExternal = true;
/// True if this [Destination] is parseable and could contain links to
/// other destinations. For example, HTML and CSS files are sources. JPEGs
@@ -72,7 +66,7 @@ class Destination {
/// Set of anchors on the page.
///
/// Only for [isSource] == `true`.
- List anchors;
+ List anchors = [];
/// If the URL is unparseable (malformed), this will be `true`.
bool isInvalid = false;
@@ -88,9 +82,7 @@ class Destination {
// them via toMap?
bool wasDeniedByRobotsTxt = false;
- int _hashCode;
-
- Uri _finalUri;
+ Uri? _finalUri;
/// The encoding is not UTF-8 or LATIN-1.
bool hasUnsupportedEncoding = false;
@@ -100,50 +92,19 @@ class Destination {
bool wasParsed = false;
- bool _isUnsupportedScheme;
+ bool? _isUnsupportedScheme;
Destination(Uri uri)
: url = uri.removeFragment().toString(),
- _uri = uri.removeFragment() {
- _hashCode = url.hashCode;
- }
-
- factory Destination.fromMap(Map map) {
- var destination = Destination.fromString(map["url"] as String);
- var contentType = map["primaryType"] == null
- ? null
- : ContentType(map["primaryType"] as String, map["subType"] as String);
- destination
- ..statusCode = map["statusCode"] as int
- ..contentType = contentType
- ..redirects = (map["redirects"] as List