From 5247ff3da07844c3e4977ad435ba2ace9379a993 Mon Sep 17 00:00:00 2001 From: David Woloszyn Date: Mon, 19 Sep 2022 10:50:02 +1000 Subject: [PATCH] MDL-75476 scssphp: Updated from 1.10.0 to 1.11.0 --- lib/scssphp/Compiler.php | 221 +++++++++++++------ lib/scssphp/Exception/ParserException.php | 10 +- lib/scssphp/Formatter.php | 15 +- lib/scssphp/Formatter/Expanded.php | 4 +- lib/scssphp/Formatter/Nested.php | 4 +- lib/scssphp/Formatter/OutputBlock.php | 2 +- lib/scssphp/Logger/QuietLogger.php | 2 + lib/scssphp/Logger/StreamLogger.php | 2 + lib/scssphp/Node/Number.php | 32 ++- lib/scssphp/Parser.php | 117 ++++++---- lib/scssphp/SourceMap/SourceMapGenerator.php | 17 +- lib/scssphp/Version.php | 2 +- lib/thirdpartylibs.xml | 2 +- 13 files changed, 301 insertions(+), 129 deletions(-) diff --git a/lib/scssphp/Compiler.php b/lib/scssphp/Compiler.php index b6ef02727a8..ecafc8cb953 100644 --- a/lib/scssphp/Compiler.php +++ b/lib/scssphp/Compiler.php @@ -166,7 +166,7 @@ class Compiler /** * @var array - * @phpstan-var array + * @phpstan-var array */ protected $userFunctions = []; /** @@ -211,9 +211,15 @@ class Compiler private $charset = true; /** - * @var string|\ScssPhp\ScssPhp\Formatter + * @var Formatter */ - protected $formatter = Expanded::class; + protected $formatter; + + /** + * @var string + * @phpstan-var class-string + */ + private $configuredFormatter = Expanded::class; /** * @var Environment @@ -381,7 +387,7 @@ class Compiler 'encoding' => $this->encoding, 'sourceMap' => serialize($this->sourceMap), 'sourceMapOptions' => $this->sourceMapOptions, - 'formatter' => $this->formatter, + 'formatter' => $this->configuredFormatter, 'legacyImportPath' => $this->legacyCwdImportPath, ]; @@ -502,7 +508,7 @@ class Compiler $tree = $this->parser->parse($source); $this->parser = null; - $this->formatter = new $this->formatter(); + $this->formatter = new $this->configuredFormatter(); $this->rootBlock = null; $this->rootEnv = $this->pushEnv($tree); @@ -529,6 +535,7 @@ class Compiler $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); } } + assert($this->scope !== null); $out = $this->formatter->format($this->scope, $sourceMapGenerator); @@ -542,6 +549,7 @@ class Compiler $sourceMap = null; if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) { + assert($sourceMapGenerator !== null); $sourceMap = $sourceMapGenerator->generateJson($prefix); $sourceMapUrl = null; @@ -671,7 +679,7 @@ class Compiler /** * Push extends * - * @param array $target + * @param string[] $target * @param array $origin * @param array|null $block * @@ -714,9 +722,9 @@ class Compiler $out->sourceLine = $this->env->block->sourceLine; $out->sourceColumn = $this->env->block->sourceColumn; } else { - $out->sourceName = null; - $out->sourceLine = null; - $out->sourceColumn = null; + $out->sourceName = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : '(stdin)'; + $out->sourceLine = $this->sourceLine; + $out->sourceColumn = $this->sourceColumn; } return $out; @@ -734,6 +742,7 @@ class Compiler $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT); $this->compileChildrenNoReturn($rootBlock->children, $this->scope); + assert($this->scope !== null); $this->flattenSelectors($this->scope); $this->missingSelectors(); } @@ -815,6 +824,7 @@ class Compiler } if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) { + assert($block->parent !== null); unset($block->parent->children[$parentKey]); return; @@ -1287,6 +1297,7 @@ class Compiler $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env)); if (! empty($mediaQueries)) { + assert($this->scope !== null); $previousScope = $this->scope; $parentScope = $this->mediaParent($this->scope); @@ -1452,7 +1463,7 @@ class Compiler if ( ! $selfParent->selectors && - isset($block->parent) && $block->parent && + isset($block->parent) && isset($block->parent->selectors) && $block->parent->selectors ) { $selfParent = $block->parent; @@ -1460,13 +1471,15 @@ class Compiler $this->env = $this->filterWithWithout($envs, $with, $without); + assert($this->scope !== null); $saveScope = $this->scope; $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without); // propagate selfParent to the children where they still can be useful $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent); - $this->scope = $this->completeScope($this->scope, $saveScope); + assert($this->scope !== null); + $this->completeScope($this->scope, $saveScope); $this->scope = $saveScope; $this->env = $this->extractEnv($envs); @@ -1490,6 +1503,7 @@ class Compiler if ($scope->type === Type::T_ROOT) { return $scope; } + assert($this->rootBlock !== null); // start from the root while ($scope->parent && $scope->parent->type !== Type::T_ROOT) { @@ -1557,7 +1571,7 @@ class Compiler */ protected function completeScope($scope, $previousScope) { - if (! $scope->type && (! $scope->selectors || ! \count($scope->selectors)) && \count($scope->lines)) { + if (! $scope->type && ! $scope->selectors && \count($scope->lines)) { $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth); } @@ -1768,10 +1782,12 @@ class Compiler $this->scope = $this->makeOutputBlock($block->type, $selectors); $this->scope->depth = 1; + assert($this->scope->parent !== null); $this->scope->parent->children[] = $this->scope; $this->compileChildrenNoReturn($block->children, $this->scope); + assert($this->scope !== null); $this->scope = $this->scope->parent; $this->env = $this->extractEnv($envs); @@ -1829,6 +1845,7 @@ class Compiler $this->pushEnv($block); $this->scope = $this->makeOutputBlock($block->type, $selectors); + assert($this->scope->parent !== null); $this->scope->parent->children[] = $this->scope; // wrap assign children in a block @@ -1862,6 +1879,7 @@ class Compiler $this->compileChildrenNoReturn($block->children, $this->scope); + assert($this->scope !== null); $this->scope = $this->scope->parent; $this->popEnv(); @@ -1890,10 +1908,12 @@ class Compiler protected function compileBlock(Block $block) { $env = $this->pushEnv($block); + assert($block->selectors !== null); $env->selectors = $this->evalSelectors($block->selectors); $out = $this->makeOutputBlock(null); + assert($this->scope !== null); $this->scope->children[] = $out; if (\count($block->children)) { @@ -1911,6 +1931,7 @@ class Compiler // and revert for the following children of the same block if ($selfParentSelectors) { + assert($block->selfParent !== null); $block->selfParent->selectors = $selfParentSelectors; } } @@ -1966,6 +1987,7 @@ class Compiler $out = $this->makeOutputBlock(Type::T_COMMENT); $out->lines[] = $this->compileCommentValue($block, true); + assert($this->scope !== null); $this->scope->children[] = $out; } @@ -1980,7 +2002,11 @@ class Compiler { $this->shouldEvaluate = false; - $selectors = array_map([$this, 'evalSelector'], $selectors); + $evaluatedSelectors = []; + foreach ($selectors as $selector) { + $evaluatedSelectors[] = $this->evalSelector($selector); + } + $selectors = $evaluatedSelectors; // after evaluating interpolates, we might need a second pass if ($this->shouldEvaluate) { @@ -2008,6 +2034,8 @@ class Compiler * @param array $selector * * @return array + * + * @phpstan-impure */ protected function evalSelector($selector) { @@ -2020,6 +2048,8 @@ class Compiler * @param array $part * * @return array + * + * @phpstan-impure */ protected function evalSelectorPart($part) { @@ -2033,8 +2063,8 @@ class Compiler } } elseif ( \is_string($p) && \strlen($p) >= 2 && - ($first = $p[0]) && ($first === '"' || $first === "'") && - substr($p, -1) === $first + ($p[0] === '"' || $p[0] === "'") && + substr($p, -1) === $p[0] ) { $p = substr($p, 1, -1); } @@ -2320,9 +2350,9 @@ class Compiler } /** - * Compile children and throw exception if unexpected `@return` + * Compile children and throw exception if unexpected at-return * - * @param array $stms + * @param array[] $stms * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * @param \ScssPhp\ScssPhp\Block $selfParent * @param string $traceName @@ -2337,13 +2367,13 @@ class Compiler foreach ($stms as $stm) { if ($selfParent && isset($stm[1]) && \is_object($stm[1]) && $stm[1] instanceof Block) { + $oldSelfParent = $stm[1]->selfParent; $stm[1]->selfParent = $selfParent; $ret = $this->compileChild($stm, $out); - $stm[1]->selfParent = null; + $stm[1]->selfParent = $oldSelfParent; } elseif ($selfParent && \in_array($stm[0], [Type::T_INCLUDE, Type::T_EXTEND])) { $stm['selfParent'] = $selfParent; $ret = $this->compileChild($stm, $out); - unset($stm['selfParent']); } else { $ret = $this->compileChild($stm, $out); } @@ -2401,7 +2431,7 @@ class Compiler $queryString = $this->compileMediaQuery([$queryList[$kql]]); $queryString = reset($queryString); - if (strpos($queryString, '@media ') === 0) { + if ($queryString !== false && strpos($queryString, '@media ') === 0) { $queryString = substr($queryString, 7); $queries = []; @@ -2857,7 +2887,7 @@ class Compiler $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null; $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1; $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1; - } elseif (\is_array($child) && isset($child[1]->sourceLine)) { + } elseif (\is_array($child) && isset($child[1]->sourceLine) && $child[1] instanceof Block) { $this->sourceIndex = $child[1]->sourceIndex; $this->sourceLine = $child[1]->sourceLine; $this->sourceColumn = $child[1]->sourceColumn; @@ -3093,6 +3123,7 @@ class Compiler if (! $selectors && isset($child['selfParent'])) { $selectors = $this->multiplySelectors($this->env, $child['selfParent']); } + assert($selectors !== null); if (\count($result) > 1) { $replacement = implode(', ', $result); @@ -3255,7 +3286,7 @@ EOL; // and assign this fake parent to childs $selfParent = null; - if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) { + if (isset($child['selfParent']) && $child['selfParent'] instanceof Block && isset($child['selfParent']->selectors)) { $selfParent = $child['selfParent']; } else { $parentSelectors = $this->multiplySelectors($this->env); @@ -3265,7 +3296,7 @@ EOL; $parent->selectors = $parentSelectors; foreach ($mixin->children as $k => $child) { - if (isset($child[1]) && \is_object($child[1]) && $child[1] instanceof Block) { + if (isset($child[1]) && $child[1] instanceof Block) { $mixin->children[$k][1]->parent = $parent; } } @@ -3371,6 +3402,8 @@ EOL; default: throw $this->error("unknown child type: $child[0]"); } + + return null; } /** @@ -3505,32 +3538,24 @@ EOL; $ucLType = ucfirst($ltype); $ucRType = ucfirst($rtype); + $shouldEval = $inParens || $inExp; + // this tries: // 1. op[op name][left type][right type] - // 2. op[left type][right type] (passing the op as first arg + // 2. op[left type][right type] (passing the op as first arg) // 3. op[op name] - $fn = "op${ucOpName}${ucLType}${ucRType}"; + if (\is_callable([$this, $fn = "op{$ucOpName}{$ucLType}{$ucRType}"])) { + $out = $this->$fn($left, $right, $shouldEval); + } elseif (\is_callable([$this, $fn = "op{$ucLType}{$ucRType}"])) { + $out = $this->$fn($op, $left, $right, $shouldEval); + } elseif (\is_callable([$this, $fn = "op{$ucOpName}"])) { + $out = $this->$fn($left, $right, $shouldEval); + } else { + $out = null; + } - if ( - \is_callable([$this, $fn]) || - (($fn = "op${ucLType}${ucRType}") && - \is_callable([$this, $fn]) && - $passOp = true) || - (($fn = "op${ucOpName}") && - \is_callable([$this, $fn]) && - $genOp = true) - ) { - $shouldEval = $inParens || $inExp; - - if (isset($passOp)) { - $out = $this->$fn($op, $left, $right, $shouldEval); - } else { - $out = $this->$fn($left, $right, $shouldEval); - } - - if (isset($out)) { - return $out; - } + if (isset($out)) { + return $out; } return $this->expToString($value); @@ -3867,11 +3892,12 @@ EOL; // Special functions overriding a CSS function are case-insensitive. We normalize them as lowercase // to avoid the deprecation warning about the wrong case being used. - if ($lowercasedName === 'min' || $lowercasedName === 'max') { + if ($lowercasedName === 'min' || $lowercasedName === 'max' || $lowercasedName === 'rgb' || $lowercasedName === 'rgba' || $lowercasedName === 'hsl' || $lowercasedName === 'hsla') { $normalizedName = $lowercasedName; } if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) { + /** @var string $libName */ $libName = $f[1]; $prototype = isset(static::$$libName) ? static::$$libName : null; @@ -5517,11 +5543,11 @@ EOL; { switch ($style) { case OutputStyle::EXPANDED: - $this->formatter = Expanded::class; + $this->configuredFormatter = Expanded::class; break; case OutputStyle::COMPRESSED: - $this->formatter = Compressed::class; + $this->configuredFormatter = Compressed::class; break; default: @@ -5539,6 +5565,8 @@ EOL; * @return void * * @deprecated Use {@see setOutputStyle} instead. + * + * @phpstan-param class-string $formatterName */ public function setFormatter($formatterName) { @@ -5547,7 +5575,7 @@ EOL; } @trigger_error('The method "setFormatter" is deprecated. Use "setOutputStyle" instead.', E_USER_DEPRECATED); - $this->formatter = $formatterName; + $this->configuredFormatter = $formatterName; } /** @@ -5688,6 +5716,10 @@ EOL; // see if tree is cached $realPath = realpath($path); + if ($realPath === false) { + $realPath = $path; + } + if (substr($path, -5) === '.sass') { $this->sourceIndex = \count($this->sourceNames); $this->sourceNames[] = $path; @@ -6031,6 +6063,8 @@ EOL; * * @param string $msg Message with optional sprintf()-style vararg parameters * + * @return never + * * @throws \ScssPhp\ScssPhp\Exception\CompilerException * * @deprecated use "error" and throw the exception in the caller instead. @@ -6050,7 +6084,8 @@ EOL; * * @internal * - * @param string $msg Message with optional sprintf()-style vararg parameters + * @param string $msg Message with optional sprintf()-style vararg parameters + * @param bool|float|int|string|null ...$args * * @return CompilerException */ @@ -6168,6 +6203,8 @@ EOL; * * @param string $name * + * @return void + * * @throws \Exception */ protected function handleImportLoop($name) @@ -6362,6 +6399,9 @@ EOL; if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) { // notation 100 127 255 / 0 is in fact a simple list of 4 values foreach ($args as $k => $arg) { + if (!isset($arg[1])) { + continue; // This happens when using a trailing comma + } if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) { $args[$k][1][2] = $this->extractSlashAlphaInColorFunction($arg[1][2]); } @@ -6794,7 +6834,7 @@ EOL; $prototype = ['arguments' => [], 'rest_argument' => null]; $originalRestArgumentName = null; - foreach ($argDef as $i => $arg) { + foreach ($argDef as $arg) { list($name, $default, $isVariable) = $arg; $normalizedName = str_replace('_', '-', $name); @@ -7262,6 +7302,8 @@ EOL; protected function coerceString($value) { if ($value[0] === Type::T_STRING) { + assert(\is_array($value)); + return $value; } @@ -7365,6 +7407,7 @@ EOL; if ($value[0] !== Type::T_LIST) { throw $this->error('expecting list, %s received', $value[0]); } + assert(\is_array($value)); return $value; } @@ -7534,7 +7577,7 @@ EOL; $h = 60 * ($green - $blue) / $d; } elseif ($green == $max) { $h = 60 * ($blue - $red) / $d + 120; - } elseif ($blue == $max) { + } else { $h = 60 * ($red - $green) / $d + 240; } } @@ -7613,9 +7656,9 @@ EOL; * * @api * - * @param int $hue H from 0 to 360 - * @param int $whiteness W from 0 to 100 - * @param int $blackness B from 0 to 100 + * @param int|float $hue H from 0 to 360 + * @param int|float $whiteness W from 0 to 100 + * @param int|float $blackness B from 0 to 100 * * @return array */ @@ -7666,7 +7709,7 @@ EOL; $h = 60 * ($green - $blue) / $d; } elseif ($green == $max) { $h = 60 * ($blue - $red) / $d + 120; - } elseif ($blue == $max) { + } else { $h = 60 * ($red - $green) / $d + 240; } } @@ -7790,6 +7833,14 @@ EOL; ['channels'], ['red', 'green', 'blue'], ['red', 'green', 'blue', 'alpha'] ]; + + /** + * @param array $args + * @param array $kwargs + * @param string $funcName + * + * @return array + */ protected function libRgb($args, $kwargs, $funcName = 'rgb') { switch (\count($args)) { @@ -7872,14 +7923,7 @@ EOL; $scale = $operation === 'scale'; $change = $operation === 'change'; - /** - * @param string $name - * @param float|int $max - * @param bool $checkPercent - * @param bool $assertPercent - * - * @return float|int|null - */ + /** @phpstan-var callable(string, float|int, bool=, bool=): (float|int|null) $getParam */ $getParam = function ($name, $max, $checkPercent = false, $assertPercent = false) use (&$kwargs, $scale, $change) { if (!isset($kwargs[$name])) { return null; @@ -7903,7 +7947,11 @@ EOL; $max = 100; } - return $number->valueInRange($change ? 0 : -$max, $max, $name); + if ($scale || $assertPercent) { + return $number->valueInRange($change ? 0 : -$max, $max, $name); + } + + return $number->valueInRangeWithUnit($change ? 0 : -$max, $max, $name, $checkPercent ? '%' : ''); }; $alpha = $getParam('alpha', 1); @@ -7938,7 +7986,6 @@ EOL; $hasRgb = $red !== null || $green !== null || $blue !== null; $hasSL = $saturation !== null || $lightness !== null; $hasWB = $whiteness !== null || $blackness !== null; - $found = false; if ($hasRgb && ($hasSL || $hasWB || $hue !== null)) { throw new SassScriptException(sprintf('RGB parameters may not be passed along with %s parameters.', $hasWB ? 'HWB' : 'HSL')); @@ -8159,6 +8206,14 @@ EOL; ['hue', 'saturation'], ['hue', 'saturation', 'lightness'], ['hue', 'saturation', 'lightness', 'alpha'] ]; + + /** + * @param array $args + * @param array $kwargs + * @param string $funcName + * + * @return array|null + */ protected function libHsl($args, $kwargs, $funcName = 'hsl') { $args_to_check = $args; @@ -8183,7 +8238,7 @@ EOL; throw new SassScriptException('Missing argument $lightness.'); } - foreach ($kwargs as $k => $arg) { + foreach ($kwargs as $arg) { if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) { return null; } @@ -8531,7 +8586,7 @@ EOL; $color = $this->assertColor($args[0], 'color'); $amount = $this->assertNumber($args[1], 'amount'); - $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount->valueInRange(0, 1, 'amount'); + $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount->valueInRangeWithUnit(0, 1, 'amount', ''); $color[4] = min(1, max(0, $color[4])); return $color; @@ -8550,7 +8605,7 @@ EOL; $color = $this->assertColor($args[0], 'color'); $amount = $this->assertNumber($args[1], 'amount'); - $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount->valueInRange(0, 1, 'amount'); + $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount->valueInRangeWithUnit(0, 1, 'amount', ''); $color[4] = min(1, max(0, $color[4])); return $color; @@ -8718,7 +8773,7 @@ will be an error in future versions of Sass.\n on line $line of $fname"; protected function libNth($args) { $list = $this->coerceList($args[0], ',', false); - $n = $this->assertNumber($args[1])->getDimension(); + $n = $this->assertInteger($args[1]); if ($n > 0) { $n--; @@ -8733,7 +8788,7 @@ will be an error in future versions of Sass.\n on line $line of $fname"; protected function libSetNth($args) { $list = $this->coerceList($args[0]); - $n = $this->assertNumber($args[1])->getDimension(); + $n = $this->assertInteger($args[1]); if ($n > 0) { $n--; @@ -9350,7 +9405,7 @@ will be an error in future versions of Sass.\n on line $line of $fname"; $index = $index - 1; } if ($index < 0) { - $index = Util::mbStrlen($stringContent) + 1 + $index; + $index = max(Util::mbStrlen($stringContent) + 1 + $index, 0); } $string[2] = [ @@ -9538,7 +9593,25 @@ will be an error in future versions of Sass.\n on line $line of $fname"; protected function libRandom($args) { if (isset($args[0]) && $args[0] !== static::$null) { - $n = $this->assertInteger($args[0], 'limit'); + $limit = $this->assertNumber($args[0], 'limit'); + + if ($limit->hasUnits()) { + $unitString = $limit->unitStr(); + $message = <<addLocationToMessage($message)); + } + + $n = $this->assertInteger($limit, 'limit'); if ($n < 1) { throw new SassScriptException("\$limit: Must be greater than 0, was $n."); @@ -10003,6 +10076,8 @@ will be an error in future versions of Sass.\n on line $line of $fname"; $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true); } + assert(!empty($selectorsMap)); + $envs = []; foreach ($selectorsMap as $selectors) { diff --git a/lib/scssphp/Exception/ParserException.php b/lib/scssphp/Exception/ParserException.php index 00d77ec98ed..f0726698fe7 100644 --- a/lib/scssphp/Exception/ParserException.php +++ b/lib/scssphp/Exception/ParserException.php @@ -22,7 +22,8 @@ namespace ScssPhp\ScssPhp\Exception; class ParserException extends \Exception implements SassException { /** - * @var array + * @var array|null + * @phpstan-var array{string, int, int}|null */ private $sourcePosition; @@ -30,6 +31,9 @@ class ParserException extends \Exception implements SassException * Get source position * * @api + * + * @return array|null + * @phpstan-return array{string, int, int}|null */ public function getSourcePosition() { @@ -42,6 +46,10 @@ class ParserException extends \Exception implements SassException * @api * * @param array $sourcePosition + * + * @return void + * + * @phpstan-param array{string, int, int} $sourcePosition */ public function setSourcePosition($sourcePosition) { diff --git a/lib/scssphp/Formatter.php b/lib/scssphp/Formatter.php index c88ddba9db7..6137dc65071 100644 --- a/lib/scssphp/Formatter.php +++ b/lib/scssphp/Formatter.php @@ -286,9 +286,18 @@ abstract class Formatter ob_start(); - $this->block($block); + try { + $this->block($block); + } catch (\Exception $e) { + ob_end_clean(); + throw $e; + } catch (\Throwable $e) { + ob_end_clean(); + throw $e; + } $out = ob_get_clean(); + assert($out !== false); return $out; } @@ -331,6 +340,8 @@ abstract class Formatter // If the written line starts is empty, adding a mapping would add it for // a non-existent column as we are at the end of the line if ($line !== '') { + assert($this->currentBlock->sourceLine !== null); + assert($this->currentBlock->sourceName !== null); $this->sourceMapGenerator->addMapping( $this->currentLine, $this->currentColumn, @@ -346,6 +357,8 @@ abstract class Formatter } if ($lastLine !== '') { + assert($this->currentBlock->sourceLine !== null); + assert($this->currentBlock->sourceName !== null); $this->sourceMapGenerator->addMapping( $this->currentLine, $this->currentColumn, diff --git a/lib/scssphp/Formatter/Expanded.php b/lib/scssphp/Formatter/Expanded.php index a280416de21..6eb4a0cb78c 100644 --- a/lib/scssphp/Formatter/Expanded.php +++ b/lib/scssphp/Formatter/Expanded.php @@ -57,7 +57,9 @@ class Expanded extends Formatter foreach ($block->lines as $index => $line) { if (substr($line, 0, 2) === '/*') { - $block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line); + $replacedLine = preg_replace('/\r\n?|\n|\f/', $this->break, $line); + assert($replacedLine !== null); + $block->lines[$index] = $replacedLine; } } diff --git a/lib/scssphp/Formatter/Nested.php b/lib/scssphp/Formatter/Nested.php index c11ea8adfde..d5ed85cc261 100644 --- a/lib/scssphp/Formatter/Nested.php +++ b/lib/scssphp/Formatter/Nested.php @@ -68,7 +68,9 @@ class Nested extends Formatter foreach ($block->lines as $index => $line) { if (substr($line, 0, 2) === '/*') { - $block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line); + $replacedLine = preg_replace('/\r\n?|\n|\f/', $this->break, $line); + assert($replacedLine !== null); + $block->lines[$index] = $replacedLine; } } diff --git a/lib/scssphp/Formatter/OutputBlock.php b/lib/scssphp/Formatter/OutputBlock.php index 5cc91a080ab..2799656a435 100644 --- a/lib/scssphp/Formatter/OutputBlock.php +++ b/lib/scssphp/Formatter/OutputBlock.php @@ -22,7 +22,7 @@ namespace ScssPhp\ScssPhp\Formatter; class OutputBlock { /** - * @var string + * @var string|null */ public $type; diff --git a/lib/scssphp/Logger/QuietLogger.php b/lib/scssphp/Logger/QuietLogger.php index 0f358c64672..ad7c07537fc 100644 --- a/lib/scssphp/Logger/QuietLogger.php +++ b/lib/scssphp/Logger/QuietLogger.php @@ -14,6 +14,8 @@ namespace ScssPhp\ScssPhp\Logger; /** * A logger that silently ignores all messages. + * + * @final */ class QuietLogger implements LoggerInterface { diff --git a/lib/scssphp/Logger/StreamLogger.php b/lib/scssphp/Logger/StreamLogger.php index f5da6c9cf64..7db7cc18929 100644 --- a/lib/scssphp/Logger/StreamLogger.php +++ b/lib/scssphp/Logger/StreamLogger.php @@ -14,6 +14,8 @@ namespace ScssPhp\ScssPhp\Logger; /** * A logger that prints to a PHP stream (for instance stderr) + * + * @final */ class StreamLogger implements LoggerInterface { diff --git a/lib/scssphp/Node/Number.php b/lib/scssphp/Node/Number.php index ca9b5b652d4..48e711f0855 100644 --- a/lib/scssphp/Node/Number.php +++ b/lib/scssphp/Node/Number.php @@ -227,6 +227,16 @@ class Number extends Node implements \ArrayAccess return \count($this->numeratorUnits) === 0 && \count($this->denominatorUnits) === 0; } + /** + * Returns true if the number has any units + * + * @return bool + */ + public function hasUnits() + { + return !$this->unitless(); + } + /** * Checks whether the number has exactly this unit * @@ -266,7 +276,27 @@ class Number extends Node implements \ArrayAccess try { return Util::checkRange('', new Range($min, $max), $this); } catch (RangeException $e) { - throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s', $this, $min, $this->unitStr(), $max), $name); + throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $this->unitStr(), $max), $name); + } + } + + /** + * @param float|int $min + * @param float|int $max + * @param string $name + * @param string $unit + * + * @return float|int + * @throws SassScriptException + * + * @internal + */ + public function valueInRangeWithUnit($min, $max, $name, $unit) + { + try { + return Util::checkRange('', new Range($min, $max), $this); + } catch (RangeException $e) { + throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $unit, $max), $name); } } diff --git a/lib/scssphp/Parser.php b/lib/scssphp/Parser.php index 36e7ac03615..1c76e7c6b04 100644 --- a/lib/scssphp/Parser.php +++ b/lib/scssphp/Parser.php @@ -27,6 +27,7 @@ use ScssPhp\ScssPhp\Block\WhileBlock; use ScssPhp\ScssPhp\Exception\ParserException; use ScssPhp\ScssPhp\Logger\LoggerInterface; use ScssPhp\ScssPhp\Logger\QuietLogger; +use ScssPhp\ScssPhp\Node\Number; /** * Parser @@ -85,10 +86,6 @@ class Parser * @var array */ private $sourcePositions; - /** - * @var array|null - */ - private $charset; /** * The current offset in the buffer * @@ -147,11 +144,9 @@ class Parser { $this->sourceName = $sourceName ?: '(stdin)'; $this->sourceIndex = $sourceIndex; - $this->charset = null; $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8'; $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais'; $this->commentsSeen = []; - $this->commentsSeen = []; $this->allowVars = true; $this->cssOnly = $cssOnly; $this->logger = $logger ?: new QuietLogger(); @@ -255,7 +250,6 @@ class Parser if ($this->cache) { $cacheKey = $this->sourceName . ':' . md5($buffer); $parseOptions = [ - 'charset' => $this->charset, 'utf8' => $this->utf8, ]; $v = $this->cache->getCache('parse', $cacheKey, $parseOptions); @@ -296,11 +290,8 @@ class Parser throw $this->parseError('unclosed block'); } - if ($this->charset) { - array_unshift($this->env->children, $this->charset); - } - $this->restoreEncoding(); + assert($this->env !== null); if ($this->cache) { $this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions); @@ -380,8 +371,8 @@ class Parser * * @api * - * @param string $buffer - * @param string|array $out + * @param string $buffer + * @param array $out * * @return bool */ @@ -827,18 +818,6 @@ class Parser $this->valueList($charset) && $this->end() ) { - if (! isset($this->charset)) { - $statement = [Type::T_CHARSET, $charset]; - - list($line, $column) = $this->getSourcePosition($s); - - $statement[static::SOURCE_LINE] = $line; - $statement[static::SOURCE_COLUMN] = $column; - $statement[static::SOURCE_INDEX] = $this->sourceIndex; - - $this->charset = $statement; - } - return true; } @@ -1074,6 +1053,8 @@ class Parser $block = $this->popBlock(); if (! isset($block->type) || $block->type !== Type::T_IF) { + assert($this->env !== null); + if ($this->env->parent) { $this->append(null); // collect comments before next statement if needed } @@ -1093,6 +1074,7 @@ class Parser // collect comments just after the block closing if needed if ($this->eatWhiteDefault) { $this->whitespace(); + assert($this->env !== null); if ($this->env->comments) { $this->append(null); @@ -1161,6 +1143,7 @@ class Parser // collect comments at the beginning of a block if needed if ($this->eatWhiteDefault) { $this->whitespace(); + assert($this->env !== null); if ($this->env->comments) { $this->append(null); @@ -1195,6 +1178,7 @@ class Parser */ protected function popBlock() { + assert($this->env !== null); // collect comments ending just before of a block closing if ($this->env->comments) { @@ -1245,6 +1229,8 @@ class Parser * Seek to position in input stream (or return current position in input stream) * * @param int $where + * + * @return void */ protected function seek($where) { @@ -1257,6 +1243,8 @@ class Parser * @param array|false $parsed * @param int $startPos * + * @return array + * * @throws ParserException */ protected function assertPlainCssValid($parsed, $startPos = null) @@ -1288,7 +1276,7 @@ class Parser * @param array $parsed * @param bool $allowExpression * - * @return bool|array + * @return array|false */ protected function isPlainCssValidElement($parsed, $allowExpression = false) { @@ -1468,6 +1456,8 @@ class Parser * @param string $delim Delimiter * * @return bool True if match; false otherwise + * + * @phpstan-impure */ protected function matchString(&$m, $delim) { @@ -1508,6 +1498,8 @@ class Parser * @param bool $eatWhitespace * * @return bool + * + * @phpstan-impure */ protected function match($regex, &$out, $eatWhitespace = null) { @@ -1537,6 +1529,8 @@ class Parser * @param bool $eatWhitespace * * @return bool + * + * @phpstan-impure */ protected function matchChar($char, $eatWhitespace = null) { @@ -1565,6 +1559,8 @@ class Parser * @param bool $eatWhitespace * * @return bool + * + * @phpstan-impure */ protected function literal($what, $len, $eatWhitespace = null) { @@ -1589,6 +1585,8 @@ class Parser * Match some whitespace * * @return bool + * + * @phpstan-impure */ protected function whitespace() { @@ -1640,20 +1638,20 @@ class Parser if (! $comment) { // single part static comment - $this->appendComment([Type::T_COMMENT, $c]); + $commentStatement = [Type::T_COMMENT, $c]; } else { $comment[] = $c; $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount); $commentStatement = [Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]; - - list($line, $column) = $this->getSourcePosition($startCommentCount); - $commentStatement[self::SOURCE_LINE] = $line; - $commentStatement[self::SOURCE_COLUMN] = $column; - $commentStatement[self::SOURCE_INDEX] = $this->sourceIndex; - - $this->appendComment($commentStatement); } + list($line, $column) = $this->getSourcePosition($startCommentCount); + $commentStatement[self::SOURCE_LINE] = $line; + $commentStatement[self::SOURCE_COLUMN] = $column; + $commentStatement[self::SOURCE_INDEX] = $this->sourceIndex; + + $this->appendComment($commentStatement); + $this->commentsSeen[$startCommentCount] = true; $this->count = $endCommentCount; } else { @@ -1675,9 +1673,13 @@ class Parser * Append comment to current block * * @param array $comment + * + * @return void */ protected function appendComment($comment) { + assert($this->env !== null); + if (! $this->discardComments) { $this->env->comments[] = $comment; } @@ -1688,9 +1690,13 @@ class Parser * * @param array|null $statement * @param int $pos + * + * @return void */ protected function append($statement, $pos = null) { + assert($this->env !== null); + if (! \is_null($statement)) { ! $this->cssOnly || ($statement = $this->assertPlainCssValid($statement, $pos)); @@ -1720,11 +1726,15 @@ class Parser */ protected function last() { + assert($this->env !== null); + $i = \count($this->env->children) - 1; if (isset($this->env->children[$i])) { return $this->env->children[$i]; } + + return null; } /** @@ -2069,10 +2079,12 @@ class Parser /** * Parse directive value list that considers $vars as keyword * - * @param array $out - * @param bool|string $endChar + * @param array $out + * @param string|false $endChar * * @return bool + * + * @phpstan-impure */ protected function directiveValue(&$out, $endChar = false) { @@ -2211,6 +2223,7 @@ class Parser { $s = $this->count; $items = []; + /** @var array|Number|null $value */ $value = null; while ($this->$parseItem($value)) { @@ -2224,9 +2237,12 @@ class Parser $trailing_delim = true; } else { + assert(\is_array($value) || $value instanceof Number); // if no delim watch that a keyword didn't eat the single/double quote // from the following starting string if ($value[0] === Type::T_KEYWORD) { + assert(\is_array($value)); + /** @var string $word */ $word = $value[1]; $last_char = substr($word, -1); @@ -2251,8 +2267,10 @@ class Parser $this->count--; } + /** @var array|Number|null $nextValue */ $nextValue = null; if ($this->$parseItem($nextValue)) { + assert(\is_array($nextValue) || $nextValue instanceof Number); if ($nextValue[0] === Type::T_KEYWORD && $nextValue[1] === $last_char) { // bad try, forget it $this->seek($currentCount); @@ -2306,6 +2324,8 @@ class Parser * @param bool $lookForExp * * @return bool + * + * @phpstan-impure */ protected function expression(&$out, $listOnly = false, $lookForExp = true) { @@ -2366,12 +2386,14 @@ class Parser /** * Parse expression specifically checking for lists in parenthesis or brackets * - * @param array $out - * @param int $s - * @param string $closingParen - * @param array $allowedTypes + * @param array $out + * @param int $s + * @param string $closingParen + * @param string[] $allowedTypes * * @return bool + * + * @phpstan-param array $allowedTypes */ protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type::T_LIST, Type::T_MAP]) { @@ -2439,7 +2461,7 @@ class Parser $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]); - while ($this->match($operators, $m, false) && static::$precedence[$m[1]] >= $minP) { + while ($this->match($operators, $m, false) && static::$precedence[strtolower($m[1])] >= $minP) { $whiteAfter = isset($this->buffer[$this->count]) && ctype_space($this->buffer[$this->count]); $varAfter = isset($this->buffer[$this->count]) && @@ -2463,7 +2485,7 @@ class Parser } // consume higher-precedence operators on the right-hand side - $rhs = $this->expHelper($rhs, static::$precedence[$op] + 1); + $rhs = $this->expHelper($rhs, static::$precedence[strtolower($op)] + 1); $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter]; @@ -2782,6 +2804,10 @@ class Parser $this->argValues($args) && $this->matchChar(')') ) { + if (strtolower($name) === 'var' && \count($args) === 2 && $args[1][0] === Type::T_NULL) { + $args[1] = [null, [Type::T_STRING, '', [' ']], false]; + } + $func = [Type::T_FUNCTION_CALL, $name, $args]; return true; @@ -4016,7 +4042,7 @@ class Parser * * @param array $value * - * @return array + * @return string[] */ protected function stripAssignmentFlags(&$value) { @@ -4043,7 +4069,7 @@ class Parser * * @param array $selectors * - * @return string + * @return bool */ protected function stripOptionalFlag(&$selectors) { @@ -4092,6 +4118,8 @@ class Parser * Extract line numbers from buffer * * @param string $buffer + * + * @return void */ private function extractLineNumbers($buffer) { @@ -4116,6 +4144,7 @@ class Parser * @param int $pos * * @return array + * @phpstan-return array{int, int} */ private function getSourcePosition($pos) { diff --git a/lib/scssphp/SourceMap/SourceMapGenerator.php b/lib/scssphp/SourceMap/SourceMapGenerator.php index 65dc24da0d7..ccd4f0261fe 100644 --- a/lib/scssphp/SourceMap/SourceMapGenerator.php +++ b/lib/scssphp/SourceMap/SourceMapGenerator.php @@ -107,7 +107,7 @@ class SourceMapGenerator */ public function __construct(array $options = []) { - $this->options = array_merge($this->defaultOptions, $options); + $this->options = array_replace($this->defaultOptions, $options); $this->encoder = new Base64VLQ(); } @@ -140,7 +140,7 @@ class SourceMapGenerator * * @param string $content The content to write * - * @return string + * @return string|null * * @throws \ScssPhp\ScssPhp\Exception\CompilerException If the file could not be saved * @deprecated @@ -148,6 +148,7 @@ class SourceMapGenerator public function saveMap($content) { $file = $this->options['sourceMapWriteTo']; + assert($file !== null); $dir = \dirname($file); // directory does not exist @@ -201,7 +202,7 @@ class SourceMapGenerator // A list of original sources used by the 'mappings' entry. $sourceMap['sources'] = []; - foreach ($this->sources as $sourceUri => $sourceFilename) { + foreach ($this->sources as $sourceFilename) { $sourceMap['sources'][] = $this->normalizeFilename($sourceFilename); } @@ -223,7 +224,15 @@ class SourceMapGenerator unset($sourceMap['sourceRoot']); } - return json_encode($sourceMap, JSON_UNESCAPED_SLASHES); + $jsonSourceMap = json_encode($sourceMap, JSON_UNESCAPED_SLASHES); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException(json_last_error_msg()); + } + + assert($jsonSourceMap !== false); + + return $jsonSourceMap; } /** diff --git a/lib/scssphp/Version.php b/lib/scssphp/Version.php index f8d728475b4..d604a505764 100644 --- a/lib/scssphp/Version.php +++ b/lib/scssphp/Version.php @@ -19,5 +19,5 @@ namespace ScssPhp\ScssPhp; */ class Version { - const VERSION = '1.10.0'; + const VERSION = '1.11.0'; } diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index 47118aed1ce..af99c17010f 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -366,7 +366,7 @@ All rights reserved. scssphp scssphp scssphp is a compiler for SCSS written in PHP. - 1.10.0 + 1.11.0 MIT https://github.com/scssphp/scssphp/