Deny light-mode definitions in selectors

This commit is contained in:
Eric Lippmann 2022-02-08 19:59:26 +01:00
parent 25acc9602e
commit 0ce968bfda
5 changed files with 128 additions and 40 deletions

View File

@ -2,48 +2,50 @@
namespace Icinga\Less;
use ArrayIterator;
use InvalidArgumentException;
use IteratorAggregate;
use Less_Environment;
/**
* Registry for light modes and the environments in which they are defined
*/
class LightMode
class LightMode implements IteratorAggregate
{
/** @var array Mode environments as mode-environment pairs */
protected $envs = [];
/** @var array Assoc list of modes */
protected $modes = [];
public function add($mode, $module = null)
/** @var array Mode selectors as mode-selector pairs */
protected $selectors = [];
/**
* @param string $mode
*
* @return $this
*
* @throws InvalidArgumentException If the mode already exists
*/
public function add($mode)
{
if (array_key_exists($mode, $this->modes)) {
throw new InvalidArgumentException("$mode already exists");
}
$this->modes[$mode] = $module ?: null;
$this->modes[$mode] = true;
return $this;
}
public function isModule($mode)
{
return isset($this->modes[$mode]);
}
public function list()
{
$modes = [];
foreach ($this->modes as $mode => $module) {
$modes[$module][] = $mode;
}
$byModule = $modes;
unset($byModule[null]);
return [isset($modes[null]) ? $modes[null] : [], $byModule];
}
/**
* @param string $mode
*
* @return Less_Environment
*
* @throws InvalidArgumentException If there is no environment for the given mode
*/
public function getEnv($mode)
{
if (! isset($this->envs[$mode])) {
@ -53,6 +55,14 @@ class LightMode
return $this->envs[$mode];
}
/**
* @param string $mode
* @param Less_Environment $env
*
* @return $this
*
* @throws InvalidArgumentException If an environment for given the mode already exists
*/
public function setEnv($mode, Less_Environment $env)
{
if (array_key_exists($mode, $this->envs)) {
@ -63,4 +73,54 @@ class LightMode
return $this;
}
/**
* @param string $mode
*
* @return bool
*/
public function hasSelector($mode)
{
return isset($this->selectors[$mode]);
}
/**
* @param string $mode
*
* @return string
*
* @throws InvalidArgumentException If there is no selector for the given mode
*/
public function getSelector($mode)
{
if (! isset($this->selectors[$mode])) {
throw new InvalidArgumentException("$mode does not exist");
}
return $this->selectors[$mode];
}
/**
* @param string $mode
* @param string $selector
*
* @return $this
*
* @throws InvalidArgumentException If a selector for given the mode already exists
*/
public function setSelector($mode, $selector)
{
if (array_key_exists($mode, $this->selectors)) {
throw new InvalidArgumentException("$mode already exists");
}
$this->selectors[$mode] = $selector;
return $this;
}
public function getIterator()
{
return new ArrayIterator(array_keys($this->modes));
}
}

View File

@ -3,7 +3,9 @@
namespace Icinga\Less;
use Less_Environment;
use Less_Exception_Compiler;
use Less_Tree_DetachedRuleset;
use Less_Tree_Ruleset;
/**
* Register the environment in which the light mode is defined
@ -54,6 +56,17 @@ class LightModeDefinition extends Less_Tree_DetachedRuleset
{
$drs = parent::compile($env);
/** @var $frame Less_Tree_Ruleset */
foreach ($env->frames as $frame) {
if ($frame->variable($this->getName())) {
if (! empty($frame->first_oelements) && ! isset($frame->first_oelements['.icinga-module'])) {
throw new Less_Exception_Compiler('Light mode definition not allowed in selectors');
}
break;
}
}
$this->getLightMode()->setEnv($this->getName(), $env->copyEvalEnv($env->frames));
return $drs;

View File

@ -15,11 +15,7 @@ class LightModeVisitor extends Less_VisitorReplacing
public function visitRulesetCall($c)
{
if ($this->getLightMode()->isModule($c->variable)) {
return LightModeCall::fromRulesetCall($c)->setLightMode($this->getLightMode());
}
return $c;
return LightModeCall::fromRulesetCall($c)->setLightMode($this->getLightMode());
}
public function run($node)

View File

@ -84,13 +84,15 @@ CSS;
if ($this->variableOrigin->name === '@' . static::LIGHT_MODE_NAME) {
$this->variableOrigin->name .= '-' . substr(sha1(uniqid(mt_rand(), true)), 0, 7);
$this->lightMode->add($this->variableOrigin->name, $this->moduleSelector);
$this->lightMode->add($this->variableOrigin->name);
if ($this->moduleSelector !== false) {
$drs = LightModeDefinition::fromDetachedRuleset($drs)
->setLightMode($this->lightMode)
->setName($this->variableOrigin->name);
$this->lightMode->setSelector($this->variableOrigin->name, $this->moduleSelector);
}
$drs = LightModeDefinition::fromDetachedRuleset($drs)
->setLightMode($this->lightMode)
->setName($this->variableOrigin->name);
}
// Since a detached ruleset is a variable definition in the first place,
@ -190,14 +192,14 @@ CSS;
$evald = $this->visitObj($node);
// The visitor has registered all light modes in visitDetachedRuleset, but has not called them yet.
// Now the light mode calls are prepared with the appropriate module CSS selector.
// Now the light mode calls are prepared with the appropriate CSS selectors.
$calls = [];
list($modes, $moduleModes) = $this->lightMode->list();
if (! empty($modes)) {
$calls[] = implode("();\n", $modes) . '();';
}
foreach ($moduleModes as $module => $modes) {
$calls[] = "$module {\n" . implode("();\n", $modes) . "();\n}";
foreach ($this->lightMode as $mode) {
if ($this->lightMode->hasSelector($mode)) {
$calls[] = "{$this->lightMode->getSelector($mode)} {\n$mode();\n}";
} else {
$calls[] = "$mode();";
}
}
if (! empty($calls)) {

View File

@ -5,6 +5,7 @@ namespace Tests\Icinga\Util;
use Icinga\Test\BaseTestCase;
use Icinga\Util\LessParser;
use Less_Exception_Compiler;
class LessParserTest extends BaseTestCase
{
@ -459,7 +460,7 @@ LESS
:root {
--my-other-color: green;
}
.wrapper {
.icinga-module.module-test {
--greenish-color: lime;
--blueish-color: navy;
}
@ -483,7 +484,7 @@ CSS
}
};
.wrapper {
.icinga-module.module-test {
@light-mode: {
@more-light-colors();
};
@ -497,4 +498,20 @@ LESS
)
);
}
public function testLightModeDefinitionRestrictedInSelectors()
{
$this->expectException(Less_Exception_Compiler::class);
$this->compileLess(<<<LESS
.selector {
@light-mode: {
:root {
--my-color: orange;
}
};
}
LESS
);
}
}