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>) - ?.map((obj) => BasicRedirectInfo.fromMap(obj)) - ?.toList() - ..finalUrl = map["finalUrl"] as String - ..isExternal = map["isExternal"] as bool - ..isSource = map["isSource"] as bool - ..anchors = map["anchors"] as List - ..isInvalid = map["isInvalid"] as bool - ..didNotConnect = map["didNotConnect"] as bool - ..wasParsed = map["wasParsed"] as bool - ..hasUnsupportedEncoding = map["hasUnsupportedEncoding"] as bool; - return destination; - } + _uri = uri.removeFragment(); Destination.fromString(String url) - : url = url.contains("#") ? url.split("#").first : url { - _hashCode = this.url.hashCode; - } + : url = url.contains("#") ? url.split("#").first : url; - Destination.invalid(String url) - : url = url, - isInvalid = true { - _hashCode = url.hashCode; - } + Destination.invalid(this.url) : isInvalid = true; - Destination.unsupported(String url) : url = url { + Destination.unsupported(this.url) { _isUnsupportedScheme = true; - _hashCode = url.hashCode; } // TODO: make sure we don't assign the same hashcode to two destinations like @@ -152,7 +113,7 @@ class Destination { Uri get finalUri => _finalUri ??= Uri.parse(finalUrl ?? url); @override - int get hashCode => _hashCode; + int get hashCode => url.hashCode; /// A bad or busted server didn't give us any content type. This is a warning. bool get hasNoMimeType => wasTried && contentType == null; @@ -176,15 +137,14 @@ class Destination { bool get isParseableMimeType => isHtmlMimeType || isCssMimeType; bool get isPermanentlyRedirected => - redirects != null && - redirects.isNotEmpty && - redirects.first.statusCode == 301; + redirects.isNotEmpty && redirects.first.statusCode == 301; - bool get isRedirected => redirects != null && redirects.isNotEmpty; + bool get isRedirected => redirects.isNotEmpty; /// True if the destination URI isn't one of the [supportedSchemes]. - bool get isUnsupportedScheme { - if (_isUnsupportedScheme != null) return _isUnsupportedScheme; + late final bool isUnsupportedScheme = () { + var specifiedUnsupported = _isUnsupportedScheme; + if (specifiedUnsupported != null) return specifiedUnsupported; bool result = true; try { // This can throw a FormatException when the URI cannot be parsed. @@ -192,9 +152,8 @@ class Destination { } on FormatException { // Pass. } - _isUnsupportedScheme = result; return result; - } + }(); String get statusDescription { if (isUnsupportedScheme) return "scheme unsupported"; @@ -212,15 +171,15 @@ class Destination { } Uri get uri { - if (_uri != null) return _uri; + var specifiedUri = _uri; + if (specifiedUri != null) return specifiedUri; try { - _uri = Uri.parse(url); + return _uri = Uri.parse(url); } on FormatException catch (e, s) { print("Stack trace: $s"); throw StateError("Tried parsing '$url' as URI:\n" "$e"); } - return _uri; } bool get wasTried => didNotConnect || statusCode != null; @@ -232,28 +191,11 @@ class Destination { /// Returns `true` if the [fragment] (such as #something) will find it's mark /// on this [Destination]. If the fragment is `null` or empty, it will /// automatically succeed. - bool satisfiesFragment(String fragment) { + bool satisfiesFragment(String? fragment) { if (fragment == null || fragment == '') return true; - if (anchors == null) return false; return anchors.contains(normalizeAnchor(fragment)); } - Map toMap() => { - "url": url, - "statusCode": statusCode, - "primaryType": contentType?.primaryType, - "subType": contentType?.subType, - "redirects": redirects?.map((info) => info.toMap())?.toList(), - "finalUrl": finalUrl, - "isExternal": isExternal, - "isSource": isSource, - "anchors": anchors, - "isInvalid": isInvalid, - "didNotConnect": didNotConnect, - "wasParsed": wasParsed, - "hasUnsupportedEncoding": hasUnsupportedEncoding - }; - @override String toString() => url; @@ -261,9 +203,11 @@ class Destination { assert(url == result.url); finalUrl = result.finalUrl; statusCode = result.statusCode; - contentType = result.primaryType == null + var primaryType = result.primaryType; + var subType = result.subType; + contentType = primaryType == null || subType == null ? null - : ContentType(result.primaryType, result.subType); + : ContentType(primaryType, subType); redirects = result.redirects; isSource = result.isSource; anchors = result.anchors; @@ -275,56 +219,35 @@ class Destination { /// Data about destination coming from a fetch. class DestinationResult { - String url; - String finalUrl; - int statusCode; - String primaryType; - String subType; + final String url; + String? finalUrl; + int? statusCode; + String? primaryType; + String? subType; List redirects; - bool isSource = false; + final bool isSource; List anchors; - bool didNotConnect = false; + final bool didNotConnect; bool wasParsed = false; bool hasUnsupportedEncoding = false; - DestinationResult.fromDestination(Destination destination) + DestinationResult.fromDestination(Destination destination, + {this.didNotConnect = false, + this.redirects = const [], + this.anchors = const []}) + : url = destination.url, + isSource = destination.isSource; + + DestinationResult.fromResponse( + Destination destination, HttpClientResponse response) : url = destination.url, isSource = destination.isSource, - redirects = []; - - DestinationResult.fromMap(Map map) - : url = map["url"] as String, - finalUrl = map["finalUrl"] as String, - statusCode = map["statusCode"] as int, - primaryType = map["primaryType"] as String, - subType = map["subType"] as String, - redirects = (map["redirects"] as List>) - .map((obj) => BasicRedirectInfo.fromMap(obj)) - .toList(), - isSource = map["isSource"] as bool, - anchors = map["anchors"] as List, - didNotConnect = map["didNotConnect"] as bool, - wasParsed = map["wasParsed"] as bool, - hasUnsupportedEncoding = map["hasUnsupportedEncoding"] as bool; - - Map toMap() => { - "url": url, - "finalUrl": finalUrl, - "statusCode": statusCode, - "primaryType": primaryType, - "subType": subType, - "redirects": redirects.map((info) => info.toMap()).toList(), - "isSource": isSource, - "anchors": anchors, - "didNotConnect": didNotConnect, - "wasParsed": wasParsed, - "hasUnsupportedEncoding": hasUnsupportedEncoding - }; - - void updateFromResponse(HttpClientResponse response) { - statusCode = response.statusCode; - redirects = - response.redirects.map((info) => BasicRedirectInfo.from(info)).toList(); + redirects = response.redirects + .map((info) => BasicRedirectInfo.from(info)) + .toList(growable: false), + statusCode = response.statusCode, + anchors = const [], + didNotConnect = false { if (redirects.isEmpty) { finalUrl = url; } else { @@ -335,9 +258,10 @@ class DestinationResult { current.resolve(redirect.url)) .toString(); } - if (response.headers.contentType != null) { - primaryType = response.headers.contentType.primaryType; - subType = response.headers.contentType.subType; + var contentType = response.headers.contentType; + if (contentType != null) { + primaryType = contentType.primaryType; + subType = contentType.subType; } } } diff --git a/lib/src/link.dart b/lib/src/link.dart index 2a9d428..cafe35a 100644 --- a/lib/src/link.dart +++ b/lib/src/link.dart @@ -1,12 +1,10 @@ -library linkcheck.link; - -import 'package:linkcheck/src/destination.dart'; -import 'package:linkcheck/src/origin.dart'; +import 'destination.dart'; +import 'origin.dart'; class Link { - Origin origin; + final Origin origin; Destination destination; - String fragment; + final String? fragment; /// Whether or not this link was marked as skipped. /// @@ -14,17 +12,10 @@ class Link { /// has a match, [wasSkipped] will be `true`. bool wasSkipped = false; - Link(this.origin, this.destination, String fragment, + Link(this.origin, this.destination, String? fragment, [this.wasSkipped = false]) : fragment = fragment == null || fragment.isEmpty ? null : fragment; - Link.fromMap(Map map) - : this( - Origin.fromMap(map["origin"] as Map), - Destination.fromMap(map["destination"] as Map), - map["destinationAnchor"] as String, - map["wasSkipped"] as bool); - bool get breaksAnchor => !wasSkipped && destination.wasParsed && @@ -56,15 +47,8 @@ class Link { bool hasWarning(bool shouldCheckAnchors) => (shouldCheckAnchors && breaksAnchor) || destination.hasNoMimeType; - Map toMap() => { - "origin": origin.toMap(), - "destination": destination.toMap(), - "destinationAnchor": fragment, - "wasSkipped": wasSkipped - }; - @override String toString() => "$origin => $destination" - "${fragment == null ? '' : '#' + fragment} " + "${fragment == null ? '' : '#$fragment'} " "(${destination.statusDescription})"; } diff --git a/lib/src/origin.dart b/lib/src/origin.dart index 6e0ec3f..1f18524 100644 --- a/lib/src/origin.dart +++ b/lib/src/origin.dart @@ -1,9 +1,9 @@ -library linkcheck.source; - +import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; /// Origin of a link. Contains information about the exact place in a file /// (URI) and some additional helpful info. +@immutable class Origin { final Uri uri; final SourceSpan span; @@ -13,47 +13,6 @@ class Origin { Origin(this.uri, this.span, this.tagName, this.text, this.outerHtml); - Origin.fromMap(Map map) - : this( - Uri.parse(map["uri"] as String), - _deserializeSpan(map["span"] as Map), - map["tagName"] as String, - map["text"] as String, - map["outerHtml"] as String); - @override - String toString() => "$uri (${span.start.line + 1}:${span.start.column})"; - - Map toMap() => { - "uri": uri.toString(), - "span": _serializeSpan(span), - "tagName": tagName, - "text": text, - "outerHtml": outerHtml - }; + String toString() => '$uri (${span.start.line + 1}:${span.start.column})'; } - -Map _serializeSpan(SourceSpan span) => { - "start": _serializeSourceLocation(span.start), - "end": _serializeSourceLocation(span.end), - "text": span.text - }; - -SourceSpan _deserializeSpan(Map map) => SourceSpan( - _deserializeSourceLocation(map["start"] as Map), - _deserializeSourceLocation(map["end"] as Map), - map["text"] as String); - -Map _serializeSourceLocation(SourceLocation location) => - { - "offset": location.offset, - "line": location.line, - "column": location.column, - "sourceUrl": location.sourceUrl.toString() - }; - -SourceLocation _deserializeSourceLocation(Map map) => - SourceLocation(map["offset"] as int, - sourceUrl: Uri.parse(map["sourceUrl"] as String), - line: map["line"] as int, - column: map["column"] as int); diff --git a/lib/src/parsers/css.dart b/lib/src/parsers/css.dart index a76ed12..5dc11cc 100644 --- a/lib/src/parsers/css.dart +++ b/lib/src/parsers/css.dart @@ -1,17 +1,12 @@ -library linkcheck.parsers.css; - import 'package:csslib/parser.dart' as css; import 'package:csslib/parser.dart'; import 'package:csslib/visitor.dart'; import 'package:source_span/source_span.dart'; -import '../worker/fetch_results.dart'; import '../destination.dart'; import '../link.dart'; import '../origin.dart'; -import 'package:logging/logging.dart'; - -Logger _log = Logger('parseCSS'); +import '../worker/fetch_results.dart'; FetchResults parseCss( String content, Destination current, DestinationResult checked) { @@ -28,24 +23,18 @@ FetchResults parseCss( start = content.indexOf("}", start); if (start < content.length - 1) start += 1; } - StyleSheet style; - try { - style = css.parse(content.substring(start), errors: errors); - } catch (e) { - // csslib itself crashes when trying to parse this. - // TODO: remove when https://github.com/dart-lang/csslib/issues/92 - // is fixed. - _log.severe('Parsing ${current.url} crashed csslib'); - break; - } + var style = css.parse(content.substring(start), errors: errors); style.visit(urlHarvester); - int offset = 0; - errors.forEach((error) { + var offset = 0; + for (var error in errors) { if (error.level == MessageLevel.severe) { - offset = error.span.end.offset; - foundError = true; + var errorSpan = error.span; + if (errorSpan != null) { + offset = errorSpan.end.offset; + foundError = true; + } } - }); + } start += offset; } while (foundError); @@ -57,7 +46,7 @@ FetchResults parseCss( // Valid URLs can be surrounded by spaces. var url = reference.url.trim(); - Link link; + Link? link; // Deal with unsupported schemes such as `telnet:` or `mailto:`. if (!checkSchemeSupported(url, current.finalUri)) { @@ -89,7 +78,7 @@ FetchResults parseCss( continue; } - Destination destination = Destination(destinationUri); + var destination = Destination(destinationUri); currentDestinations.add(destination); link = Link(origin, destination, null); links.add(link); @@ -100,16 +89,20 @@ FetchResults parseCss( } class CssReference { - SourceSpan span; - String url; + final SourceSpan span; + final String url; + CssReference(this.span, this.url); } class CssUrlHarvester extends Visitor { - List references = []; + final List references = []; @override void visitUriTerm(UriTerm node) { - references.add(CssReference(node.span, node.text)); + var span = node.span; + if (span != null) { + references.add(CssReference(span, node.text)); + } } } diff --git a/lib/src/parsers/html.dart b/lib/src/parsers/html.dart index 8357510..2260a19 100644 --- a/lib/src/parsers/html.dart +++ b/lib/src/parsers/html.dart @@ -1,5 +1,3 @@ -library linkcheck.parsers.html; - import 'package:html/dom.dart'; import 'package:html/parser.dart'; @@ -18,16 +16,20 @@ import '../worker/fetch_results.dart'; /// Setting [parseable] to true will create a link to a destination with /// [Destination.isSource] set to `true`. For example, links in are /// often parseable (they are HTML), links in often aren't. -Link extractLink( - Uri originUri, - Uri baseUri, - Element element, - final List attributes, - final List destinations, - bool parseable) { - var origin = Origin(originUri, element.sourceSpan, element.localName, - element.text, element.outerHtml); - String reference; +Link extractLink(Uri originUri, Uri baseUri, Element element, + final List attributes, final List destinations, + {bool parseable = false}) { + var sourceSpan = element.sourceSpan; + var localName = element.localName; + + if (sourceSpan == null || localName == null) { + throw StateError( + 'Element $element is missing a ${#sourceSpan} or ${#localName}.'); + } + + var origin = + Origin(originUri, sourceSpan, localName, element.text, element.outerHtml); + String? reference; for (var attributeName in attributes) { reference = element.attributes[attributeName]; if (reference != null) break; @@ -98,21 +100,26 @@ FetchResults parseHtml(String content, Uri uri, Destination current, var anchors = doc .querySelectorAll("body [id], body [name]") .map((element) => element.attributes["id"] ?? element.attributes["name"]) + .whereType() .map(normalizeAnchor) - .toList(); + .toList(growable: false); checked.anchors = anchors; if (ignoreLinks) { checked.wasParsed = true; - return FetchResults(checked, const []); + return FetchResults(checked); } Uri baseUri = current.finalUri; var baseElements = doc.querySelectorAll("base[href]"); if (baseElements.isNotEmpty) { - // More than one base element per page is not according to HTML specs. - // At the moment, we just ignore that. But TODO: solve for pages with more - baseUri = baseUri.resolve(baseElements.first.attributes["href"]); + var firstBaseHref = baseElements.first.attributes["href"]; + + if (firstBaseHref != null) { + // More than one base element per page is not according to HTML specs. + // At the moment, we just ignore that. But TODO: solve for pages with more + baseUri = baseUri.resolve(firstBaseHref); + } } var linkElements = doc.querySelectorAll( @@ -123,7 +130,8 @@ FetchResults parseHtml(String content, Uri uri, Destination current, /// TODO: add destinations to queue, but NOT as a side effect inside extractLink List links = linkElements .map((element) => extractLink(current.finalUri, baseUri, element, - const ["href", "src"], currentDestinations, true)) + const ["href", "src"], currentDestinations, + parseable: true)) .toList(); // Find resources @@ -131,7 +139,7 @@ FetchResults parseHtml(String content, Uri uri, Destination current, doc.querySelectorAll("link[href], [src], object[data]"); Iterable currentResourceLinks = resourceElements.map((element) => extractLink(current.finalUri, baseUri, element, - const ["src", "href", "data"], currentDestinations, false)); + const ["src", "href", "data"], currentDestinations)); links.addAll(currentResourceLinks); diff --git a/lib/src/parsers/robots_txt.dart b/lib/src/parsers/robots_txt.dart index 8d13eef..6d06f7b 100644 --- a/lib/src/parsers/robots_txt.dart +++ b/lib/src/parsers/robots_txt.dart @@ -1,5 +1,6 @@ -library linkcheck.parsers.robots_txt; +import 'package:meta/meta.dart'; +@immutable class RobotsBouncer { /// The shortest possible identifying part of the user agent. /// @@ -13,8 +14,6 @@ class RobotsBouncer { const userAgentString = "User-agent:"; const disallowString = "Disallow:"; - assert(robotName != null); - Set currentUserAgents = {}; Set currentPaths = {}; for (var line in lines) { @@ -64,9 +63,11 @@ class RobotsBouncer { } } +@immutable class _Rule { final Set userAgents; final Set paths; + _Rule(this.userAgents, this.paths); // 'Disallow:' (with empty rulepath) means something like 'allow all' diff --git a/lib/src/parsers/url_skipper.dart b/lib/src/parsers/url_skipper.dart index 546b1a3..93d036a 100644 --- a/lib/src/parsers/url_skipper.dart +++ b/lib/src/parsers/url_skipper.dart @@ -1,5 +1,3 @@ -library linkcheck.parsers.url_skipper; - const _commentStart = "#"; const _commentStartEscape = r"\#"; @@ -7,7 +5,7 @@ const _commentStartEscape = r"\#"; /// Parses and keeps record of the URL patterns to skip. class UrlSkipper { /// Path of the provided file with regexps to skip. - final String path; + final String? path; final List<_UrlSkipperRecord> _records; @@ -40,7 +38,7 @@ class UrlSkipper { } static Iterable<_UrlSkipperRecord> _parse(Iterable lines) sync* { - int lineNumber = 1; + var lineNumber = 1; for (var line in lines) { line = line.trim(); diff --git a/lib/src/server_info.dart b/lib/src/server_info.dart index 92bdea2..241d7e4 100644 --- a/lib/src/server_info.dart +++ b/lib/src/server_info.dart @@ -1,4 +1,4 @@ -library linkcheck.server_info; +import 'package:meta/meta.dart'; import 'parsers/robots_txt.dart'; @@ -11,11 +11,11 @@ class ServerInfo { /// No duration. static const immediate = Duration(); - String host; + final String host; - int port; + final int? port; - RobotsBouncer bouncer; + RobotsBouncer? bouncer; /// The total count of connection attempts, both successful and failed. int connectionAttempts = 0; @@ -39,7 +39,7 @@ class ServerInfo { /// or 405). bool hasFailedHeadRequest = false; - DateTime _lastRequest; + DateTime? _lastRequest; ServerInfo(String authority) : host = authority.split(':').first, @@ -57,9 +57,10 @@ class ServerInfo { /// Creates the minimum duration to wait before the server should be bothered /// again. Duration getThrottlingDuration() { - if (_lastRequest == null) return immediate; + var lastRequest = _lastRequest; + if (lastRequest == null) return immediate; if (isLocalhost) return immediate; - var sinceLastRequest = DateTime.now().difference(_lastRequest); + var sinceLastRequest = DateTime.now().difference(lastRequest); if (sinceLastRequest.isNegative) { // There's a request scheduled in the future. return -sinceLastRequest + minimumDelay; @@ -83,7 +84,7 @@ class ServerInfo { forRobot: robotName); } - void updateFromStatusCode(int statusCode) { + void updateFromStatusCode(int? statusCode) { connectionAttempts += 1; if (statusCode == null) { didNotConnectCount += 1; @@ -107,21 +108,14 @@ class ServerInfo { } /// To be sent from Worker to main thread. +@immutable class ServerInfoUpdate { - String host; - bool didNotConnect = false; - String robotsTxtContents = ""; - - ServerInfoUpdate(this.host); - ServerInfoUpdate.fromMap(Map map) - : this._(map["host"] as String, map["robots"] as String, - map["didNotConnect"] as bool); - - ServerInfoUpdate._(this.host, this.robotsTxtContents, this.didNotConnect); - - Map toMap() => { - "host": host, - "robots": robotsTxtContents, - "didNotConnect": didNotConnect - }; + final String host; + final bool didNotConnect; + final String robotsTxtContents; + + ServerInfoUpdate(this.host, + {this.robotsTxtContents = '', this.didNotConnect = false}); + + ServerInfoUpdate.didNotConnect(String host) : this(host, didNotConnect: true); } diff --git a/lib/src/uri_glob.dart b/lib/src/uri_glob.dart index ff11004..64410ad 100644 --- a/lib/src/uri_glob.dart +++ b/lib/src/uri_glob.dart @@ -1,8 +1,8 @@ -library linkcheck.uri_glob; - import 'package:glob/glob.dart'; +import 'package:meta/meta.dart'; import 'package:path/path.dart'; +@immutable class UriGlob { static final _urlContext = Context(style: Style.url); diff --git a/lib/src/worker/fetch_options.dart b/lib/src/worker/fetch_options.dart index ada4eb9..bdb7ef6 100644 --- a/lib/src/worker/fetch_options.dart +++ b/lib/src/worker/fetch_options.dart @@ -1,5 +1,3 @@ -library linkcheck.fetch_options; - import 'dart:async'; import '../uri_glob.dart'; @@ -11,7 +9,7 @@ class FetchOptions { final headIncompatible = {}; // TODO: send to main // TODO: hashmap of known problematic servers etc. = List - final StreamSink> _sink; + final StreamSink _sink; FetchOptions(this._sink); @@ -22,7 +20,7 @@ class FetchOptions { } void info(String message) { - _sink.add({verbKey: infoFromWorkerVerb, dataKey: message}); + _sink.add(WorkerTask(verb: WorkerVerb.infoFromWorker, data: message)); } /// Returns true if the provided [uri] should be considered internal. This diff --git a/lib/src/worker/fetch_results.dart b/lib/src/worker/fetch_results.dart index 8e3866f..1f8a825 100644 --- a/lib/src/worker/fetch_results.dart +++ b/lib/src/worker/fetch_results.dart @@ -1,22 +1,14 @@ -library linkcheck.fetch_results; - import '../destination.dart'; import '../link.dart'; class FetchResults { final DestinationResult checked; final List links; - FetchResults(this.checked, this.links); - FetchResults.fromMap(Map map) - : this( - DestinationResult.fromMap(map["checked"] as Map), - List.from((map["links"] as List).map( - (serialization) => - Link.fromMap(serialization as Map)))); + FetchResults(this.checked, [this.links = const []]); - Map toMap() => { - "checked": checked.toMap(), - "links": links?.map((link) => link.toMap())?.toList() ?? [] - }; + @override + String toString() { + return 'FetchResults{checked: $checked, links: $links}'; + } } diff --git a/lib/src/worker/pool.dart b/lib/src/worker/pool.dart index 344facc..736763f 100644 --- a/lib/src/worker/pool.dart +++ b/lib/src/worker/pool.dart @@ -1,5 +1,3 @@ -library linkcheck.pool; - import 'dart:async'; import '../destination.dart'; @@ -19,9 +17,9 @@ class Pool { final int count; bool _isShuttingDown = false; - List _workers; + final List _workers; - Timer _healthCheckTimer; + late Timer _healthCheckTimer; final Map _lastJobPosted = {}; final Set _hostGlobs; @@ -29,24 +27,23 @@ class Pool { final StreamController _fetchResultsSink = StreamController(); - Stream fetchResults; + late final Stream fetchResults = _fetchResultsSink.stream; final StreamController _messagesSink = StreamController(); - Stream messages; + late final Stream messages = _messagesSink.stream; final StreamController _serverCheckSink = StreamController(); - Stream serverCheckResults; + late final Stream serverCheckResults = + _serverCheckSink.stream; bool _finished = false; - Pool(this.count, this._hostGlobs) { - fetchResults = _fetchResultsSink.stream; - messages = _messagesSink.stream; - serverCheckResults = _serverCheckSink.stream; - } + Pool(this.count, this._hostGlobs) + : _workers = + List.generate(count, (i) => Worker('$i'), growable: false); /// Returns true if all workers are either waiting for a job or not really /// alive (not spawned yet, or already killed). @@ -69,7 +66,8 @@ class Pool { worker.destinationToCheck = destination; Timer(delay, () { if (_isShuttingDown) return; - worker.sink.add({verbKey: checkPageVerb, dataKey: destination.toMap()}); + worker.sink + .add(WorkerTask(verb: WorkerVerb.checkPage, data: destination)); }); return worker; } @@ -77,13 +75,13 @@ class Pool { /// Starts a job to send request for /robots.txt on the server. Worker checkServer(String host) { var worker = pickWorker(); - worker.sink.add({verbKey: checkServerVerb, dataKey: host}); + worker.sink.add(WorkerTask(verb: WorkerVerb.checkServer, data: host)); worker.serverToCheck = host; _lastJobPosted[worker] = DateTime.now(); return worker; } - Future close() async { + Future close() async { _isShuttingDown = true; _healthCheckTimer.cancel(); await Future.wait(_workers.map((worker) async { @@ -102,63 +100,62 @@ class Pool { "Please make sure to wait until Pool.allWorking is false."); } - Future spawn() async { - _workers = List.generate(count, (i) => Worker()..name = '$i'); + Future spawn() async { await Future.wait(_workers.map((worker) => worker.spawn())); - _workers.forEach((worker) => worker.stream.listen((Map message) { - switch (message[verbKey] as String) { - case checkPageDoneVerb: - var result = - FetchResults.fromMap(message[dataKey] as Map); - _fetchResultsSink.add(result); - worker.destinationToCheck = null; - return; - case checkServerDoneVerb: - var result = ServerInfoUpdate.fromMap( - message[dataKey] as Map); - _serverCheckSink.add(result); - worker.serverToCheck = null; - return; - case infoFromWorkerVerb: - _messagesSink.add(message[dataKey] as String); - return; - default: - throw StateError("Unrecognized verb from Worker: " - "${message[verbKey]}"); - } - })); + for (var worker in _workers) { + worker.stream.listen((WorkerTask message) { + switch (message.verb) { + case WorkerVerb.checkPageDone: + var result = message.data as FetchResults; + _fetchResultsSink.add(result); + worker.destinationToCheck = null; + break; + case WorkerVerb.checkServerDone: + var result = message.data as ServerInfoUpdate; + _serverCheckSink.add(result); + worker.serverToCheck = null; + break; + case WorkerVerb.infoFromWorker: + _messagesSink.add(message.data as String); + break; + default: + throw StateError("Unrecognized verb from Worker: " + "${message.verb}"); + } + }); + } _addHostGlobs(); _healthCheckTimer = Timer.periodic(healthCheckFrequency, (_) async { - if (_isShuttingDown) return null; + if (_isShuttingDown) return; var now = DateTime.now(); for (int i = 0; i < _workers.length; i++) { var worker = _workers[i]; + var lastJobPostedWorker = _lastJobPosted[worker]; if (!worker.idle && !worker.isKilled && - _lastJobPosted[worker] != null && - now.difference(_lastJobPosted[worker]) > workerTimeout) { + lastJobPostedWorker != null && + now.difference(lastJobPostedWorker) > workerTimeout) { _messagesSink.add("Killing unresponsive $worker"); var destination = worker.destinationToCheck; var server = worker.serverToCheck; _lastJobPosted.remove(worker); - var newWorker = Worker()..name = '$i'; + var newWorker = Worker('$i'); _workers[i] = newWorker; if (destination != null) { // Only notify about the failed destination when the old // worker is gone. Otherwise, crawl could fail to wrap up, thinking // that one Worker is still working. - var checked = DestinationResult.fromDestination(destination); - checked.didNotConnect = true; - var result = FetchResults(checked, const []); + var checked = DestinationResult.fromDestination(destination, + didNotConnect: true); + var result = FetchResults(checked); _fetchResultsSink.add(result); } if (server != null) { - var result = ServerInfoUpdate(server); - result.didNotConnect = true; + var result = ServerInfoUpdate.didNotConnect(server); _serverCheckSink.add(result); } @@ -177,7 +174,9 @@ class Pool { /// Sends host globs (e.g. http://example.com/**) to all the workers. void _addHostGlobs() { for (var worker in _workers) { - worker.sink.add({verbKey: addHostGlobVerb, dataKey: _hostGlobs.toList()}); + worker.sink.add(WorkerTask( + verb: WorkerVerb.addHostGlob, + data: _hostGlobs.toList(growable: false))); } } } diff --git a/lib/src/worker/worker.dart b/lib/src/worker/worker.dart index c0b0396..8a98dea 100644 --- a/lib/src/worker/worker.dart +++ b/lib/src/worker/worker.dart @@ -1,12 +1,11 @@ -library linkcheck.worker; - import 'dart:async'; import 'dart:convert'; import 'dart:io' hide Link; import 'dart:isolate'; -import 'package:stream_channel/stream_channel.dart'; +import 'package:meta/meta.dart'; import 'package:stream_channel/isolate_channel.dart'; +import 'package:stream_channel/stream_channel.dart'; import '../destination.dart'; import '../parsers/css.dart'; @@ -15,26 +14,15 @@ import '../server_info.dart'; import 'fetch_options.dart'; import 'fetch_results.dart'; -const addHostGlobVerb = "ADD_HOST"; -const checkPageDoneVerb = "CHECK_DONE"; -const checkPageVerb = "CHECK"; -const checkServerDoneVerb = "SERVER_CHECK_DONE"; -const checkServerVerb = "CHECK_SERVER"; -const dataKey = "data"; -final dieMessage = {verbKey: dieVerb}; -const dieVerb = "DIE"; -const infoFromWorkerVerb = "INFO_FROM_WORKER"; -final unrecognizedMessage = {verbKey: unrecognizedVerb}; -const unrecognizedVerb = "UNRECOGNIZED"; +const dieMessage = WorkerTask(verb: WorkerVerb.die); +const unrecognizedMessage = WorkerTask(verb: WorkerVerb.unrecognized); const userAgent = "linkcheck tool (https://github.com/filiph/linkcheck)"; -const verbKey = "message"; - Future checkServer( String host, HttpClient client, FetchOptions options) async { - ServerInfoUpdate result = ServerInfoUpdate(host); + var originalHost = host; - int port; + int? port; if (host.contains(':')) { var parts = host.split(':'); assert(parts.length == 2); @@ -45,7 +33,7 @@ Future checkServer( Uri uri = Uri(scheme: "http", host: host, port: port, path: "/robots.txt"); // Fetch the HTTP response - HttpClientResponse response; + HttpClientResponse? response; try { response = await _fetch(client, uri); } on TimeoutException { @@ -58,15 +46,14 @@ Future checkServer( // Leave response == null. } + // Request failed completely. if (response == null) { - // Request failed completely. - result.didNotConnect = true; - return result; + return ServerInfoUpdate.didNotConnect(originalHost); } // No robots.txt. if (response.statusCode != 200) { - return result; + return ServerInfoUpdate(originalHost); } String content; @@ -85,23 +72,20 @@ Future checkServer( content = ""; } - result.robotsTxtContents = content; - return result; + return ServerInfoUpdate(originalHost, robotsTxtContents: content); } Future checkPage( Destination current, HttpClient client, FetchOptions options) async { - DestinationResult checked = DestinationResult.fromDestination(current); var uri = current.uri; // Fetch the HTTP response - HttpClientResponse response; + HttpClientResponse? response; try { - if (!current.isSource && - !options.headIncompatible.contains(current.uri.host)) { + if (!current.isSource && !options.headIncompatible.contains(uri.host)) { response = await _fetchHead(client, uri); if (response == null) { - options.headIncompatible.add(current.uri.host); + options.headIncompatible.add(uri.host); // TODO: let main isolate know (options.addHeadIncompatible) } } @@ -119,26 +103,28 @@ Future checkPage( if (response == null) { // Request failed completely. - checked.didNotConnect = true; - return FetchResults(checked, const []); + var checked = + DestinationResult.fromDestination(current, didNotConnect: true); + return FetchResults(checked); } - checked.updateFromResponse(response); + DestinationResult checked = DestinationResult.fromResponse(current, response); + current.updateFromResult(checked); if (current.statusCode != 200) { - return FetchResults(checked, const []); + return FetchResults(checked); } if (!current.isParseableMimeType /* TODO: add SVG/XML */) { - return FetchResults(checked, const []); + return FetchResults(checked); } bool isExternal = !options.matchesAsInternal(current.finalUri); if (isExternal && !current.isHtmlMimeType) { // We only parse external HTML (to get anchors), not other mime types. - return FetchResults(checked, const []); + return FetchResults(checked); } String content; @@ -154,7 +140,7 @@ Future checkPage( } on FormatException { // TODO: report as a warning checked.hasUnsupportedEncoding = true; - return FetchResults(checked, const []); + return FetchResults(checked); } if (current.isCssMimeType) { @@ -169,7 +155,7 @@ Future checkPage( /// The entrypoint for the worker isolate. void worker(SendPort port) { - var channel = IsolateChannel>.connectSend(port); + var channel = IsolateChannel.connectSend(port); var sink = channel.sink; var stream = channel.stream; @@ -178,31 +164,30 @@ void worker(SendPort port) { bool alive = true; - stream.listen((Map message) async { - switch (message[verbKey] as String) { - case dieVerb: + stream.listen((WorkerTask message) async { + switch (message.verb) { + case WorkerVerb.die: client.close(force: true); alive = false; await sink.close(); - return null; - case checkPageVerb: - Destination destination = - Destination.fromMap(message[dataKey] as Map); + return; + case WorkerVerb.checkPage: + var destination = message.data as Destination; var results = await checkPage(destination, client, options); if (alive) { - sink.add({verbKey: checkPageDoneVerb, dataKey: results.toMap()}); + sink.add(WorkerTask(verb: WorkerVerb.checkPageDone, data: results)); } - return null; - case checkServerVerb: - String host = message[dataKey] as String; + return; + case WorkerVerb.checkServer: + String host = message.data as String; ServerInfoUpdate results = await checkServer(host, client, options); if (alive) { - sink.add({verbKey: checkServerDoneVerb, dataKey: results.toMap()}); + sink.add(WorkerTask(verb: WorkerVerb.checkServerDone, data: results)); } - return null; - case addHostGlobVerb: - options.addHostGlobs(message[dataKey] as List); - return null; + return; + case WorkerVerb.addHostGlob: + options.addHostGlobs(message.data as List); + return; // TODO: add to server info from main isolate default: sink.add(unrecognizedMessage); @@ -226,7 +211,7 @@ Future _fetch(HttpClient client, Uri uri) async { /// /// Some servers don't support this request, in which case they return HTTP /// status code 405. If that's the case, this function returns `null`. -Future _fetchHead(HttpClient client, Uri uri) async { +Future _fetchHead(HttpClient client, Uri uri) async { var request = await client.headUrl(uri).timeout(connectionTimeout); var response = await request.close().timeout(responseTimeout); @@ -238,26 +223,27 @@ Future _fetchHead(HttpClient client, Uri uri) async { /// Spawns a worker isolate and returns a [StreamChannel] for communicating with /// it. -Future>> _spawnWorker() async { +Future> _spawnWorker() async { var port = ReceivePort(); await Isolate.spawn(worker, port.sendPort); - return IsolateChannel>.connectReceive(port); + return IsolateChannel.connectReceive(port); } class Worker { - StreamChannel> _channel; - StreamSink> _sink; - Stream> _stream; + StreamChannel? _channel; - String name; + final String name; - Destination destinationToCheck; + Destination? destinationToCheck; - String serverToCheck; + String? serverToCheck; bool _spawned = false; bool _isKilled = false; + + Worker(this.name); + bool get idle => destinationToCheck == null && serverToCheck == null && @@ -266,26 +252,45 @@ class Worker { bool get isKilled => _isKilled; - StreamSink> get sink => _sink; + StreamSink get sink => _channel!.sink; bool get spawned => _spawned; - Stream get stream => _stream; + Stream get stream => _channel!.stream; - Future kill() async { + Future kill() async { if (!_spawned) return; _isKilled = true; - sink.add(dieMessage); - await sink.close(); + var sinkToClose = sink; + sinkToClose.add(dieMessage); + await sinkToClose.close(); } - Future spawn() async { + Future spawn() async { assert(_channel == null); _channel = await _spawnWorker(); - _sink = _channel.sink; - _stream = _channel.stream; _spawned = true; } @override - String toString() => "Worker<$name>"; + String toString() => 'Worker<$name>'; +} + +@immutable +class WorkerTask { + final WorkerVerb verb; + final Object? data; + + const WorkerTask({required this.verb, this.data}); +} + +/// Different types of tasks which can be communicated to and from workers +enum WorkerVerb { + addHostGlob, + checkPage, + checkPageDone, + checkServer, + checkServerDone, + die, + infoFromWorker, + unrecognized } diff --git a/lib/src/writer_report.dart b/lib/src/writer_report.dart index 30d7112..8605dae 100644 --- a/lib/src/writer_report.dart +++ b/lib/src/writer_report.dart @@ -1,13 +1,11 @@ -library linkcheck.writer_report; - import 'dart:io' show Stdout; import 'dart:math' show min; import 'package:console/console.dart'; import 'crawl.dart' show CrawlResult; -import 'link.dart'; import 'destination.dart'; +import 'link.dart'; /// Writes the reports from the perspective of a website writer - which pages /// reference broken links. @@ -42,9 +40,9 @@ void reportForWriters(CrawlResult result, bool ansiTerm, .toList(growable: false); sourceUris.sort((a, b) => a.toString().compareTo(b.toString())); - TextPen pen; + TextPen? ansiPen; if (ansiTerm) { - pen = TextPen(); + ansiPen = TextPen(); } List brokenSeeds = result.destinations @@ -55,8 +53,8 @@ void reportForWriters(CrawlResult result, bool ansiTerm, if (brokenSeeds.isNotEmpty) { print("Provided URLs failing:"); for (var destination in brokenSeeds) { - if (ansiTerm) { - pen + if (ansiPen != null) { + ansiPen .reset() .yellow() .text(destination.url) @@ -80,8 +78,8 @@ void reportForWriters(CrawlResult result, bool ansiTerm, print("Access to these URLs denied by robots.txt, " "so we couldn't check them:"); for (var destination in deniedByRobots) { - if (ansiTerm) { - pen + if (ansiPen != null) { + ansiPen .reset() .normal() .text("- ") @@ -101,8 +99,8 @@ void reportForWriters(CrawlResult result, bool ansiTerm, // TODO: report invalid links for (var uri in sourceUris) { - if (ansiTerm) { - printWithAnsi(uri, problematic, pen); + if (ansiPen != null) { + printWithAnsi(uri, problematic, ansiPen); } else { printWithoutAnsi(uri, problematic, stdout); } @@ -122,8 +120,8 @@ void reportForWriters(CrawlResult result, bool ansiTerm, brokenUris.sort((a, b) => a.toString().compareTo(b.toString())); for (var uri in brokenUris) { - if (ansiTerm) { - printWithAnsi(uri, broken, pen); + if (ansiPen != null) { + printWithAnsi(uri, broken, ansiPen); } else { printWithoutAnsi(uri, broken, stdout); } @@ -192,11 +190,12 @@ void printWithoutAnsi(Uri uri, List broken, Stdout stdout) { var links = broken.where((link) => link.origin.uri == uri); for (var link in links) { String tag = _buildTagSummary(link); + var linkFragment = link.fragment; print("- (${link.origin.span.start.line + 1}" ":${link.origin.span.start.column}) " "$tag" "=> ${link.destination.url}" - "${link.fragment == null ? '' : '#' + link.fragment} " + "${linkFragment == null ? '' : '#$linkFragment'} " "(${link.destination.statusDescription}" "${!link.destination.isBroken && link.breaksAnchor ? ' but missing anchor' : ''}" ")"); diff --git a/pubspec.lock b/pubspec.lock index 9574d03..064e1cd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,456 +5,529 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: "8c7478991c7bbde2c1e18034ac697723176a5d3e7e0ca06c7f9aed69b6f388d7" + url: "https://pub.dev" source: hosted - version: "22.0.0" + version: "51.0.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: "120fe7ce25377ba616bb210e7584983b163861f45d6ec446744d507e3943881b" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "5.3.1" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: ed7cc591a948744994714375caf9a2ce89e1d82e8243997c8a2994d57181c212 + url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.3.5" args: dependency: "direct main" description: name: args - url: "https://pub.dartlang.org" + sha256: b003c3098049a51720352d219b0bb5f219b60fbfb68e7a4748139a06a5676515 + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.10.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" build_cli_annotations: dependency: transitive description: name: build_cli_annotations - url: "https://pub.dartlang.org" + sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + url: "https://pub.dev" source: hosted version: "2.0.1" cli_pkg: dependency: "direct dev" description: name: cli_pkg - url: "https://pub.dartlang.org" + sha256: b564e39edc96126238c2f0371b2ef0420fc2633e6e0780c9eadd6d1ec21cbb96 + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.6" cli_util: dependency: transitive description: name: cli_util - url: "https://pub.dartlang.org" + sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.5" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" console: dependency: "direct main" description: name: console - url: "https://pub.dartlang.org" + sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a + url: "https://pub.dev" source: hosted version: "4.1.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.1" coverage: dependency: transitive description: name: coverage - url: "https://pub.dartlang.org" + sha256: d2494157c32b303f47dedee955b1479f2979c4ff66934eb7c0def44fd9e0267a + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.6.1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" csslib: dependency: "direct main" description: name: csslib - url: "https://pub.dartlang.org" + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.17.2" dhttpd: dependency: "direct dev" description: name: dhttpd - url: "https://pub.dartlang.org" + sha256: e7e5735549acb0d1d7f5101281dac700e8a444e3563f9c212d16196dc40384f4 + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.4" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.2.0" glob: dependency: "direct main" description: name: glob - url: "https://pub.dartlang.org" + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.1" grinder: dependency: "direct dev" description: name: grinder - url: "https://pub.dartlang.org" + sha256: b24948a441fc65d07bc8b219d7ee8d6cc0af4cdb13823e0d3be6d848eb787b04 + url: "https://pub.dev" source: hosted - version: "0.9.0" + version: "0.9.2" html: dependency: "direct main" description: name: html - url: "https://pub.dartlang.org" + sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.15.1" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted - version: "0.13.3" + version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.6.5" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: "3520fa844009431b5d4491a5a778603520cdc399ab3406332dcc50f93547258c" + url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.7.0" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" + source: hosted + version: "2.0.1" logging: - dependency: "direct main" + dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: c0bbfe94d46aedf9b8b3e695cf3bd48c8e14b35e3b2c639e0aa7755d589ba946 + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" source: hosted - version: "0.12.10" + version: "0.12.13" meta: - dependency: transitive + dependency: "direct main" description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.8.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: "52e38f7e1143ef39daf532117d6b8f8f617bf4bcd6044ed8c29040d20d269630" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.3" node_interop: dependency: transitive description: name: node_interop - url: "https://pub.dartlang.org" + sha256: "3af2420c728173806f4378cf89c53ba9f27f7f67792b898561bff9d390deb98e" + url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" node_preamble: dependency: transitive description: name: node_preamble - url: "https://pub.dartlang.org" + sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d" + url: "https://pub.dev" source: hosted version: "2.0.1" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" path: dependency: "direct main" description: name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - pedantic: - dependency: "direct dev" - description: - name: pedantic - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.8.3" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "5.1.0" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + url: "https://pub.dev" + source: hosted + version: "3.6.2" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.4.0" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler - url: "https://pub.dartlang.org" + sha256: aef74dc9195746a384843102142ab65b6a4735bb3beea791e63527b88cc83306 + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" shelf_static: dependency: transitive description: name: shelf_static - url: "https://pub.dartlang.org" + sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.3" source_map_stack_trace: dependency: transitive description: name: source_map_stack_trace - url: "https://pub.dartlang.org" + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" source_maps: dependency: transitive description: name: source_maps - url: "https://pub.dartlang.org" + sha256: "490098075234dcedb83c5d949b4c93dad5e6b7702748de000be2b57b8e6b2427" + url: "https://pub.dev" source: hosted - version: "0.10.10" + version: "0.10.11" source_span: dependency: "direct main" description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: "direct main" description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test: dependency: "direct dev" description: name: test - url: "https://pub.dartlang.org" + sha256: "98403d1090ac0aa9e33dfc8bf45cc2e0c1d5c58d7cb832cee1e50bf14f37961d" + url: "https://pub.dev" source: hosted - version: "1.17.8" + version: "1.22.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: c9282698e2982b6c3817037554e52f99d4daba493e8028f8112a83d68ccd0b12 + url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.17" test_core: dependency: transitive description: name: test_core - url: "https://pub.dartlang.org" + sha256: c9e4661a5e6285b795d47ba27957ed8b6f980fc020e98b218e276e88aff02168 + url: "https://pub.dev" source: hosted - version: "0.3.28" + version: "0.4.21" test_process: dependency: transitive description: name: test_process - url: "https://pub.dartlang.org" + sha256: b0e6702f58272d459d5b80b88696483f86eac230dab05ecf73d0662e305d005e + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.3" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" vm_service: dependency: transitive description: name: vm_service - url: "https://pub.dartlang.org" + sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 + url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "9.4.0" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: "3a969ddcc204a3e34e863d204b29c0752716f78b6f9cc8235083208d268a4ccd" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol - url: "https://pub.dartlang.org" + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.0" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: "80d494c09849dc3f899d227a78c30c5b949b985ededf884cb3f3bcd39f4b447a" + url: "https://pub.dev" source: hosted - version: "5.1.2" + version: "5.4.1" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" sdks: - dart: ">=2.16.0 <3.0.0" + dart: ">=2.18.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 22cbc60..cbba6b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,32 +1,32 @@ name: linkcheck -version: 2.0.23 # Don't forget to update in lib/linkcheck.dart, too. +version: 3.0.0-dev # Don't forget to update in lib/linkcheck.dart, too. description: >- A very fast link-checker. Crawls sites and checks integrity of links both in HTML and in CSS. -homepage: https://github.com/filiph/linkcheck +repository: https://github.com/filiph/linkcheck environment: - sdk: '>=2.11.99 <3.0.0' + sdk: '>=2.18.0 <4.0.0' dependencies: - args: '>=1.5.1 <3.0.0' + args: ^2.2.0 console: ^4.1.0 csslib: ^0.17.0 - glob: ^2.0.1 + glob: ^2.1.0 html: ^0.15.0 - logging: ^1.0.1 - path: ^1.6.2 - source_span: ^1.5.5 - stream_channel: ^2.0.0 + meta: ^1.8.0 + path: ^1.8.0 + source_span: ^1.8.0 + stream_channel: ^2.1.0 dev_dependencies: + lints: ^2.0.1 dhttpd: ^4.0.0 - pedantic: ^1.7.0 - test: ^1.6.3 - cli_pkg: ^2.1.1 - grinder: ^0.9.0 + test: ^1.22.1 + cli_pkg: ^2.1.6 + grinder: ^0.9.2 executables: linkcheck: linkcheck diff --git a/test/e2e_test.dart b/test/e2e_test.dart index fd0d7ac..684ec21 100644 --- a/test/e2e_test.dart +++ b/test/e2e_test.dart @@ -1,5 +1,3 @@ -library linkcheck.e2e_test; - import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -12,7 +10,7 @@ import 'package:test/test.dart'; // Get the directory of the script being run. void main() { group("linkcheck e2e", () { - _MockStdout out; + late _MockStdout out; int port = 4321; setUp(() { @@ -209,10 +207,10 @@ void main() { }, tags: ["integration"]); } -var directory = path.absolute(path.dirname(scriptPath)); -var scriptPath = scriptUri.toFilePath(); +String directory = path.absolute(path.dirname(scriptPath)); +String scriptPath = scriptUri.toFilePath(); -var scriptUri = Platform.script; +Uri scriptUri = Platform.script; String getServingPath(int caseNumber) => path.join(directory, "case$caseNumber"); @@ -224,14 +222,14 @@ class _MockStdout implements Stdout { StringBuffer buf = StringBuffer(); @override - final Encoding encoding = Encoding.getByName("utf-8"); + final Encoding encoding = const Utf8Codec(); _MockStdout() { // _sink = _controller.sink; } @override - Future get done => throw UnimplementedError(); + Never get done => throw UnimplementedError(); @override set encoding(Encoding encoding) { @@ -264,36 +262,36 @@ class _MockStdout implements Stdout { } @override - void addError(error, [StackTrace stackTrace]) { + Never addError(Object error, [StackTrace? stackTrace]) { throw error; // _sink.addError(error, stackTrace); } @override - Future addStream(Stream> stream) => throw UnimplementedError(); + Never addStream(Stream> stream) => throw UnimplementedError(); void clearOutput() { buf.clear(); } @override - Future close() async { + Future close() async { // await _sink.close(); // await _controller.close(); } @override - Future flush() => throw UnimplementedError(); + Never flush() => throw UnimplementedError(); @override - void write(Object object) { + void write(Object? object) { String string = '$object'; buf.write(string); } @override - void writeAll(Iterable objects, [String sep = ""]) { - Iterator iterator = objects.iterator; + void writeAll(Iterable objects, [String sep = ""]) { + var iterator = objects.iterator; if (!iterator.moveNext()) return; if (sep.isEmpty) { do { @@ -314,7 +312,8 @@ class _MockStdout implements Stdout { } @override - void writeln([Object object = ""]) { + void writeln([Object? object]) { + object ??= ''; write(object); write("\n"); } diff --git a/test/glob_test.dart b/test/glob_test.dart index 1acb943..6cb38e7 100644 --- a/test/glob_test.dart +++ b/test/glob_test.dart @@ -1,3 +1,4 @@ +import 'package:linkcheck/src/worker/worker.dart'; import 'package:test/test.dart'; import 'dart:async'; @@ -5,25 +6,25 @@ import 'package:linkcheck/src/worker/fetch_options.dart'; void main() { test("parses simple example", () { - var sink = StreamController>(); + var sink = StreamController(); var options = FetchOptions(sink); Uri uri = Uri.parse("http://localhost:4000/"); - options.addHostGlobs([uri.toString() + "**"]); + options.addHostGlobs(["$uri**"]); expect(options.matchesAsInternal(uri), isTrue); sink.close(); }); test("parses localhost:4000/guides", () { - var sink = StreamController>(); + var sink = StreamController(); var options = FetchOptions(sink); Uri uri = Uri.parse("http://localhost:4000/guides"); - options.addHostGlobs([uri.toString() + "**"]); + options.addHostGlobs(["$uri**"]); expect(options.matchesAsInternal(uri), isTrue); sink.close(); }); test("parses localhost:4000/guides/", () { - var sink = StreamController>(); + var sink = StreamController(); var options = FetchOptions(sink); Uri uri = Uri.parse("http://localhost:4000/guides/"); options.addHostGlobs(["http://localhost:4000/guides**"]); diff --git a/tool/grind.dart b/tool/grind.dart index fa8d78a..6cbca29 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -11,7 +11,7 @@ void main(List args) { @DefaultTask() @Task() -Future test() => TestRunner().testAsync(); +Future test() => TestRunner().testAsync(); @Task() void clean() => defaultClean();