Skip to content

Commit

Permalink
Merge pull request #710 from stof/prepare_evaluation
Browse files Browse the repository at this point in the history
Prepare the implementation of the new evaluation system
  • Loading branch information
stof committed Apr 3, 2024
2 parents 87bbbc5 + faf8fca commit 030845a
Show file tree
Hide file tree
Showing 28 changed files with 567 additions and 93 deletions.
16 changes: 8 additions & 8 deletions src/Ast/Sass/ArgumentDeclaration.php
Expand Up @@ -17,6 +17,7 @@
use ScssPhp\ScssPhp\Logger\LoggerInterface;
use ScssPhp\ScssPhp\Parser\ScssParser;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Util\StringUtil;

/**
* An argument declaration, as for a function or mixin definition.
Expand Down Expand Up @@ -114,24 +115,23 @@ public function verify(int $positional, array $names): void

if ($positional > \count($this->arguments)) {
$message = sprintf(
'Only %d %sargument%s allowed, but %d %s passed.',
'Only %d %s%s allowed, but %d %s passed.',
\count($this->arguments),
empty($names) ? '' : 'positional ',
\count($this->arguments) === 1 ? '' : 's',
StringUtil::pluralize('argument', \count($this->arguments)),
$positional,
$positional === 1 ? 'was' : 'were'
StringUtil::pluralize('was', $positional, 'were')
);
throw new SassScriptException($message);
}

if ($nameUsed < \count($names)) {
$unknownNames = array_values(array_diff(array_keys($names), array_map(fn($argument) => $argument->getName(), $this->arguments)));
$lastName = array_pop($unknownNames);
\assert(\count($unknownNames) > 0);
$message = sprintf(
'No argument%s named $%s%s.',
$unknownNames ? 's' : '',
$unknownNames ? implode(', $', $unknownNames) . ' or $' : '',
$lastName
'No %s named %s.',
StringUtil::pluralize('argument', \count($unknownNames)),
StringUtil::toSentence(array_map(fn ($name) => '$' . $name, $unknownNames), 'or')
);
throw new SassScriptException($message);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Ast/Selector/PseudoSelector.php
Expand Up @@ -267,7 +267,7 @@ public function unify(array $compound): ?array
}
}

if (EquatableUtil::listContains($compound, $this)) {
if (EquatableUtil::iterableContains($compound, $this)) {
return $compound;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Ast/Selector/SimpleSelector.php
Expand Up @@ -107,7 +107,7 @@ public function unify(array $compound): ?array
}
}

if (EquatableUtil::listContains($compound, $this)) {
if (EquatableUtil::iterableContains($compound, $this)) {
return $compound;
}

Expand Down
24 changes: 22 additions & 2 deletions src/Exception/SassRuntimeException.php
Expand Up @@ -13,6 +13,8 @@
namespace ScssPhp\ScssPhp\Exception;

use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\StackTrace\Trace;
use ScssPhp\ScssPhp\Util;

/**
* @internal
Expand All @@ -31,12 +33,25 @@ final class SassRuntimeException extends \Exception implements SassException
*/
private $span;

public function __construct(string $message, FileSpan $span, \Throwable $previous = null)
private readonly Trace $sassTrace;

public function __construct(string $message, FileSpan $span, ?Trace $sassTrace = null, \Throwable $previous = null)
{
$this->originalMessage = $message;
$this->span = $span;
$this->sassTrace = $sassTrace ?? new Trace([Util::frameForSpan($span, 'root stylesheet')]);

$formattedMessage = $span->message($message); // TODO add the highlighting

foreach (explode("\n", $this->sassTrace->getFormattedTrace()) as $frame) {
if ($frame === '') {
continue;
}
$formattedMessage .= "\n";
$formattedMessage .= ' ' . $frame;
}

parent::__construct($span->message($message), 0, $previous);
parent::__construct($formattedMessage, 0, $previous);
}

