From 6d74ad6735faa53aa2e16be22b05223dbf9a85e8 Mon Sep 17 00:00:00 2001 From: jurcovicovam Date: Mon, 23 Jun 2014 14:25:30 +0200 Subject: [PATCH] Detached rulesets without scoping. #186 --- .../core/compiler/expressions/GuardValue.java | 8 +- .../expressions/LocalScopeFilter.java | 13 ++ .../less4j/core/compiler/stages/AstLogic.java | 4 + ...Result.java => BodyCompilationResult.java} | 21 ++- .../compiler/stages/DefaultGuardHelper.java | 19 +- .../stages/InitialScopeExtractor.java | 20 ++- .../core/compiler/stages/MixinsSolver.java | 163 ++++++++++++++++-- .../compiler/stages/ReferencesSolver.java | 78 ++++++--- .../less4j/core/problems/ProblemsHandler.java | 11 +- .../sommeri/less4j/utils/LessPrinter.java | 4 + src/test/resources/minitests/debug1.less | 17 +- 11 files changed, 283 insertions(+), 75 deletions(-) create mode 100644 src/main/java/com/github/sommeri/less4j/core/compiler/expressions/LocalScopeFilter.java rename src/main/java/com/github/sommeri/less4j/core/compiler/stages/{MixinCompilationResult.java => BodyCompilationResult.java} (63%) diff --git a/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/GuardValue.java b/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/GuardValue.java index 11b42fba..87edfbad 100644 --- a/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/GuardValue.java +++ b/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/GuardValue.java @@ -1,17 +1,17 @@ package com.github.sommeri.less4j.core.compiler.expressions; -import com.github.sommeri.less4j.core.compiler.stages.MixinCompilationResult; +import com.github.sommeri.less4j.core.compiler.stages.BodyCompilationResult; import com.github.sommeri.less4j.utils.ArraysUtils.Filter; public enum GuardValue { USE, DO_NOT_USE, USE_IF_DEFAULT, USE_IF_NOT_DEFAULT; - public Filter filter() { + public Filter filter() { return new DefaultFunctionUseFilter(this); } } -class DefaultFunctionUseFilter implements Filter { +class DefaultFunctionUseFilter implements Filter { private final GuardValue value; @@ -21,7 +21,7 @@ public DefaultFunctionUseFilter(GuardValue value) { } @Override - public boolean accept(MixinCompilationResult t) { + public boolean accept(BodyCompilationResult t) { return t.getGuardValue().equals(value); } diff --git a/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/LocalScopeFilter.java b/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/LocalScopeFilter.java new file mode 100644 index 00000000..42b7aae4 --- /dev/null +++ b/src/main/java/com/github/sommeri/less4j/core/compiler/expressions/LocalScopeFilter.java @@ -0,0 +1,13 @@ +package com.github.sommeri.less4j.core.compiler.expressions; + +import com.github.sommeri.less4j.core.ast.Expression; +import com.github.sommeri.less4j.core.compiler.scopes.IScope; + +@Deprecated //FIXME: !!!!!!!!!!!!!!!! delete interface +public interface LocalScopeFilter { + + IScope apply(IScope input); + + Expression apply(Expression value); + +} diff --git a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/AstLogic.java b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/AstLogic.java index f1542be6..5554006c 100644 --- a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/AstLogic.java +++ b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/AstLogic.java @@ -76,4 +76,8 @@ public static boolean isQuotelessUrlFunction(ASTCssNode kid) { return "".equals(stringParameter.getQuoteType()); } + public static boolean isExpression(ASTCssNode kid) { + return kid instanceof Expression; + } + } diff --git a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/MixinCompilationResult.java b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/BodyCompilationResult.java similarity index 63% rename from src/main/java/com/github/sommeri/less4j/core/compiler/stages/MixinCompilationResult.java rename to src/main/java/com/github/sommeri/less4j/core/compiler/stages/BodyCompilationResult.java index e068cbcc..70549b97 100644 --- a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/MixinCompilationResult.java +++ b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/BodyCompilationResult.java @@ -3,20 +3,19 @@ import java.util.List; import com.github.sommeri.less4j.core.ast.ASTCssNode; -import com.github.sommeri.less4j.core.ast.ReusableStructure; import com.github.sommeri.less4j.core.compiler.expressions.GuardValue; import com.github.sommeri.less4j.core.compiler.scopes.IScope; import com.github.sommeri.less4j.utils.ArraysUtils; -public class MixinCompilationResult { +public class BodyCompilationResult { - private ReusableStructure mixin; + private ASTCssNode compiledBodyOwner; private List replacement; private IScope returnValues; private GuardValue guardValue; - public MixinCompilationResult(ReusableStructure mixin, List replacement, IScope returnValues) { - this.mixin = mixin; + public BodyCompilationResult(ASTCssNode compiledBodyOwner, List replacement, IScope returnValues) { + this.compiledBodyOwner = compiledBodyOwner; this.replacement = replacement; this.returnValues = returnValues; } @@ -45,17 +44,17 @@ public void setReturnValues(IScope returnValues) { this.returnValues = returnValues; } - public ReusableStructure getMixin() { - return mixin; + public ASTCssNode getCompiledBodyOwner() { + return compiledBodyOwner; } - public void setMixin(ReusableStructure mixin) { - this.mixin = mixin; + public void setCompiledBodyOwner(ASTCssNode compiledBodyOwner) { + this.compiledBodyOwner = compiledBodyOwner; } @Override - protected MixinCompilationResult clone() { - return new MixinCompilationResult(mixin.clone(), ArraysUtils.deeplyClonedList(replacement), returnValues); + protected BodyCompilationResult clone() { + return new BodyCompilationResult(compiledBodyOwner.clone(), ArraysUtils.deeplyClonedList(replacement), returnValues); } } \ No newline at end of file diff --git a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/DefaultGuardHelper.java b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/DefaultGuardHelper.java index 2b0ebf38..90bd32f0 100644 --- a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/DefaultGuardHelper.java +++ b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/DefaultGuardHelper.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Set; +import com.github.sommeri.less4j.core.ast.ASTCssNode; import com.github.sommeri.less4j.core.ast.MixinReference; import com.github.sommeri.less4j.core.ast.ReusableStructure; import com.github.sommeri.less4j.core.compiler.expressions.GuardValue; @@ -21,7 +22,7 @@ public DefaultGuardHelper(ProblemsHandler problemsHandler) { this.problemsHandler = problemsHandler; } - public List chooseMixinsToBeUsed(List compiledMixins, final MixinReference reference) { + public List chooseMixinsToBeUsed(List compiledMixins, final MixinReference reference) { // count how many mixins of each kind we encountered int normalMixinsCnt = ArraysUtils.count(compiledMixins, GuardValue.USE.filter()); int ifNotCnt = ArraysUtils.count(compiledMixins, GuardValue.USE_IF_NOT_DEFAULT.filter()); @@ -38,7 +39,7 @@ public List chooseMixinsToBeUsed(List 1) { - List errorSet = keepOnly(compiledMixins, GuardValue.USE_IF_DEFAULT,GuardValue.USE_IF_NOT_DEFAULT); + List errorSet = keepOnly(compiledMixins, GuardValue.USE_IF_DEFAULT,GuardValue.USE_IF_NOT_DEFAULT); problemsHandler.ambiguousDefaultSet(reference, extractOriginalMixins(errorSet)); //no mixins are going to be used return Collections.emptyList(); @@ -56,11 +57,11 @@ public List chooseMixinsToBeUsed(List keepOnly(List compiledMixins, GuardValue... kind) { + private List keepOnly(List compiledMixins, GuardValue... kind) { Set expectedUses = ArraysUtils.asSet(kind); - Iterator iterator = compiledMixins.iterator(); + Iterator iterator = compiledMixins.iterator(); while (iterator.hasNext()) { - MixinCompilationResult compiled = iterator.next(); + BodyCompilationResult compiled = iterator.next(); if (!expectedUses.contains(compiled.getGuardValue())) { iterator.remove(); } @@ -68,10 +69,10 @@ private List keepOnly(List compi return compiledMixins; } - private List extractOriginalMixins(List compiledMixins) { - List result = new ArrayList(); - for (MixinCompilationResult compiled : compiledMixins) { - result.add(compiled.getMixin()); + private List extractOriginalMixins(List compiledMixins) { + List result = new ArrayList(); + for (BodyCompilationResult compiled : compiledMixins) { + result.add(compiled.getCompiledBodyOwner()); } return result; } diff --git a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/InitialScopeExtractor.java b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/InitialScopeExtractor.java index 52c16368..4f3ff922 100644 --- a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/InitialScopeExtractor.java +++ b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/InitialScopeExtractor.java @@ -6,6 +6,8 @@ import com.github.sommeri.less4j.core.ast.ASTCssNode; import com.github.sommeri.less4j.core.ast.ASTCssNodeType; +import com.github.sommeri.less4j.core.ast.DetachedRuleset; +import com.github.sommeri.less4j.core.ast.Expression; import com.github.sommeri.less4j.core.ast.ReusableStructure; import com.github.sommeri.less4j.core.ast.RuleSet; import com.github.sommeri.less4j.core.ast.VariableDeclaration; @@ -50,7 +52,7 @@ private IScope buildScope(ASTCssNode node) { buildScope(kid); if (kid.getType() == ASTCssNodeType.IMPORT) { - importsPlaceholders.add(createPlaceholderScope(kid)); + importsPlaceholders.add(createPlaceholderScope(kid)); } else if (kid.getType() == ASTCssNodeType.VARIABLE_DECLARATION) { currentScope.registerVariable((VariableDeclaration) kid); manipulator.removeFromBody(kid); @@ -62,6 +64,13 @@ private IScope buildScope(ASTCssNode node) { if (bodyScope.hasParent()) bodyScope.getParent().removedFromAst(); // remove also arguments scope from tree manipulator.removeFromBody(kid); + } else if (kid.getType() == ASTCssNodeType.DETACHED_RULESET) { + DetachedRuleset detached = (DetachedRuleset) kid; + IScope bodyScope = currentScope.childByOwners(detached, detached.getBody()); + if (bodyScope.hasParent()) + bodyScope.getParent().removedFromAst(); // remove also arguments scope from tree + bodyScope.removedFromAst(); + detached.setScope(bodyScope); } else if (kid.getType() == ASTCssNodeType.RULE_SET) { RuleSet ruleSet = (RuleSet) kid; if (ruleSet.isUsableAsReusableStructure()) { @@ -70,6 +79,11 @@ private IScope buildScope(ASTCssNode node) { } } else if (kid.getType() == ASTCssNodeType.MIXIN_REFERENCE) { currentScope.createDataPlaceholder(); + } else if (kid.getType() == ASTCssNodeType.DETACHED_RULESET_REFERENCE) { + currentScope.createDataPlaceholder(); + } else if (AstLogic.isExpression(kid)) { + Expression value = (Expression) kid; + value.setScope(currentScope); } } @@ -105,7 +119,7 @@ private void decreaseScope() { private void increaseScope(ASTCssNode owner) { if (currentScope == null) { currentScope = ScopeFactory.createDefaultScope(owner); - } else if(AstLogic.isBodyOwner(owner)) { + } else if (AstLogic.isBodyOwner(owner)) { currentScope = ScopeFactory.createBodyOwnerScope(owner, currentScope); } else { currentScope = ScopeFactory.createScope(owner, currentScope); @@ -118,7 +132,7 @@ private void increaseScope(ASTCssNode owner) { private PlaceholderScope createPlaceholderScope(ASTCssNode owner) { if (currentScope == null) { throw new BugHappened("No parent scope available.", owner); - } + } return ScopeFactory.createPlaceholderScope(owner, currentScope); } diff --git a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/MixinsSolver.java b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/MixinsSolver.java index 03fc2873..6c26653a 100644 --- a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/MixinsSolver.java +++ b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/MixinsSolver.java @@ -8,10 +8,14 @@ import com.github.sommeri.less4j.core.ast.Body; import com.github.sommeri.less4j.core.ast.BodyOwner; import com.github.sommeri.less4j.core.ast.Declaration; +import com.github.sommeri.less4j.core.ast.DetachedRuleset; +import com.github.sommeri.less4j.core.ast.DetachedRulesetReference; +import com.github.sommeri.less4j.core.ast.Expression; import com.github.sommeri.less4j.core.ast.GeneralBody; import com.github.sommeri.less4j.core.ast.MixinReference; import com.github.sommeri.less4j.core.ast.ReusableStructure; import com.github.sommeri.less4j.core.compiler.expressions.ExpressionEvaluator; +import com.github.sommeri.less4j.core.compiler.expressions.ExpressionFilter; import com.github.sommeri.less4j.core.compiler.expressions.GuardValue; import com.github.sommeri.less4j.core.compiler.expressions.MixinsGuardsValidator; import com.github.sommeri.less4j.core.compiler.scopes.FullMixinDefinition; @@ -39,18 +43,20 @@ public MixinsSolver(ReferencesSolver parentSolver, AstNodesStack semiCompiledNod this.defaultGuardHelper = new DefaultGuardHelper(problemsHandler); } - private MixinCompilationResult resolveMixinReference(final IScope callerScope, final FullMixinDefinition referencedMixin, final IScope mixinWorkingScope) { + //FIXME !!!!!!!! unify with done one + @Deprecated + private BodyCompilationResult resolveMixinReference(final IScope callerScope, final FullMixinDefinition referencedMixin, final IScope mixinWorkingScope) { final ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(mixinWorkingScope, problemsHandler, configuration); final ReusableStructure mixin = referencedMixin.getMixin(); final IScope referencedMixinScope = mixinWorkingScope; // ... and I'm starting to see the point of closures ... - return InScopeSnapshotRunner.runInOriginalDataSnapshot(referencedMixinScope, new IFunction() { + return InScopeSnapshotRunner.runInOriginalDataSnapshot(referencedMixinScope, new IFunction() { @Override - public MixinCompilationResult run() { + public BodyCompilationResult run() { // compile referenced mixin - keep the original copy unchanged - List replacement = compileReferencedMixin(mixin, referencedMixinScope); + List replacement = compileBody(mixin.getBody(), referencedMixinScope); // collect variables and mixins to be imported IScope returnValues = expressionEvaluator.evaluateValues(referencedMixinScope); @@ -59,24 +65,51 @@ public MixinCompilationResult run() { List allMixinsToImport = mixinsToImport(callerScope, referencedMixinScope, unmodifiedMixinsToImport); returnValues.addAllMixins(allMixinsToImport); - return new MixinCompilationResult(mixin, replacement, returnValues); + return new BodyCompilationResult(mixin, replacement, returnValues); } }); } + + private BodyCompilationResult resolveReferencedBody(final IScope callerScope, final BodyOwner mixin, final IScope mixinWorkingScope) { + final ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(mixinWorkingScope, problemsHandler, configuration); + + + final IScope referencedMixinScope = mixinWorkingScope; + // ... and I'm starting to see the point of closures ... + return InScopeSnapshotRunner.runInOriginalDataSnapshot(referencedMixinScope, new IFunction() { + + @Override + public BodyCompilationResult run() { + // compile referenced mixin - keep the original copy unchanged + List replacement = compileBody(mixin.getBody(), referencedMixinScope); - //TODO: all these methods names are too similar to each other - better and clearer naming is needed - private List compileReferencedMixin(ReusableStructure referencedMixin, IScope referencedMixinScopeSnapshot) { - semiCompiledNodes.push(referencedMixin); + // collect variables and mixins to be imported + IScope returnValues = ScopeFactory.createDummyScope(); + returnValues.addFilteredVariables(new ImportedScopeFilter(expressionEvaluator, callerScope), referencedMixinScope); + + //FIXME: !!!!!!!!!! clean up + return new BodyCompilationResult((ASTCssNode) mixin, replacement, returnValues); + + + } + + }); + } + + + private List compileBody(Body body, IScope scopeSnapshot) { + semiCompiledNodes.push(body.getParent()); try { - GeneralBody bodyClone = referencedMixin.getBody().clone(); - parentSolver.unsafeDoSolveReferences(bodyClone, referencedMixinScopeSnapshot); + Body bodyClone = body.clone(); + parentSolver.unsafeDoSolveReferences(bodyClone, scopeSnapshot); return bodyClone.getMembers(); } finally { semiCompiledNodes.pop(); } } + private List mixinsToImport(IScope referenceScope, IScope referencedMixinScope, List unmodifiedMixinsToImport) { List result = new ArrayList(); for (FullMixinDefinition mixinToImport : unmodifiedMixinsToImport) { @@ -99,7 +132,7 @@ private List mixinsToImport(IScope referenceScope, IScope r return result; } - private void shiftComments(MixinReference reference, GeneralBody result) { + private void shiftComments(ASTCssNode reference, GeneralBody result) { List childs = result.getMembers(); if (!childs.isEmpty()) { childs.get(0).addOpeningComments(reference.getOpeningComments()); @@ -118,7 +151,7 @@ public GeneralBody buildMixinReferenceReplacement(final MixinReference reference return result; //candidate mixins with information about their default() function use are stored here - final List compiledMixins = new ArrayList(); + final List compiledMixins = new ArrayList(); for (final FullMixinDefinition fullMixin : mixins) { final ReusableStructure mixin = fullMixin.getMixin(); @@ -138,7 +171,7 @@ public void run() { if (guardValue!=GuardValue.DO_NOT_USE) { //OPTIMIZATION POSSIBLE: there is no need to compile mixins at this point, some of them are not going to be //used and create snapshot operation is cheap now. It should be done later on. - MixinCompilationResult compiled = resolveMixinReference(callerScope, fullMixin, mixinWorkingScope); + BodyCompilationResult compiled = resolveMixinReference(callerScope, fullMixin, mixinWorkingScope); //mark the mixin according to its default() function use compiled.setGuardValue(guardValue); //store the mixin as candidate @@ -149,10 +182,10 @@ public void run() { } // filter out mixins we do not want to use - List mixinsToBeUsed = defaultGuardHelper.chooseMixinsToBeUsed(compiledMixins, reference); + List mixinsToBeUsed = defaultGuardHelper.chooseMixinsToBeUsed(compiledMixins, reference); // update mixin replacements and update scope with imported variables and mixins - for (MixinCompilationResult compiled : mixinsToBeUsed) { + for (BodyCompilationResult compiled : mixinsToBeUsed) { result.addMembers(compiled.getReplacement()); callerScope.addToDataPlaceholder(compiled.getReturnValues()); } @@ -164,6 +197,22 @@ public void run() { return result; } + public GeneralBody buildDetachedRulesetReplacement(DetachedRulesetReference reference, IScope callerScope, DetachedRuleset detachedRuleset, IScope detachedRulesetScope) { + //FIXME: !!!!!!!!!!!! this should run in detachedRulesetScope parent snapshot - check whether it is the case + IScope mixinWorkingScope = calculateBodyWorkingScope(callerScope, null, detachedRulesetScope); + BodyCompilationResult compiled = resolveReferencedBody(callerScope, detachedRuleset, mixinWorkingScope); + GeneralBody result = new GeneralBody(reference.getUnderlyingStructure()); + + result.addMembers(compiled.getReplacement()); + callerScope.addToDataPlaceholder(compiled.getReturnValues()); + callerScope.closeDataPlaceholder(); + + //resolveImportance(reference, result); + shiftComments(reference, result); + + return result; + } + private void resolveImportance(MixinReference reference, GeneralBody result) { if (reference.isImportant()) { declarationsAreImportant(result); @@ -183,6 +232,7 @@ private void declarationsAreImportant(Body result) { } } + @Deprecated //FIXME: !!! evaluate need for this private static IScope calculateMixinsWorkingScope(IScope callerScope, IScope arguments, IScope mixinScope) { // add arguments IScope mixinDeclarationScope = mixinScope.getParent(); @@ -199,5 +249,88 @@ private static IScope calculateMixinsWorkingScope(IScope callerScope, IScope arg return result; } + private static IScope calculateBodyWorkingScope(IScope callerScope, IScope arguments, IScope bodyScope) { + // add arguments + if (arguments != null) { + IScope mixinDeclarationScope = bodyScope.getParent(); + mixinDeclarationScope.add(arguments); + } + + //FIXME !!!!!!!!!!! make this a method in IScope + //FIXME !!!!!! find the case where this is needed for mixins and create test case for it + // locally defined mixin does not require any other action + boolean isLocallyDefined = bodyScope.seesLocalDataOf(callerScope); + IScope parent = callerScope.getParent(); + while (isLocallyDefined && parent!=null) { + isLocallyDefined = bodyScope.seesLocalDataOf(parent);; + parent = parent.getParent(); + } + + if (isLocallyDefined) { + return bodyScope; + } + + //join scopes + IScope result = ScopeFactory.createJoinedScopesView(callerScope, bodyScope); + return result; + } + + //FIXME !!!! refactor and clean, unify with references + class ImportedScopeFilter implements ExpressionFilter { + + private final ExpressionEvaluator expressionEvaluator; + private final IScope importTargetScope; + + public ImportedScopeFilter(ExpressionEvaluator expressionEvaluator, IScope importTargetScope) { + super(); + this.expressionEvaluator = expressionEvaluator; + this.importTargetScope = importTargetScope; + } + + public Expression apply(Expression input) { + Expression result = expressionEvaluator.evaluate(input); + //FIXME: !!!!!!!! probably not necessary? expression evaluator should take care of this + IScope newScope = apply(result.getScope()); + result.setScope(newScope); + return result; + } + + private IScope apply(IScope input) { + if (input==null) + return importTargetScope; + + return constructImportedBodyScope(importTargetScope, input); + + } + + //FIXME !!!! refactor and clean, unify with references solver joiner + private ScopeView constructImportedBodyScope(IScope importTargetScope, IScope bodyToBeImportedScope) { + ScopeView newScope = null; + boolean isLocalImport = bodyToBeImportedScope.seesLocalDataOf(importTargetScope); + + //FIXME !!!! unify with the above scope joiners + // locally defined mixin does not require any other action + IScope parent = importTargetScope.getParent(); + while (isLocalImport && parent!=null) { + isLocalImport = bodyToBeImportedScope.seesLocalDataOf(parent);; + parent = parent.getParent(); + } + + if (isLocalImport) { + // we need to copy the whole tree, because this runs inside referenced mixin scope + // snapshot and imported mixin needs to remember the scope as it is now + newScope = ScopeFactory.createJoinedScopesView(null, bodyToBeImportedScope); + newScope.saveLocalDataForTheWholeWayUp(); + } else { + // since this is non-local import, we need to join reference scope and imported mixins scope + // imported mixin needs to have access to variables defined in caller + newScope = ScopeFactory.createJoinedScopesView(importTargetScope, bodyToBeImportedScope); + newScope.saveLocalDataForTheWholeWayUp(); + } + return newScope; + } + + } + } diff --git a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/ReferencesSolver.java b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/ReferencesSolver.java index ce3da6de..aef3a01a 100644 --- a/src/main/java/com/github/sommeri/less4j/core/compiler/stages/ReferencesSolver.java +++ b/src/main/java/com/github/sommeri/less4j/core/compiler/stages/ReferencesSolver.java @@ -10,10 +10,13 @@ import com.github.sommeri.less4j.core.ast.ASTCssNode; import com.github.sommeri.less4j.core.ast.ASTCssNodeType; import com.github.sommeri.less4j.core.ast.CssString; +import com.github.sommeri.less4j.core.ast.DetachedRuleset; +import com.github.sommeri.less4j.core.ast.DetachedRulesetReference; import com.github.sommeri.less4j.core.ast.EmbeddedScript; import com.github.sommeri.less4j.core.ast.EscapedSelector; import com.github.sommeri.less4j.core.ast.EscapedValue; import com.github.sommeri.less4j.core.ast.Expression; +import com.github.sommeri.less4j.core.ast.FaultyNode; import com.github.sommeri.less4j.core.ast.FixedNamePart; import com.github.sommeri.less4j.core.ast.GeneralBody; import com.github.sommeri.less4j.core.ast.IndirectVariable; @@ -85,14 +88,14 @@ private void unsafeDoSolveReferences(ASTCssNode node, IteratedScope iteratedScop IScope scope = iteratedScope.getScope(); // solve all detached ruleset/mixin references and store solutions - Map solvedMixinReferences = solveCalls(childs, scope); + Map solvedReferences = solveCalls(childs, scope); // solve whatever is not a mixin/detached ruleset reference solveNonCalligReferences(childs, iteratedScope); // replace mixin references by their solutions - we need to do it in the end // the scope and ast would get out of sync otherwise - replaceMixinReferences(solvedMixinReferences); + replaceMixinReferences(solvedReferences); } } finally { semiCompiledNodes.pop(); @@ -102,7 +105,7 @@ private void unsafeDoSolveReferences(ASTCssNode node, IteratedScope iteratedScop private void solveNonCalligReferences(List childs, IteratedScope iteratedScope) { ExpressionEvaluator cssGuardsValidator = new ExpressionEvaluator(iteratedScope.getScope(), problemsHandler, configuration); for (ASTCssNode kid : childs) { - if (isMixinReference(kid)) + if (isMixinReference(kid) || isDetachedRulesetReference(kid)) continue; if (isRuleset(kid)) { @@ -113,12 +116,12 @@ private void solveNonCalligReferences(List childs, IteratedScope ite manipulator.removeFromClosestBody(ruleSet); //skip child scope iteratedScope.getNextChild(); - continue ; + continue; } } - + if (AstLogic.isQuotelessUrlFunction(kid)) { - continue ; + continue; } if (AstLogic.hasOwnScope(kid)) { @@ -135,8 +138,7 @@ private void solveNonCalligReferences(List childs, IteratedScope ite private boolean isMixinReference(ASTCssNode kid) { return kid.getType() == ASTCssNodeType.MIXIN_REFERENCE; } - - @SuppressWarnings("unused") + private boolean isDetachedRulesetReference(ASTCssNode kid) { return kid.getType() == ASTCssNodeType.DETACHED_RULESET_REFERENCE; } @@ -145,32 +147,68 @@ private boolean isRuleset(ASTCssNode kid) { return kid.getType() == ASTCssNodeType.RULE_SET; } - private void replaceMixinReferences(Map solvedMixinReferences) { - for (Entry entry : solvedMixinReferences.entrySet()) { - MixinReference mixinReference = entry.getKey(); + private void replaceMixinReferences(Map solvedReferences) { + for (Entry entry : solvedReferences.entrySet()) { + ASTCssNode mixinReference = entry.getKey(); GeneralBody replacement = entry.getValue(); - + manipulator.setTreeSilentness(replacement, mixinReference.isSilent()); manipulator.replaceInBody(mixinReference, replacement.getMembers()); } } - private Map solveCalls(List childs, IScope mixinReferenceScope) { - Map solvedMixinReferences = new HashMap(); + private Map solveCalls(List childs, IScope referenceScope) { + Map solvedMixinReferences = new HashMap(); for (ASTCssNode kid : childs) { if (isMixinReference(kid)) { - MixinReference mixinReference = (MixinReference) kid; - - List foundMixins = findReferencedMixins(mixinReference, mixinReferenceScope); - GeneralBody replacement = mixinsSolver.buildMixinReferenceReplacement(mixinReference, mixinReferenceScope, foundMixins); + MixinReference reference = (MixinReference) kid; + + List foundMixins = findReferencedMixins(reference, referenceScope); + GeneralBody replacement = mixinsSolver.buildMixinReferenceReplacement(reference, referenceScope, foundMixins); + + AstLogic.validateLessBodyCompatibility(reference, replacement.getMembers(), problemsHandler); + solvedMixinReferences.put(reference, replacement); + } else if (isDetachedRulesetReference(kid)) { + DetachedRulesetReference detachedRulesetReference = (DetachedRulesetReference) kid; + Expression fullNodeDefinition = referenceScope.getValue(detachedRulesetReference.getVariable()); + + if (fullNodeDefinition == null) { + handleUnavailableDetachedRulesetReference(detachedRulesetReference, solvedMixinReferences); + } else { + ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(referenceScope, problemsHandler, configuration); + Expression evaluatedDetachedRuleset = expressionEvaluator.evaluate(fullNodeDefinition); + fullNodeDefinition = evaluatedDetachedRuleset; + if (evaluatedDetachedRuleset.getType() != ASTCssNodeType.DETACHED_RULESET) { + handleWrongDetachedRulesetReference(detachedRulesetReference, evaluatedDetachedRuleset, solvedMixinReferences); + } else { + DetachedRuleset detachedRuleset = (DetachedRuleset) evaluatedDetachedRuleset; + IScope scope = detachedRuleset.getScope(); + GeneralBody replacement = mixinsSolver.buildDetachedRulesetReplacement(detachedRulesetReference, referenceScope, detachedRuleset, scope); + AstLogic.validateLessBodyCompatibility(kid, replacement.getMembers(), problemsHandler); + solvedMixinReferences.put(kid, replacement); + } + } - AstLogic.validateLessBodyCompatibility(mixinReference, replacement.getMembers(), problemsHandler); - solvedMixinReferences.put(mixinReference, replacement); } + } return solvedMixinReferences; } + private void handleUnavailableDetachedRulesetReference(DetachedRulesetReference detachedRulesetReference, Map solvedReferences) { + problemsHandler.detachedRulesetNotfound(detachedRulesetReference); + GeneralBody errorBody = new GeneralBody(detachedRulesetReference.getUnderlyingStructure()); + errorBody.addMember(new FaultyNode(detachedRulesetReference)); + solvedReferences.put(detachedRulesetReference, errorBody); + } + + private void handleWrongDetachedRulesetReference(DetachedRulesetReference detachedRulesetReference, Expression value, Map solvedReferences) { + problemsHandler.wrongDetachedRulesetReference(detachedRulesetReference, value); + GeneralBody errorBody = new GeneralBody(detachedRulesetReference.getUnderlyingStructure()); + errorBody.addMember(new FaultyNode(detachedRulesetReference)); + solvedReferences.put(detachedRulesetReference, errorBody); + } + protected List findReferencedMixins(MixinReference mixinReference, IScope scope) { MixinReferenceFinder finder = new MixinReferenceFinder(this, semiCompiledNodes); List sameNameMixins = finder.getNearestMixins(scope, mixinReference); diff --git a/src/main/java/com/github/sommeri/less4j/core/problems/ProblemsHandler.java b/src/main/java/com/github/sommeri/less4j/core/problems/ProblemsHandler.java index e3275d44..b4f7548b 100644 --- a/src/main/java/com/github/sommeri/less4j/core/problems/ProblemsHandler.java +++ b/src/main/java/com/github/sommeri/less4j/core/problems/ProblemsHandler.java @@ -9,6 +9,7 @@ import com.github.sommeri.less4j.core.ast.ArgumentDeclaration; import com.github.sommeri.less4j.core.ast.Body; import com.github.sommeri.less4j.core.ast.ComparisonExpressionOperator; +import com.github.sommeri.less4j.core.ast.DetachedRulesetReference; import com.github.sommeri.less4j.core.ast.EscapedSelector; import com.github.sommeri.less4j.core.ast.Expression; import com.github.sommeri.less4j.core.ast.FunctionExpression; @@ -199,6 +200,14 @@ private CompilationError createUndefinedMixin(ReusableStructureName name, MixinR return new CompilationError(reference.getFinalName(), "Could not find mixin named \"" + name.asString() + "\"."); } + public void detachedRulesetNotfound(DetachedRulesetReference reference) { + addError(reference, "Could not find detached ruleset for \"" + reference.getVariable().getName() + "\"."); + } + + public void wrongDetachedRulesetReference(DetachedRulesetReference reference, Expression value) { + addError(reference, "Detached ruleset reference \"" + reference.getVariable().getName() + "\" does not evaluate to detached ruleset. It resolved to expression defined at " + printer.toPosition(value)); + } + public void noMixinHasRightParametersCountError(MixinReference reference) { collector.addError(createNoMixinHasRightParametersCountError(reference)); } @@ -272,7 +281,7 @@ public void warnIE8UnsafeDataUri(FunctionExpression errorNode, String filename, addWarning(errorNode, "Skipped data-uri embedding of " + filename + " because its size ("+fileSizeInKB+"dKB) exceeds IE8-safe "+dataUriMaxKb+"dKB!"); } - public void ambiguousDefaultSet(MixinReference reference, List possibleMixins) { + public void ambiguousDefaultSet(MixinReference reference, List possibleMixins) { addError(reference, "Ambiguous use of `default()` found when matching reference " + reference.getFinalName() +". Matched mixins using default are located at " + printer.toNodesPositions(possibleMixins)); } diff --git a/src/main/java/com/github/sommeri/less4j/utils/LessPrinter.java b/src/main/java/com/github/sommeri/less4j/utils/LessPrinter.java index b5222613..5baab7c8 100644 --- a/src/main/java/com/github/sommeri/less4j/utils/LessPrinter.java +++ b/src/main/java/com/github/sommeri/less4j/utils/LessPrinter.java @@ -41,6 +41,10 @@ public String toVariablesString(List cycle) { return result; } + + public String toPosition(ASTCssNode node) { + return node.getSourceLine()+":"+node.getSourceColumn(); + } public String toMixinReferencesString(List cycle) { String result = ""; diff --git a/src/test/resources/minitests/debug1.less b/src/test/resources/minitests/debug1.less index 28a16d60..247b4751 100644 --- a/src/test/resources/minitests/debug1.less +++ b/src/test/resources/minitests/debug1.less @@ -1,14 +1,7 @@ -@label-grey : #333; +@detached: { + property: mixin; +}; -.label-color(@color) { - @label-class:~"label-@{color}"; - @badge-class:~"badge-@{color}"; - @label-color:@@label-class; - - .@{label-class}, .@{badge-class} { - background-color:@label-color !important; - } +.img-responsive { + @detached(); } - -.label-color(~"grey"); -