Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to null safety and support Dart 3 #105

Merged
merged 14 commits into from
Dec 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Files and directories created by pub
.buildlog
.dart_tool/
.packages
.project
.pub/
build/
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <kbd>Edit</kbd> and hit
as well, it's currently required to manually <kbd>Edit</kbd> and hit
<kbd>Update release</kbd> on the release page once. No changes needed.
(Source: [GiHub Community](https://github.saobby.my.eu.orgmunity/t/automatically-publish-action-to-marketplace-on-release/17978))
18 changes: 12 additions & 6 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 0 additions & 2 deletions bin/linkcheck.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
library linkcheck.executable;

import 'dart:async';
import 'dart:io';

Expand Down
4 changes: 2 additions & 2 deletions dart_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
12 changes: 5 additions & 7 deletions lib/linkcheck.dart
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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+$");

Expand Down Expand Up @@ -224,8 +222,8 @@ Future<int> run(List<String> 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<String> urls = argResults.rest.toList();
UrlSkipper skipper = UrlSkipper.empty();
Expand Down Expand Up @@ -261,7 +259,7 @@ Future<int> run(List<String> arguments, Stdout stdout) async {
}

// TODO: exit gracefully if provided URL isn't a parseable URI
List<Uri> uris = urls.map((url) => Uri.parse(url)).toList();
List<Uri> uris = urls.map((url) => Uri.parse(url)).toList(growable: false);
Set<String> hosts;
if ((argResults[hostsFlag] as Iterable<String>).isNotEmpty) {
hosts = Set<String>.from(argResults[hostsFlag] as Iterable<String>);
Expand Down
53 changes: 28 additions & 25 deletions lib/src/crawl.dart
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -36,8 +35,8 @@ Future<CrawlResult> 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();
Expand All @@ -64,7 +63,9 @@ Future<CrawlResult> 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<Destination> openExternal = Queue<Destination>();
Expand Down Expand Up @@ -106,7 +107,7 @@ Future<CrawlResult> crawl(

int count = 0;
if (!verbose) {
if (ansiTerm) {
if (cursor != null) {
cursor.write("Crawling: $count");
} else {
print("Crawling...");
Expand All @@ -116,12 +117,12 @@ Future<CrawlResult> crawl(
// TODO:
// - --cache for creating a .linkcheck.cache file

var allDone = Completer<Null>();
var allDone = Completer<void>();

// Respond to Ctrl-C
StreamSubscription stopSignalSubscription;
late final StreamSubscription<void> stopSignalSubscription;
stopSignalSubscription = stopSignal.listen((dynamic _) async {
if (ansiTerm) {
if (pen != null) {
pen
.text("\n")
.red()
Expand All @@ -148,11 +149,11 @@ Future<CrawlResult> crawl(
}
}

bool _serverIsKnown(Destination destination) =>
bool serverIsKnown(Destination destination) =>
servers.keys.contains(destination.uri.authority);

Iterable<Destination> 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.
Expand All @@ -164,8 +165,8 @@ Future<CrawlResult> 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;
Expand All @@ -176,8 +177,9 @@ Future<CrawlResult> 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;
Expand Down Expand Up @@ -236,7 +238,7 @@ Future<CrawlResult> 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());
Expand All @@ -259,7 +261,7 @@ Future<CrawlResult> 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
Expand Down Expand Up @@ -287,12 +289,12 @@ Future<CrawlResult> 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());
Expand Down Expand Up @@ -436,11 +438,10 @@ Future<CrawlResult> crawl(
}

// Fix links (dedupe destinations).
var urlMap = Map<String, Destination>.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) {
Expand Down Expand Up @@ -472,9 +473,11 @@ Future<CrawlResult> crawl(
return CrawlResult(links, closed);
}

@immutable
class CrawlResult {
final Set<Link> links;
final Set<Destination> destinations;

const CrawlResult(this.links, this.destinations);
}

Expand Down
Loading