/**
Expand All @@ -51,4 +66,9 @@ public function getSpan(): FileSpan
{
return $this->span;
}

public function getSassTrace(): Trace
{
return $this->sassTrace;
}
}
2 changes: 1 addition & 1 deletion src/Extend/ExtendUtil.php
Expand Up @@ -639,7 +639,7 @@ private static function mustUnify(array $complex1, array $complex2): bool

foreach ($complex2 as $component) {
foreach ($component->getSelector()->getComponents() as $simple) {
if (self::isUnique($simple) && EquatableUtil::listContains($uniqueSelectors, $simple)) {
if (self::isUnique($simple) && EquatableUtil::iterableContains($uniqueSelectors, $simple)) {
return true;
}
}
Expand Down
10 changes: 4 additions & 6 deletions src/Parser/InterpolationMap.php
Expand Up @@ -14,6 +14,7 @@

use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\SourceSpan\FileLocation;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\SourceSpan\SourceLocation;
use ScssPhp\ScssPhp\Util\Character;
Expand Down Expand Up @@ -84,7 +85,7 @@ public function mapSpan(FileSpan $target): FileSpan
}

/**
* @return FileSpan|SourceLocation
* @return FileSpan|FileLocation
*/
private function mapLocation(SourceLocation $target): object
{
Expand Down Expand Up @@ -127,11 +128,8 @@ private function indexInContents(SourceLocation $target): int
* Note that this can be tricked by a `#{` that appears within a single-line
* comment before the expression, but since it's only used for error
* reporting that's probably fine.
*
* @param SourceLocation $start
* @return int
*/
private function expandInterpolationSpanLeft(SourceLocation $start): int
private function expandInterpolationSpanLeft(FileLocation $start): int
{
$source = $start->getFile()->getString();
$i = $start->getOffset() - 1;
Expand Down Expand Up @@ -173,7 +171,7 @@ private function expandInterpolationSpanLeft(SourceLocation $start): int
* Given the end of a {@see FileSpan} covering an interpolated expression, returns
* the offset of the interpolation's closing `}`.
*/
private function expandInterpolationSpanRight(SourceLocation $end): int
private function expandInterpolationSpanRight(FileLocation $end): int
{
$source = $end->getFile()->getString();
$i = $end->getOffset();
Expand Down
4 changes: 2 additions & 2 deletions src/Parser/Parser.php
Expand Up @@ -17,9 +17,9 @@
use ScssPhp\ScssPhp\Logger\DeprecationAwareLoggerInterface;
use ScssPhp\ScssPhp\Logger\LoggerInterface;
use ScssPhp\ScssPhp\Logger\QuietLogger;
use ScssPhp\ScssPhp\SourceSpan\FileLocation;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\SourceSpan\LazyFileSpan;
use ScssPhp\ScssPhp\SourceSpan\SourceLocation;
use ScssPhp\ScssPhp\Util;
use ScssPhp\ScssPhp\Util\Character;
use ScssPhp\ScssPhp\Util\ParserUtil;
Expand Down Expand Up @@ -976,7 +976,7 @@ private function adjustExceptionSpan(FileSpan $span): FileSpan
* This helps avoid missing token errors pointing at the next closing bracket
* rather than the line where the problem actually occurred.
*/
private function firstNewlineBefore(SourceLocation $location): SourceLocation
private function firstNewlineBefore(FileLocation $location): FileLocation
{
$text = $location->getFile()->getText(0, $location->getOffset());
$index = $location->getOffset() - 1;
Expand Down
198 changes: 198 additions & 0 deletions src/SassCallable/BuiltInCallable.php
@@ -0,0 +1,198 @@
<?php

/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/

namespace ScssPhp\ScssPhp\SassCallable;

use ScssPhp\ScssPhp\Ast\Sass\ArgumentDeclaration;
use ScssPhp\ScssPhp\Exception\SassFormatException;
use ScssPhp\ScssPhp\Value\SassNull;
use ScssPhp\ScssPhp\Value\Value;

/**
* A callable defined in PHP code.
*
* Unlike user-defined callables, built-in callables support overloads. They
* may declare multiple different callbacks with multiple different sets of
* arguments. When the callable is invoked, the first callback with matching
* arguments is invoked.
*
* @internal
*/
class BuiltInCallable implements SassCallable
{
private readonly string $name;

/**
* @var list<array{ArgumentDeclaration, callable(list<Value>): Value}>
*/
private readonly array $overloads;

private readonly bool $acceptsContent;

/**
* Creates a function with a single $arguments declaration and a single
* $callback.
*
* The argument declaration is parsed from $arguments, which should not
* include parentheses. Throws a {@see SassFormatException} if parsing fails.
*
* If passed, $url is the URL of the module in which the function is
* defined.
*
* @param callable(list<Value>): Value $callback
*
* @throws SassFormatException
*/
public static function function(string $name, string $arguments, callable $callback, ?string $url = null): BuiltInCallable
{
return self::parsed(
$name,
ArgumentDeclaration::parse("@function $name($arguments) {", url: $url),
$callback
);
}

/**
* Creates a mixin with a single $arguments declaration and a single
* $callback.
*
* The argument declaration is parsed from $arguments, which should not
* include parentheses. Throws a {@see SassFormatException} if parsing fails.
*
* If passed, $url is the URL of the module in which the mixin is
* defined.
*
* @param callable(list<Value>): void $callback
*
* @throws SassFormatException
*/
public static function mixin(string $name, string $arguments, callable $callback, ?string $url = null, bool $acceptsContent = false): BuiltInCallable
{
return self::parsed(
$name,
ArgumentDeclaration::parse("@mixin $name($arguments) {", url: $url),
function ($arguments) use ($callback) {
$callback($arguments);

return SassNull::create();
},
$acceptsContent
);
}

/**
* Creates a function with multiple implementations.
*
* Each key/value pair in $overloads defines the argument declaration for
* the overload (which should not include parentheses), and the callback to
* execute if that argument declaration matches. Throws a
* {@see SassFormatException} if parsing fails.
*
* If passed, $url is the URL of the module in which the function is
* defined.
*
* @param array<string, callable(list<Value>): Value> $overloads
*
* @throws SassFormatException
*/
public static function overloadedFunction(string $name, array $overloads, ?string $url = null): BuiltInCallable
{
$processedOverloads = [];

foreach ($overloads as $args => $callback) {
$overloads[] = [
ArgumentDeclaration::parse("@function $name($args) {", url: $url),
$callback
];
}

return new BuiltInCallable($name, $processedOverloads, false);
}

/**
* Creates a callable with a single $arguments declaration and a single $callback.
*
* @param callable(list<Value>): Value $callback
*/
private static function parsed(string $name, ArgumentDeclaration $arguments, callable $callback, bool $acceptsContent = false): BuiltInCallable
{
return new BuiltInCallable($name, [[$arguments, $callback]], $acceptsContent);
}

/**
* @param list<array{ArgumentDeclaration, callable(list<Value>): Value}> $overloads
*/
private function __construct(string $name, array $overloads, bool $acceptsContent)
{
$this->name = $name;
$this->overloads = $overloads;
$this->acceptsContent = $acceptsContent;
}

public function getName(): string
{
return $this->name;
}

public function acceptsContent(): bool
{
return $this->acceptsContent;
}

/**
* Returns the argument declaration and PHP callback for the given
* positional and named arguments.
*
* If no exact match is found, finds the closest approximation. Note that this
* doesn't guarantee that $positional and $names are valid for the returned
* {@see ArgumentDeclaration}.
*
* @param array<string, mixed> $names Only the keys are relevant
*
* @return array{ArgumentDeclaration, callable(list<Value>): Value}
*/
public function callbackFor(int $positional, array $names): array
{
$fuzzyMatch = null;
$minMismatchDistance = null;

foreach ($this->overloads as $overload) {
// Ideally, find an exact match.
if ($overload[0]->matches($positional, $names)) {
return $overload;
}

$mismatchDistance = \count($overload[0]->getArguments()) - $positional;

if ($minMismatchDistance !== null) {
if (abs($mismatchDistance) > $minMismatchDistance) {
continue;
}

// If two overloads have the same mismatch distance, favor the overload
// that has more arguments.
if (abs($mismatchDistance) === abs($minMismatchDistance) && $mismatchDistance < 0) {
continue;
}
}

$minMismatchDistance = $mismatchDistance;
$fuzzyMatch = $overload;
}

if ($fuzzyMatch !== null) {
return $fuzzyMatch;
}

throw new \LogicException("BuiltInCallable {$this->name} may not have empty overloads.");
}
}

0 comments on commit 030845a

Please sign in to comment.