/* * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.flex.compiler.internal.css.semantics; import static org.apache.flex.compiler.internal.css.CSSStringPropertyValue.stripQuotes; import static com.google.common.collect.Collections2.filter; import static com.google.common.collect.Collections2.transform; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.apache.flex.compiler.common.XMLName; import org.apache.flex.compiler.constants.IASLanguageConstants; import org.apache.flex.compiler.css.ICSSDocument; import org.apache.flex.compiler.css.ICSSNamespaceDefinition; import org.apache.flex.compiler.css.ICSSProperty; import org.apache.flex.compiler.css.ICSSPropertyValue; import org.apache.flex.compiler.css.ICSSRule; import org.apache.flex.compiler.css.ICSSSelector; import org.apache.flex.compiler.definitions.IClassDefinition; import org.apache.flex.compiler.definitions.IDefinition; import org.apache.flex.compiler.definitions.metadata.IMetaTag; import org.apache.flex.compiler.internal.css.CSSFunctionCallPropertyValue; import org.apache.flex.compiler.internal.css.CSSManager; import org.apache.flex.compiler.internal.css.CSSSelector; import org.apache.flex.compiler.internal.css.CSSStringPropertyValue; import org.apache.flex.compiler.internal.css.codegen.CSSCompilationSession; import org.apache.flex.compiler.internal.mxml.MXMLDialect; import org.apache.flex.compiler.internal.parsing.as.ASParser; import org.apache.flex.compiler.internal.projects.ASProject; import org.apache.flex.compiler.internal.targets.Target; import org.apache.flex.compiler.internal.tree.as.metadata.MetaTagsNode; import org.apache.flex.compiler.internal.units.EmbedCompilationUnit; import org.apache.flex.compiler.internal.units.EmbedCompilationUnitFactory; import org.apache.flex.compiler.mxml.IXMLNameResolver; import org.apache.flex.compiler.problems.CSSEmbedAssetProblem; import org.apache.flex.compiler.problems.CSSExcludedStylePropertyProblem; import org.apache.flex.compiler.problems.CSSUndefinedNamespacePrefixProblem; import org.apache.flex.compiler.problems.CSSUndefinedTypeProblem; import org.apache.flex.compiler.problems.CSSUnknownDefaultNamespaceProblem; import org.apache.flex.compiler.problems.CSSUnresolvedClassReferenceProblem; import org.apache.flex.compiler.problems.CSSUnusedTypeSelectorProblem; import org.apache.flex.compiler.problems.ICompilerProblem; import org.apache.flex.compiler.projects.ICompilerProject; import org.apache.flex.compiler.projects.IFlexProject; import org.apache.flex.compiler.tree.metadata.IMetaTagNode; import org.apache.flex.compiler.units.ICompilationUnit; import org.apache.flex.utils.FilenameNormalization; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.collect.Collections2; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; /** * Semantic analyzer for CSS. This is a static class. It's used by * {@link CSSManager}. */ public class CSSSemanticAnalyzer { /** Global selector. */ private static final String GLOBAL_SELECTOR = "global"; /** Universal selector. */ private static final String UNIVERSAL_SELECTOR = "*"; /** * Try to resolve all the dependencies introduced by * {@code ClassReference()} and {@code Embed()} property values in a CSS * rule. * * @param resolvedEmbedProperties A map from {@code Embed()} property values * to their resolved {@link EmbedCompilationUnit}'s. * @param cssRule CSS rule. * @param project Current project. * @param classReferences Definitions of the {@code ClassReference("...")} * properties will be stored in this collection after the function returns. * @param embedCompilationUnits {@link EmbedCompilationUnit}'s used in the * rules will be stored in this collection after the function returns. * @param problems Compiler problems. This method reports * {@link CSSUnresolvedClassReferenceProblem} issues. */ public static void resolveDependencies( final Map<CSSFunctionCallPropertyValue, EmbedCompilationUnit> resolvedEmbedProperties, final ICSSRule cssRule, final ICompilerProject project, final Set<IClassDefinition> classReferences, final Set<EmbedCompilationUnit> embedCompilationUnits, final Collection<ICompilerProblem> problems) { assert cssRule != null : "CSS rule can't be null"; assert project != null : "Project can't be null"; assert problems != null : "Problems can't be null"; assert classReferences != null : "Expected an output collection for ClassReferences."; assert embedCompilationUnits != null : "Expected an output collection for Embed."; for (final ICSSProperty property : cssRule.getProperties()) { final ICSSPropertyValue propertyValue = property.getValue(); if (propertyValue instanceof CSSFunctionCallPropertyValue) { final CSSFunctionCallPropertyValue functionCall = (CSSFunctionCallPropertyValue)propertyValue; if (CSSFunctionCallPropertyValue.CLASS_REFERENCE.equals(functionCall.name)) { // Found a ClassReference() property. if ("null".equals(functionCall.rawArguments)) { // Do nothing. ClassReference(null) resets the skin class. } else { final String qName; if (CSSStringPropertyValue.isQuoted(functionCall.rawArguments)) qName = stripQuotes(functionCall.rawArguments); else qName = functionCall.rawArguments; final IDefinition definition = project.resolveQNameToDefinition(qName); // The definition is expected to be a class definition. if (definition != null && definition instanceof IClassDefinition) { classReferences.add((IClassDefinition)definition); } else { final CSSUnresolvedClassReferenceProblem problem = new CSSUnresolvedClassReferenceProblem(functionCall); problems.add(problem); } } } else if (CSSFunctionCallPropertyValue.EMBED.equals(functionCall.name)) { final String embedMetadata = String.format("[%s(%s)]", functionCall.name, functionCall.rawArguments); // TODO Calling normalize here prevents an assert later // in the getFileSpecification() of Workspace. The problem is that an embed // in default.css inside a SWC has a source path which doesn't look normalized. final String sourcePath = FilenameNormalization.normalize(functionCall.getSourcePath()); final MetaTagsNode metadata = ASParser.parseMetadata(project.getWorkspace(), embedMetadata, sourcePath, functionCall.getStart(), functionCall.getLine(), functionCall.getColumn(), problems); final IMetaTagNode embedTag = metadata.getTagByName("Embed"); if (embedTag == null) { problems.add(new CSSEmbedAssetProblem(functionCall)); } else { try { final EmbedCompilationUnit embedCompilationUnit = EmbedCompilationUnitFactory.getCompilationUnit( (ASProject)project, embedTag.getSourcePath(), functionCall, embedTag.getAllAttributes(), problems); if (embedCompilationUnit == null) { problems.add(new CSSEmbedAssetProblem(functionCall)); } else { resolvedEmbedProperties.put(functionCall, embedCompilationUnit); embedCompilationUnits.add(embedCompilationUnit); } } catch (InterruptedException e) { // Incremental build interrupts the current build. We can // throw away the results. } } } } } } /** * Resolve type selectors to class definitions within a file scope context * <p> * If a namespace short name is mapped to a undefined namespace URI, just * ignore. * * @param xmlNameResolver XML name resolver * @param css CSS DOM. * @param problems Collect problems. * @param isCompatibilityVersion3 If true, do not create * {@link CSSUnknownDefaultNamespaceProblem}'s. * @return A map of CSS selectors to QNames of their resolved types. */ public static ImmutableMap<ICSSSelector, String> resolveSelectors( final IXMLNameResolver xmlNameResolver, final ICSSDocument css, final Collection<ICompilerProblem> problems, final boolean isCompatibilityVersion3) { assert xmlNameResolver != null : "Expected xmlNameResolver"; assert css != null : "Expected CSS"; final ImmutableSet<ICSSSelector> allSelectors = getAllSelectors(css); if (isCompatibilityVersion3) return resolveSelectorsAsFlex3Style(allSelectors); final ICSSNamespaceDefinition defaultNamespace = css.getDefaultNamespaceDefinition(); final Builder<ICSSSelector, String> builder = new Builder<ICSSSelector, String>(); for (final ICSSSelector selector : allSelectors) { // Expand selector to QName and conditions. if (isWildcardSelector(selector)) continue; final String prefix = selector.getNamespacePrefix(); final ICSSNamespaceDefinition namespace; if (prefix == null) { // Check if the selector is a type selector without explicit namespace. if (defaultNamespace == null) { problems.add(new CSSUnknownDefaultNamespaceProblem((CSSSelector)selector)); continue; } else { namespace = defaultNamespace; } } else { // Resolve namespace. namespace = css.getNamespaceDefinition(prefix); } if (namespace == null) { problems.add(new CSSUndefinedNamespacePrefixProblem((CSSSelector)selector)); continue; } assert (selector.getElementName() != null) : "Null element name should be skipped as a wildcard selector."; final XMLName xmlName = new XMLName(namespace.getURI(), selector.getElementName()); // Resolve type name. final String qname = xmlNameResolver.resolveXMLNameToQualifiedName(xmlName, MXMLDialect.MXML_2009); if (qname == null) { if (defaultNamespace != null && defaultNamespace.getURI().equals("http://www.w3.org/1999/xhtml")) builder.put(selector, selector.getElementName()); else problems.add(new CSSUndefinedTypeProblem((CSSSelector)selector)); } else { builder.put(selector, qname); } } return builder.build(); } /** * Resolve selectors as Flex 3 CSS. In Flex 3 CSS, selectors don't have * namespaces. As a result, they don't have to be resolved to a type * definition. For example, selector "Button" will be emitted as a selector * literal "Button". Whereas, in Flex 4 mode, "Button" would be resolved to * a default namespace and then emitted as a Spark Button QName or MX Button * QName. * * @param selectors All the selectors to resolve. * @return A map from selectors to its resolved runtime style selector name. */ private static ImmutableMap<ICSSSelector, String> resolveSelectorsAsFlex3Style( final Iterable<ICSSSelector> selectors) { final ImmutableMap.Builder<ICSSSelector, String> builder = new ImmutableMap.Builder<ICSSSelector, String>(); for (final ICSSSelector selector : selectors) { builder.put(selector, selector.getCSSSyntax()); } return builder.build(); } /** * Collect all the selectors in the CSS document including the subjects and * the combination selectors. * * @param document CSS document * @return All the selectors in the CSS. */ public static ImmutableSet<ICSSSelector> getAllSelectors(final ICSSDocument document) { assert document != null : "Expected CSS document"; final ImmutableSet.Builder<ICSSSelector> builder = new ImmutableSet.Builder<ICSSSelector>(); for (final ICSSRule rule : document.getRules()) { for (final ICSSSelector subject : rule.getSelectorGroup()) { ICSSSelector selector = subject; while (selector != null) { builder.add(selector); if (selector.getCombinator() != null) selector = selector.getCombinator().getSelector(); else selector = null; } } } return builder.build(); } /** * A {@link Predicate} that filters {@link ICSSSelector}'s matched by a * given set of class definitions. This is created for * {@link Collections2#filter(Collection, Predicate)}. */ private static class MatchedCSSRulePredicate implements Predicate<ICSSRule> { /** * QNames of the definitions to be matched by the CSS rules. */ private final ImmutableSet<String> qnames; /** * A map of selectors resolved to class definitions. */ private final ImmutableMap<ICSSSelector, String> resolvedSelectors; /** * Create a predicate for filtering matched CSS rules. * * @param qnames A set of class definitions to be matched by the * CSS rules. * @param resolvedSelectors A map of selectors resolved to class * definitions. */ public MatchedCSSRulePredicate(final ImmutableSet<String> qnames, final ImmutableMap<ICSSSelector, String> resolvedSelectors) { assert qnames != null : "Expected a set of definition for the CSS rules to match."; assert resolvedSelectors != null : "Expected a map of selectors resolved to class definitions."; this.qnames = qnames; this.resolvedSelectors = resolvedSelectors; } /** * Return true if any of the <b>subject</b> selectors in the * {@code rule}'s selector group match any definitions in * {@link #qnames}. Combinator selectors are ignored. */ @Override public boolean apply(final ICSSRule rule) { for (final ICSSSelector selector : rule.getSelectorGroup()) { if (isWildcardSelector(selector)) return true; final String qname = resolvedSelectors.get(selector); if (qnames.contains(qname)) return true; } return false; } } /** * This predicate is created for {@code -compatibility-version=3} mode. In * Flex 3, the selectors don't have namespace specifiers. Under the * "compatible" mode, {@code Button} means {@code *|Button} in CSS3 syntax. * <p> * All selectors with Flex 4 advanced syntax will be dropped. * <p> * This class only compares the selector element names and the definition * short names. */ private static class Flex3CSSRulePredicate implements Predicate<ICSSRule> { private final ImmutableSet<String> definitionSimpleNames; private Flex3CSSRulePredicate(final ImmutableSet<String> definitionSimpleNames) { this.definitionSimpleNames = definitionSimpleNames; } @Override public boolean apply(ICSSRule rule) { for (final ICSSSelector selector : rule.getSelectorGroup()) { // drop advanced selectors for flex 3 if (selector.isAdvanced()) return false; // drop unused css rules final String elementName = selector.getElementName(); if (GLOBAL_SELECTOR.equals(elementName)) continue; if (elementName == null) continue; if (!definitionSimpleNames.contains(elementName)) return false; } return true; } } /** * Convert a dot-separated QName string to the simple name. For example: * <ul> * <li>{@code f("a.b.foo") = "foo";}</li> * <li>{@code f("bar") = "bar";}</li> * </ul> */ private static final Function<String, String> QNAME_TO_SIMPLE_NAME = new Function<String, String>() { @Override public String apply(String qname) { return Iterables.getLast(Splitter.on(".").omitEmptyStrings().split(qname)); } }; /** * Get a set of {@link ICSSRule}'s that match any of the class definitions * passed in. * * @param session CSS compilation session data. * @param flexProject Flex project. * @param cssDocument CSS document. * @param qnames A set of QNames of the definitions to be matched the CSS * rules. * @param problems Problems collection. * @return A set of CSS rules matched by one of the given class definitions. */ public static ImmutableSet<ICSSRule> getMatchedRules( final CSSCompilationSession session, final IFlexProject flexProject, final ICSSDocument cssDocument, final ImmutableSet<String> qnames, final Collection<ICompilerProblem> problems) { final boolean isFlex3CSS = flexProject.getCSSManager().isFlex3CSS(); final ImmutableMap<ICSSSelector, String> resolvedSelectors = resolveSelectors(flexProject, cssDocument, problems, isFlex3CSS); final Predicate<ICSSRule> predicate; if (isFlex3CSS) { final ImmutableSet<String> simpleNames = ImmutableSet.copyOf(transform(qnames, QNAME_TO_SIMPLE_NAME)); predicate = new Flex3CSSRulePredicate(simpleNames); } else { predicate = new MatchedCSSRulePredicate(qnames, resolvedSelectors); } // Cache the result of selector resolution on the session. // The CSS code generation will use this map later. session.resolvedSelectors.putAll(resolvedSelectors); // Find rules with selectors that match types in a given definition set. return ImmutableSet.copyOf(filter(cssDocument.getRules(), predicate)); } /** * Check if the selector is a wildcard selector. For example: * <ul> * <li>global</li> * <li>*</li> * <li>.highlight</li> * <li>:up</li> * </ul> * * @param selector CSS selector * @return True if the selector is a "wildcard" selector. */ public static boolean isWildcardSelector(ICSSSelector selector) { final String elementName = selector.getElementName(); return elementName == null || UNIVERSAL_SELECTOR.equals(elementName) || GLOBAL_SELECTOR.equals(elementName); } /** * Build a map from QNames to class definitions. * * @param classDefinitions Class definitions. * @return Lookup map. */ public static final ImmutableMap<String, IClassDefinition> buildQNameToDefinitionMap(final Collection<IClassDefinition> classDefinitions) { final Map<String, IClassDefinition> builder = new HashMap<String, IClassDefinition>(); for (final IClassDefinition classDefinition : classDefinitions) { builder.put(classDefinition.getQualifiedName(), classDefinition); } return ImmutableMap.copyOf(builder); } /** * Find all the class definitions in the given collection. * * @param definitions A collection of definitions. * @return A set of class definitions. */ public static ImmutableSet<IClassDefinition> getClassDefinitionSet(final Collection<IDefinition> definitions) { final ImmutableSet.Builder<IClassDefinition> builder = new ImmutableSet.Builder<IClassDefinition>(); for (final IDefinition def : definitions) { if (def instanceof IClassDefinition) builder.add((IClassDefinition)def); } final ImmutableSet<IClassDefinition> classDefinitions = builder.build(); return classDefinitions; } /** * <p> * Validate a CSS model. The validation only works for Flex 4+. * </p> * <h1>Find CSS rules with unused type selectors.</h1> * <p> * The result is added to the problem collection. * <p> * For example, if an MXML document only uses {@code <s:Button>} tags, and * its {@code <fx:Style>} block contains: * * <pre> * ... * s|Button { fontSize : 12; } * local|MyComponent { color : red; } * ... * </pre> * * Since {@code <local:MyComponent>} isn't used in the current MXML * document, the {@code local|MyComponent} is a rule with an <i>unused type * selector</i>. * <p> * The validation process only finds all the unused type selectors, but it * doesn't take them out of the code generation. * <p> * <h1>Find usages of excludes styles.</h1> If a component declares one of * its styles to be "excluded", usages of such styles will be reported as * {@link CSSExcludedStylePropertyProblem}. * * <pre> * [Exclude(kind="style", name="foo")] * public class MyButton * { * } * </pre> * * The following CSS will cause the problem: * * <pre> * local|MyButton { foo : something; } * </pre> * * @param linkingCompilationUnits All type selectors that doesn't map to any * definition in this collection are "unused". * @param session {@link CSSCompilationSession#cssDocuments} has all the CSS * models an MXML document has. * @throws InterruptedException Abort compilation. */ public static void validate( final Set<ICompilationUnit> linkingCompilationUnits, final CSSCompilationSession session, final Collection<ICompilerProblem> problems) throws InterruptedException { final CSSValidator validator = new CSSValidator(session, linkingCompilationUnits, problems); for (final ICSSDocument cssDocument : session.cssDocuments) { visit(cssDocument, validator); } } /** * CSS model visitor. */ private static interface ICSSVisitor { /** * Visit a CSS document. */ void beginDocument(final ICSSDocument document); /** * Visit a CSS rule. */ void beginRule(final ICSSRule rule); /** * Visit a CSS subject selector. */ void beginSubject(final ICSSSelector selector, final ICSSRule rule); /** * Visit a CSS property. */ void beginProperty(final ICSSProperty property, final ICSSRule rule); } /** * Validate the following CSS semantic constraints: * <ol> * <li>unused type selectors</li> * <li>usage of excluded styles</li> * </ol> */ private static class CSSValidator implements ICSSVisitor { private final CSSCompilationSession session; private final ImmutableMap<String, IClassDefinition> qnameToDefinition; private final Collection<ICompilerProblem> problems; private final Multimap<ICSSSelector, String> excludedStyles; /** * Create a CSS validating visitor. * * @param session CSS compilation session. * @param linkingCompilationUnits All the compilation units to be * linked. This is used to find <i>unused type selectors</i>. * @param problems Problem collection. * @throws InterruptedException Compilation aborted. */ private CSSValidator(final CSSCompilationSession session, final Set<ICompilationUnit> linkingCompilationUnits, final Collection<ICompilerProblem> problems) throws InterruptedException { this.session = session; final ImmutableList<IDefinition> linkingDefinitions = Target.getAllExternallyVisibleDefinitions(linkingCompilationUnits); final ImmutableSet<IClassDefinition> classDefinitions = getClassDefinitionSet(linkingDefinitions); this.qnameToDefinition = buildQNameToDefinitionMap(classDefinitions); this.problems = problems; this.excludedStyles = HashMultimap.create(); } /** * <ol> * <li>Find all excluded styles for the current subject.</li> * <li>Find unused type selectors.</li> * </ol> */ @Override public void beginSubject(final ICSSSelector selector, final ICSSRule rule) { if (!isWildcardSelector(selector)) { final String qname = session.resolvedSelectors.get(selector); if (qnameToDefinition.containsKey(qname)) { // The subject's resolved QName is in the linking set. // Collect all "excluded" styles for this subject selector. // Only check "Exclude" styles on used styles. final IClassDefinition classDefinition = qnameToDefinition.get(qname); final IMetaTag[] excludeMetaTags = classDefinition.getMetaTagsByName(IASLanguageConstants.EXCLUDE_META_TAG); for (final IMetaTag exclude : excludeMetaTags) { final String kind = exclude.getAttributeValue(IASLanguageConstants.EXCLUDE_META_TAG_KIND); if (IASLanguageConstants.EXCLUDE_META_TAG_STYLE.equals(kind)) { final String excludedStyleName = exclude.getAttributeValue(IASLanguageConstants.EXCLUDE_META_TAG_NAME); if (excludedStyleName != null && !excludedStyleName.isEmpty()) { this.excludedStyles.put(selector, excludedStyleName); } } } } else { // Selector's resolved QName is not in the linking set. problems.add(new CSSUnusedTypeSelectorProblem(selector)); } } } @Override public void beginRule(final ICSSRule rule) { } @Override public void beginDocument(final ICSSDocument document) { } /** * Check usages of excluded styles. */ @Override public void beginProperty(final ICSSProperty property, final ICSSRule rule) { for (final ICSSSelector subject : rule.getSelectorGroup()) { final Collection<String> excludedStylesForSubject = excludedStyles.get(subject); if (excludedStylesForSubject != null && excludedStylesForSubject.contains(property.getName())) { final String qname = session.resolvedSelectors.get(subject); problems.add(new CSSExcludedStylePropertyProblem(property, qname)); } } } } /** * Visit a CSS document model. * * @param document CSS model. * @param visitor Handler for various visit methods. */ private static void visit(final ICSSDocument document, final ICSSVisitor visitor) { visitor.beginDocument(document); for (final ICSSRule rule : document.getRules()) { visitor.beginRule(rule); for (final ICSSSelector selector : rule.getSelectorGroup()) visitor.beginSubject(selector, rule); for (final ICSSProperty property : rule.getProperties()) visitor.beginProperty(property, rule); } } }