diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index e8ccdd7dc..90405bbd0 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -8,7 +8,7 @@ jobs: build: name: Build - runs-on: ubuntu-latest + runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -16,19 +16,24 @@ jobs: include: - arch: x64 lib: lib64 + runner: ubuntu-latest platform: linux/amd64 - arch: ia32 lib: lib + runner: ubuntu-latest platform: linux/amd64 - arch: arm64 lib: lib64 + runner: linux-arm64 platform: linux/arm64 - arch: arm lib: lib + runner: linux-arm64 platform: linux/arm64 # There is no docker image for riscv64 dart-sdk, build kernel snapshot instead. - arch: riscv64 lib: lib64 + runner: ubuntu-latest platform: linux/amd64 # linux/riscv64 steps: diff --git a/.github/workflows/build-linux-musl.yml b/.github/workflows/build-linux-musl.yml index 5f4ba2639..aa33b4eff 100644 --- a/.github/workflows/build-linux-musl.yml +++ b/.github/workflows/build-linux-musl.yml @@ -8,27 +8,27 @@ jobs: build: name: Build - runs-on: ubuntu-latest + runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - arch: x64 + runner: ubuntu-latest platform: linux/amd64 - arch: ia32 + runner: ubuntu-latest platform: linux/386 - arch: arm64 + runner: linux-arm64 platform: linux/arm64 - # There is a bug in qemu's mremap causing pthread_getattr_np in musl to stuck in a loop on arm. - # Unless qemu fixes the bug or we get a real linux-arm runner, we cannot build aot-snapshot - # for arm on CI. So, we create a kernel snapshot for arm build in amd64 container instead. - # https://gitlab.com/qemu-project/qemu/-/issues/1729 - arch: arm - platform: linux/amd64 # linux/arm/v7 - # There is no docker image for riscv64 dart-sdk, build kernel snapshot instead. + runner: linux-arm64 + platform: linux/arm/v7 - arch: riscv64 - platform: linux/amd64 # linux/riscv64 + runner: ubuntu-latest + platform: linux/riscv64 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index c8f5ddd41..bf28d809f 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -8,24 +8,27 @@ jobs: build: name: Build - runs-on: ubuntu-latest + runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - arch: x64 + runner: ubuntu-latest platform: linux/amd64 - arch: ia32 - platform: linux/amd64 - - arch: arm - platform: linux/arm/v7 + runner: ubuntu-latest + platform: linux/386 - arch: arm64 + runner: linux-arm64 platform: linux/arm64 - # There is no docker image for riscv64 dart-sdk, build kernel snapshot instead. - # https://github.com/dart-lang/dart-docker/issues/96#issuecomment-1669860829 + - arch: arm + runner: linux-arm64 + platform: linux/arm/v7 - arch: riscv64 - platform: linux/amd64 # linux/riscv64 + runner: ubuntu-latest + platform: linux/riscv64 steps: - uses: actions/checkout@v4 @@ -46,6 +49,7 @@ jobs: EOF - name: Build + if: matrix.arch != 'riscv64' run: | docker run --rm -i \ --platform ${{ matrix.platform }} \ @@ -57,6 +61,35 @@ jobs: dart run grinder pkg-standalone-linux-${{ matrix.arch }} EOF + # https://github.com/dart-lang/dart-docker/issues/96#issuecomment-1669860829 + # There is no official riscv64 dart container image yet, build on debian:unstable instead. + # The setup is adopted from: https://github.com/dart-lang/dart-docker/blob/main/Dockerfile-debian.template + - name: Build riscv64 + if: matrix.arch == 'riscv64' + run: | + DART_CHANNEL=stable + DART_VERSION=$(curl -fsSL https://storage.googleapis.com/dart-archive/channels/$DART_CHANNEL/release/latest/VERSION | yq .version) + curl -fsSLO https://storage.googleapis.com/dart-archive/channels/$DART_CHANNEL/release/$DART_VERSION/sdk/dartsdk-linux-${{ matrix.arch }}-release.zip + + docker run --rm -i \ + --platform ${{ matrix.platform }} \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + docker.io/library/debian:unstable-slim <<'EOF' + set -e + apt-get update + apt-get install -y --no-install-recommends ca-certificates curl dnsutils git openssh-client unzip + + export DART_SDK=/usr/lib/dart + export PATH=$DART_SDK/bin:/root/.pub-cache/bin:$PATH + + SDK="dartsdk-linux-${{ matrix.arch }}-release.zip" + unzip "$SDK" && mv dart-sdk "$DART_SDK" && rm "$SDK" + + dart pub get + dart run grinder pkg-standalone-linux-${{ matrix.arch }} + EOF + - name: Upload Artifact uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 3dff23d3b..660ceadfa 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -17,7 +17,7 @@ jobs: - arch: x64 runner: macos-13 - arch: arm64 - runner: macos-14 + runner: macos-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index ba9ff8bb1..86ea17c22 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -19,7 +19,7 @@ jobs: - arch: ia32 runner: windows-latest - arch: arm64 - runner: windows-latest + runner: windows-arm64 steps: - uses: actions/checkout@v4 diff --git a/.pubignore b/.pubignore index 2fbab300a..08992ce75 100644 --- a/.pubignore +++ b/.pubignore @@ -1,5 +1,5 @@ # This should be identical to .gitignore except that it doesn't exclude -# generated protobuf files. +# generated Dart files. .buildlog .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d64f38c1..5229a0b08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +## 1.77.6 + +* Fix a few cases where comments and occasionally even whitespace wasn't allowed + between the end of Sass statements and the following semicolon. + +## 1.77.5 + +* Fully trim redundant selectors generated by `@extend`. + +## 1.77.4 + +### Embedded Sass + +* Support passing `Version` input for `fatalDeprecations` as string over + embedded protocol. + +* Fix a bug in the JS Embedded Host where `Version` could be incorrectly accepted + as input for `silenceDeprecations` and `futureDeprecations` in pure JS. + +## 1.77.3 + +### Dart API + +* `Deprecation.duplicateVariableFlags` has been deprecated and replaced with + `Deprecation.duplicateVarFlags` to make it consistent with the + `duplicate-var-flags` name used on the command line and in the JS API. + ## 1.77.2 * Don't emit deprecation warnings for functions and mixins beginning with `__`. diff --git a/lib/src/ast/selector/compound.dart b/lib/src/ast/selector/compound.dart index bcc2beb33..9fbc8d397 100644 --- a/lib/src/ast/selector/compound.dart +++ b/lib/src/ast/selector/compound.dart @@ -42,6 +42,18 @@ final class CompoundSelector extends Selector { SimpleSelector? get singleSimple => components.length == 1 ? components.first : null; + /// Whether any simple selector in this contains a selector that requires + /// complex non-local reasoning to determine whether it's a super- or + /// sub-selector. + /// + /// This includes both pseudo-elements and pseudo-selectors that take + /// selectors as arguments. + /// + /// #nodoc + @internal + late final bool hasComplicatedSuperselectorSemantics = components + .any((component) => component.hasComplicatedSuperselectorSemantics); + CompoundSelector(Iterable components, super.span) : components = List.unmodifiable(components) { if (this.components.isEmpty) { diff --git a/lib/src/ast/selector/pseudo.dart b/lib/src/ast/selector/pseudo.dart index 44a263d15..e632c651e 100644 --- a/lib/src/ast/selector/pseudo.dart +++ b/lib/src/ast/selector/pseudo.dart @@ -67,6 +67,10 @@ final class PseudoSelector extends SimpleSelector { bool get isHostContext => isClass && name == 'host-context' && selector != null; + @internal + bool get hasComplicatedSuperselectorSemantics => + isElement || selector != null; + /// The non-selector argument passed to this selector. /// /// This is `null` if there's no argument. If [argument] and [selector] are diff --git a/lib/src/ast/selector/simple.dart b/lib/src/ast/selector/simple.dart index d8ae7864d..5ac2f0e53 100644 --- a/lib/src/ast/selector/simple.dart +++ b/lib/src/ast/selector/simple.dart @@ -34,6 +34,16 @@ abstract base class SimpleSelector extends Selector { /// sequence will contain 1000 simple selectors. int get specificity => 1000; + /// Whether this requires complex non-local reasoning to determine whether + /// it's a super- or sub-selector. + /// + /// This includes both pseudo-elements and pseudo-selectors that take + /// selectors as arguments. + /// + /// #nodoc + @internal + bool get hasComplicatedSuperselectorSemantics => false; + SimpleSelector(super.span); /// Parses a simple selector from [contents]. diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart index b13180e10..cec976714 100644 --- a/lib/src/deprecation.dart +++ b/lib/src/deprecation.dart @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC. Use of this source code is governed by an +// Copyright 2024 Google LLC. Use of this source code is governed by an // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. @@ -10,34 +10,42 @@ import 'util/nullable.dart'; /// A deprecated feature in the language. enum Deprecation { - /// Deprecation for passing a string to `call` instead of `get-function`. + // START AUTOGENERATED CODE + // + // DO NOT EDIT. This section was generated from the language repo. + // See tool/grind/generate_deprecations.dart for details. + // + // Checksum: 22d9bdbe92eb39b3c0d6d64ebe1879a431c0037e + + /// Deprecation for passing a string directly to meta.call(). callString('call-string', deprecatedIn: '0.0.0', description: 'Passing a string directly to meta.call().'), - /// Deprecation for `@elseif`. + /// Deprecation for @elseif. elseif('elseif', deprecatedIn: '1.3.2', description: '@elseif.'), - /// Deprecation for parsing `@-moz-document`. + /// Deprecation for @-moz-document. mozDocument('moz-document', deprecatedIn: '1.7.2', description: '@-moz-document.'), - /// Deprecation for importers using relative canonical URLs. - relativeCanonical('relative-canonical', deprecatedIn: '1.14.2'), + /// Deprecation for imports using relative canonical URLs. + relativeCanonical('relative-canonical', + deprecatedIn: '1.14.2', + description: 'Imports using relative canonical URLs.'), - /// Deprecation for declaring new variables with `!global`. + /// Deprecation for declaring new variables with !global. newGlobal('new-global', deprecatedIn: '1.17.2', description: 'Declaring new variables with !global.'), - /// Deprecation for certain functions in the color module matching the - /// behavior of their global counterparts for compatiblity reasons. + /// Deprecation for using color module functions in place of plain CSS functions. colorModuleCompat('color-module-compat', deprecatedIn: '1.23.0', description: 'Using color module functions in place of plain CSS functions.'), - /// Deprecation for treating `/` as division. + /// Deprecation for / operator for division. slashDiv('slash-div', deprecatedIn: '1.33.0', description: '/ operator for division.'), @@ -46,46 +54,55 @@ enum Deprecation { deprecatedIn: '1.54.0', description: 'Leading, trailing, and repeated combinators.'), - /// Deprecation for ambiguous `+` and `-` operators. + /// Deprecation for ambiguous + and - operators. strictUnary('strict-unary', deprecatedIn: '1.55.0', description: 'Ambiguous + and - operators.'), - /// Deprecation for passing invalid units to certain built-in functions. + /// Deprecation for passing invalid units to built-in functions. functionUnits('function-units', deprecatedIn: '1.56.0', description: 'Passing invalid units to built-in functions.'), - /// Deprecation for passing percentages to the Sass abs() function. - absPercent('abs-percent', - deprecatedIn: '1.65.0', - description: 'Passing percentages to the Sass abs() function.'), - - duplicateVariableFlags('duplicate-var-flags', + /// Deprecation for using !default or !global multiple times for one variable. + duplicateVarFlags('duplicate-var-flags', deprecatedIn: '1.62.0', description: 'Using !default or !global multiple times for one variable.'), + /// Deprecation for passing null as alpha in the ${isJS ? 'JS': 'Dart'} API. nullAlpha('null-alpha', deprecatedIn: '1.62.3', description: 'Passing null as alpha in the ${isJS ? 'JS' : 'Dart'} API.'), + /// Deprecation for passing percentages to the Sass abs() function. + absPercent('abs-percent', + deprecatedIn: '1.65.0', + description: 'Passing percentages to the Sass abs() function.'), + + /// Deprecation for using the current working directory as an implicit load path. fsImporterCwd('fs-importer-cwd', deprecatedIn: '1.73.0', description: 'Using the current working directory as an implicit load path.'), + /// Deprecation for function and mixin names beginning with --. cssFunctionMixin('css-function-mixin', deprecatedIn: '1.76.0', description: 'Function and mixin names beginning with --.'), - @Deprecated('This deprecation name was never actually used.') - calcInterp('calc-interp', deprecatedIn: null), - - /// Deprecation for `@import` rules. + /// Deprecation for @import rules. import.future('import', description: '@import rules.'), + // END AUTOGENERATED CODE + /// Used for deprecations coming from user-authored code. - userAuthored('user-authored', deprecatedIn: null); + userAuthored('user-authored', deprecatedIn: null), + + @Deprecated('This deprecation name was never actually used.') + calcInterp('calc-interp', deprecatedIn: null); + + @Deprecated('Use duplicateVarFlags instead.') + static const duplicateVariableFlags = duplicateVarFlags; /// A unique ID for this deprecation in kebab case. /// diff --git a/lib/src/embedded/compilation_dispatcher.dart b/lib/src/embedded/compilation_dispatcher.dart index 675216022..e8c3764b5 100644 --- a/lib/src/embedded/compilation_dispatcher.dart +++ b/lib/src/embedded/compilation_dispatcher.dart @@ -7,10 +7,10 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; -import 'package:collection/collection.dart'; import 'package:native_synchronization/mailbox.dart'; import 'package:path/path.dart' as p; import 'package:protobuf/protobuf.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:sass/sass.dart' as sass; import 'package:sass/src/importer/node_package.dart' as npi; @@ -125,20 +125,34 @@ final class CompilationDispatcher { : EmbeddedLogger(this, color: request.alertColor, ascii: request.alertAscii); - sass.Deprecation? deprecationOrWarn(String id) { - var deprecation = sass.Deprecation.fromId(id); - if (deprecation == null) { - logger.warn('Invalid deprecation "$id".'); - } - return deprecation; + Iterable? parseDeprecationsOrWarn( + Iterable deprecations, + {bool supportVersions = false}) { + return () sync* { + for (var item in deprecations) { + var deprecation = sass.Deprecation.fromId(item); + if (deprecation == null) { + if (supportVersions) { + try { + yield* sass.Deprecation.forVersion(Version.parse(item)); + } on FormatException { + logger.warn('Invalid deprecation id or version "$item".'); + } + } else { + logger.warn('Invalid deprecation id "$item".'); + } + } else { + yield deprecation; + } + } + }(); } - var fatalDeprecations = - request.fatalDeprecation.map(deprecationOrWarn).whereNotNull(); + var fatalDeprecations = parseDeprecationsOrWarn(request.fatalDeprecation, + supportVersions: true); var silenceDeprecations = - request.silenceDeprecation.map(deprecationOrWarn).whereNotNull(); - var futureDeprecations = - request.futureDeprecation.map(deprecationOrWarn).whereNotNull(); + parseDeprecationsOrWarn(request.silenceDeprecation); + var futureDeprecations = parseDeprecationsOrWarn(request.futureDeprecation); try { var importers = request.importers.map((importer) => diff --git a/lib/src/extend/extension_store.dart b/lib/src/extend/extension_store.dart index 3637b5aac..2bbc2a9cb 100644 --- a/lib/src/extend/extension_store.dart +++ b/lib/src/extend/extension_store.dart @@ -901,13 +901,6 @@ class ExtensionStore { // document, and thus should never be trimmed. List _trim(List selectors, bool isOriginal(ComplexSelector complex)) { - // Avoid truly horrific quadratic behavior. - // - // TODO(nweiz): I think there may be a way to get perfect trimming without - // going quadratic by building some sort of trie-like data structure that - // can be used to look up superselectors. - if (selectors.length > 100) return selectors; - // This is n² on the sequences, but only comparing between separate // sequences should limit the quadratic behavior. We iterate from last to // first and reverse the result so that, if two selectors are identical, we diff --git a/lib/src/extend/functions.dart b/lib/src/extend/functions.dart index 01d70d248..2fb1f2555 100644 --- a/lib/src/extend/functions.dart +++ b/lib/src/extend/functions.dart @@ -646,24 +646,28 @@ bool complexIsSuperselector(List complex1, var component1 = complex1[i1]; if (component1.combinators.length > 1) return false; if (remaining1 == 1) { - var parents = complex2.sublist(i2, complex2.length - 1); - if (parents.any((parent) => parent.combinators.length > 1)) return false; - - return compoundIsSuperselector( - component1.selector, complex2.last.selector, - parents: parents); + if (complex2.any((parent) => parent.combinators.length > 1)) { + return false; + } else { + return compoundIsSuperselector( + component1.selector, complex2.last.selector, + parents: component1.selector.hasComplicatedSuperselectorSemantics + ? complex2.sublist(i2, complex2.length - 1) + : null); + } } // Find the first index [endOfSubselector] in [complex2] such that // `complex2.sublist(i2, endOfSubselector + 1)` is a subselector of // [component1.selector]. var endOfSubselector = i2; - List? parents; while (true) { var component2 = complex2[endOfSubselector]; if (component2.combinators.length > 1) return false; if (compoundIsSuperselector(component1.selector, component2.selector, - parents: parents)) { + parents: component1.selector.hasComplicatedSuperselectorSemantics + ? complex2.sublist(i2, endOfSubselector) + : null)) { break; } @@ -675,13 +679,10 @@ bool complexIsSuperselector(List complex1, // to match. return false; } - - parents ??= []; - parents.add(component2); } if (!_compatibleWithPreviousCombinator( - previousCombinator, parents ?? const [])) { + previousCombinator, complex2.take(endOfSubselector).skip(i2))) { return false; } @@ -717,8 +718,8 @@ bool complexIsSuperselector(List complex1, /// Returns whether [parents] are valid intersitial components between one /// complex superselector and another, given that the earlier complex /// superselector had the combinator [previous]. -bool _compatibleWithPreviousCombinator( - CssValue? previous, List parents) { +bool _compatibleWithPreviousCombinator(CssValue? previous, + Iterable parents) { if (parents.isEmpty) return true; if (previous == null) return true; @@ -754,6 +755,13 @@ bool _isSupercombinator( bool compoundIsSuperselector( CompoundSelector compound1, CompoundSelector compound2, {Iterable? parents}) { + if (!compound1.hasComplicatedSuperselectorSemantics && + !compound2.hasComplicatedSuperselectorSemantics) { + if (compound1.components.length > compound2.components.length) return false; + return compound1.components + .every((simple1) => compound2.components.any(simple1.isSuperselector)); + } + // Pseudo elements effectively change the target of a compound selector rather // than narrowing the set of elements to which it applies like other // selectors. As such, if either selector has a pseudo element, they both must diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index e72e2527b..046c56e6f 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -235,7 +235,7 @@ abstract class StylesheetParser extends Parser { case 'default': if (guarded) { logger.warnForDeprecation( - Deprecation.duplicateVariableFlags, + Deprecation.duplicateVarFlags, '!default should only be written once for each variable.\n' 'This will be an error in Dart Sass 2.0.0.', span: scanner.spanFrom(flagStart)); @@ -248,7 +248,7 @@ abstract class StylesheetParser extends Parser { scanner.spanFrom(flagStart)); } else if (global) { logger.warnForDeprecation( - Deprecation.duplicateVariableFlags, + Deprecation.duplicateVarFlags, '!global should only be written once for each variable.\n' 'This will be an error in Dart Sass 2.0.0.', span: scanner.spanFrom(flagStart)); @@ -770,10 +770,15 @@ abstract class StylesheetParser extends Parser { scanner.spanFrom(start)); } + var beforeWhitespace = scanner.location; whitespace(); - var arguments = scanner.peekChar() == $lparen - ? _argumentInvocation(mixin: true) - : ArgumentInvocation.empty(scanner.emptySpan); + ArgumentInvocation arguments; + if (scanner.peekChar() == $lparen) { + arguments = _argumentInvocation(mixin: true); + whitespace(); + } else { + arguments = ArgumentInvocation.empty(beforeWhitespace.pointSpan()); + } expectStatementSeparator("@content rule"); return ContentRule(arguments, scanner.spanFrom(start)); @@ -835,7 +840,10 @@ abstract class StylesheetParser extends Parser { var value = almostAnyValue(); var optional = scanner.scanChar($exclamation); - if (optional) expectIdentifier("optional"); + if (optional) { + expectIdentifier("optional"); + whitespace(); + } expectStatementSeparator("@extend rule"); return ExtendRule(value, scanner.spanFrom(start), optional: optional); } @@ -954,6 +962,7 @@ abstract class StylesheetParser extends Parser { } var configuration = _configuration(allowGuarded: true); + whitespace(); expectStatementSeparator("@forward rule"); var span = scanner.spanFrom(start); @@ -1419,8 +1428,7 @@ abstract class StylesheetParser extends Parser { var namespace = _useNamespace(url, start); whitespace(); var configuration = _configuration(); - - expectStatementSeparator("@use rule"); + whitespace(); var span = scanner.spanFrom(start); if (!_isUseAllowed) { diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index c0839df75..3ea002075 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,19 @@ +## 10.4.6 + +* No user-visible changes. + +## 10.4.5 + +* No user-visible changes. + +## 10.4.4 + +* No user-visible changes. + +## 10.4.3 + +* No user-visible changes. + ## 10.4.2 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index e11f303a5..bcd120d20 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -2,7 +2,7 @@ name: sass_api # Note: Every time we add a new Sass AST node, we need to bump the *major* # version because it's a breaking change for anyone who's implementing the # visitor interface(s). -version: 10.4.2 +version: 10.4.6 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: - sass: 1.77.2 + sass: 1.77.6 dev_dependencies: dartdoc: ">=6.0.0 <9.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index bb285238e..936b32092 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.77.2 +version: 1.77.6 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass @@ -48,7 +48,7 @@ dev_dependencies: lints: ">=2.0.0 <5.0.0" protoc_plugin: ">=20.0.0 <22.0.0" pub_api_client: ^2.1.1 - pubspec_parse: ^1.0.0 + pubspec_parse: ^1.3.0 test: ^1.16.7 test_descriptor: ^2.0.0 test_process: ^2.0.0 diff --git a/test/double_check_test.dart b/test/double_check_test.dart index ee87b922d..44def85ae 100644 --- a/test/double_check_test.dart +++ b/test/double_check_test.dart @@ -7,25 +7,39 @@ import 'dart:io'; import 'dart:convert'; +import 'package:crypto/crypto.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/test.dart'; +import '../tool/grind/generate_deprecations.dart' as deprecations; import '../tool/grind/synchronize.dart' as synchronize; /// Tests that double-check that everything in the repo looks sensible. void main() { - group("synchronized file is up-to-date:", () { - synchronize.sources.forEach((sourcePath, targetPath) { - test(targetPath, () { - if (File(targetPath).readAsStringSync() != - synchronize.synchronizeFile(sourcePath)) { - fail("$targetPath is out-of-date.\n" - "Run `dart pub run grinder` to update it."); - } + group("up-to-date generated", () { + group("synchronized file:", () { + synchronize.sources.forEach((sourcePath, targetPath) { + test(targetPath, () { + if (File(targetPath).readAsStringSync() != + synchronize.synchronizeFile(sourcePath)) { + fail("$targetPath is out-of-date.\n" + "Run `dart run grinder` to update it."); + } + }); }); }); + + test("deprecations", () { + var inputText = File(deprecations.yamlPath).readAsStringSync(); + var outputText = File(deprecations.dartPath).readAsStringSync(); + var checksum = sha1.convert(utf8.encode(inputText)); + if (!outputText.contains('// Checksum: $checksum')) { + fail('${deprecations.dartPath} is out-of-date.\n' + 'Run `dart run grinder` to update it.'); + } + }); }, // Windows sees different bytes than other OSes, possibly because of // newline normalization issues. @@ -116,14 +130,8 @@ void main() { }); test("matches dartdoc version", () { - // TODO(nweiz): Just use equals() once dart-lang/pubspec_parse#127 lands - // and is released. - var sassDep = sassPubspec.devDependencies["dartdoc"]; - var pkgDep = pkgPubspec.devDependencies["dartdoc"]; - expect(pkgDep, isA()); - expect(sassDep, isA()); - expect((pkgDep as HostedDependency).version, - equals((sassDep as HostedDependency).version)); + expect(sassPubspec.devDependencies["dartdoc"], + equals(pkgPubspec.devDependencies["dartdoc"])); }); }); } diff --git a/tool/grind.dart b/tool/grind.dart index 631bdebdf..5f71995f5 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -11,6 +11,7 @@ import 'package:grinder/grinder.dart'; import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; +import 'grind/generate_deprecations.dart'; import 'grind/synchronize.dart'; import 'grind/utils.dart'; @@ -18,8 +19,10 @@ export 'grind/bazel.dart'; export 'grind/benchmark.dart'; export 'grind/double_check.dart'; export 'grind/frameworks.dart'; +export 'grind/generate_deprecations.dart'; export 'grind/subpackages.dart'; export 'grind/synchronize.dart'; +export 'grind/utils.dart'; void main(List args) { pkg.humanName.value = "Dart Sass"; @@ -127,7 +130,7 @@ void main(List args) { } @DefaultTask('Compile async code and reformat.') -@Depends(format, synchronize) +@Depends(format, synchronize, deprecations, protobuf) void all() {} @Task('Run the Dart formatter.') @@ -140,7 +143,7 @@ void npmInstall() => run(Platform.isWindows ? "npm.cmd" : "npm", arguments: ["install"]); @Task('Runs the tasks that are required for running tests.') -@Depends(format, synchronize, protobuf, "pkg-npm-dev", npmInstall, +@Depends(format, synchronize, protobuf, deprecations, "pkg-npm-dev", npmInstall, "pkg-standalone-dev") void beforeTest() {} @@ -213,9 +216,9 @@ String _readAndResolveMarkdown(String path) => File(path) /// Returns a map from JS type declaration file names to their contnets. Map _fetchJSTypes() { - var languageRepo = _updateLanguageRepo(); + updateLanguageRepo(); - var typeRoot = p.join(languageRepo, 'js-api-doc'); + var typeRoot = p.join('build/language', 'js-api-doc'); return { for (var entry in Directory(typeRoot).listSync(recursive: true)) if (entry is File && entry.path.endsWith('.d.ts')) @@ -231,6 +234,7 @@ void _matchError(Match match, String message, {Object? url}) { } @Task('Compile the protocol buffer definition to a Dart library.') +@Depends(updateLanguageRepo) Future protobuf() async { Directory('build').createSync(recursive: true); @@ -250,8 +254,6 @@ dart run protoc_plugin "\$@" run('chmod', arguments: ['a+x', 'build/protoc-gen-dart']); } - _updateLanguageRepo(); - await runAsync("buf", arguments: ["generate"], runOptions: RunOptions(environment: { @@ -321,19 +323,3 @@ String _updateHomebrewLanguageRevision(String formula) { match.group(0)!.replaceFirst(match.group(1)!, languageRepoRevision) + formula.substring(match.end); } - -/// Clones the main branch of `github.com/sass/sass` and returns the path to the -/// clone. -/// -/// If the `UPDATE_SASS_SASS_REPO` environment variable is `false`, this instead -/// assumes the repo that already exists at `build/language/sass`. -/// `UPDATE_SASS_PROTOCOL` is also checked as a deprecated alias for -/// `UPDATE_SASS_SASS_REPO`. -String _updateLanguageRepo() => - // UPDATE_SASS_PROTOCOL is considered deprecated, because it doesn't apply as - // generically to other tasks. - Platform.environment['UPDATE_SASS_SASS_REPO'] != 'false' && - Platform.environment['UPDATE_SASS_PROTOCOL'] != 'false' - ? cloneOrCheckout("https://github.com/sass/sass.git", "main", - name: 'language') - : 'build/language'; diff --git a/tool/grind/double_check.dart b/tool/grind/double_check.dart index 4ec0cd8e7..8b1dca18f 100644 --- a/tool/grind/double_check.dart +++ b/tool/grind/double_check.dart @@ -5,13 +5,12 @@ import 'dart:io'; import 'package:cli_pkg/cli_pkg.dart' as pkg; +import 'package:collection/collection.dart'; import 'package:grinder/grinder.dart'; import 'package:path/path.dart' as p; import 'package:pub_api_client/pub_api_client.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; -import 'package:sass/src/utils.dart'; - import 'utils.dart'; @Task('Verify that the package is in a good state to release.') @@ -21,7 +20,7 @@ Future doubleCheckBeforeRelease() async { fail("GITHUB_REF $ref is different than pubspec version ${pkg.version}."); } - if (listEquals(pkg.version.preRelease, ["dev"])) { + if (const ListEquality().equals(pkg.version.preRelease, ["dev"])) { fail("${pkg.version} is a dev release."); } diff --git a/tool/grind/generate_deprecations.dart b/tool/grind/generate_deprecations.dart new file mode 100644 index 000000000..21ce6f58e --- /dev/null +++ b/tool/grind/generate_deprecations.dart @@ -0,0 +1,82 @@ +// Copyright 2024 Google LLC. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:crypto/crypto.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:grinder/grinder.dart'; +import 'package:yaml/yaml.dart'; + +import 'utils.dart'; + +const yamlPath = 'build/language/spec/deprecations.yaml'; +const dartPath = 'lib/src/deprecation.dart'; + +final _blockRegex = + RegExp(r'// START AUTOGENERATED CODE[\s\S]*?// END AUTOGENERATED CODE'); + +@Task('Generate deprecation.g.dart from the list in the language repo.') +@Depends(updateLanguageRepo) +void deprecations() { + var yamlFile = File(yamlPath); + var dartFile = File(dartPath); + var yamlText = yamlFile.readAsStringSync(); + var data = loadYaml(yamlText, sourceUrl: yamlFile.uri) as Map; + var dartText = dartFile.readAsStringSync(); + var buffer = StringBuffer('''// START AUTOGENERATED CODE + // + // DO NOT EDIT. This section was generated from the language repo. + // See tool/grind/generate_deprecations.dart for details. + // + // Checksum: ${sha1.convert(utf8.encode(yamlText))} + +'''); + for (var MapEntry(:String key, :value) in data.entries) { + var camelCase = key.replaceAllMapped( + RegExp(r'-(.)'), (match) => match.group(1)!.toUpperCase()); + var (description, deprecatedIn, obsoleteIn) = switch (value) { + { + 'description': String description, + 'dart-sass': {'status': 'future'}, + } => + (description, null, null), + { + 'description': String description, + 'dart-sass': {'status': 'active', 'deprecated': String deprecatedIn}, + } => + (description, deprecatedIn, null), + { + 'description': String description, + 'dart-sass': { + 'status': 'obsolete', + 'deprecated': String deprecatedIn, + 'obsolete': String obsoleteIn + }, + } => + (description, deprecatedIn, obsoleteIn), + _ => throw Exception('Invalid deprecation $key: $value') + }; + description = + description.replaceAll(r'$PLATFORM', r"${isJS ? 'JS': 'Dart'}"); + var constructorName = deprecatedIn == null ? '.future' : ''; + var deprecatedClause = + deprecatedIn == null ? '' : "deprecatedIn: '$deprecatedIn', "; + var obsoleteClause = + obsoleteIn == null ? '' : "obsoleteIn: '$obsoleteIn', "; + var comment = 'Deprecation for ${description.substring(0, 1).toLowerCase()}' + '${description.substring(1)}'; + buffer.writeln('/// $comment'); + buffer.writeln( + "$camelCase$constructorName('$key', $deprecatedClause$obsoleteClause" + "description: '$description'),"); + } + buffer.write('\n // END AUTOGENERATED CODE'); + if (!dartText.contains(_blockRegex)) { + fail("Couldn't find block for generated code in lib/src/deprecation.dart"); + } + var newCode = dartText.replaceFirst(_blockRegex, buffer.toString()); + dartFile.writeAsStringSync(DartFormatter().format(newCode)); +} diff --git a/tool/grind/utils.dart b/tool/grind/utils.dart index 21d9f66c8..86ea7ed96 100644 --- a/tool/grind/utils.dart +++ b/tool/grind/utils.dart @@ -100,3 +100,20 @@ void afterTask(String taskName, FutureOr callback()) { await callback(); }); } + +/// Clones the main branch of `github.com/sass/sass`. +/// +/// If the `UPDATE_SASS_SASS_REPO` environment variable is `false`, this instead +/// assumes the repo that already exists at `build/language/sass`. +/// `UPDATE_SASS_PROTOCOL` is also checked as a deprecated alias for +/// `UPDATE_SASS_SASS_REPO`. +@Task('Clones the main branch of `github.com/sass/sass` if necessary.') +void updateLanguageRepo() { + // UPDATE_SASS_PROTOCOL is considered deprecated, because it doesn't apply as + // generically to other tasks. + if (Platform.environment['UPDATE_SASS_SASS_REPO'] != 'false' && + Platform.environment['UPDATE_SASS_PROTOCOL'] != 'false') { + cloneOrCheckout("https://github.com/sass/sass.git", "main", + name: 'language'); + } +}