From f4f3fd5fd74846be7ef6ae0caa851d9b8e1e163b Mon Sep 17 00:00:00 2001 From: vrubezhny Date: Thu, 30 Jan 2014 00:22:48 +0400 Subject: HTML5 attribute validator marks ng-app AngularJS attributes as undefined A new preferences are created for the pattern of attribute names to be ignored by HTMLAttributeValidator The preference initializer is made to initialize new preferences HTMLAttributeValidator is made to understand attribute names ignorance preferences Signed-off-by: vrubezhny --- .../preferences/HTMLCorePreferenceInitializer.java | 8 +- .../preferences/HTMLCorePreferenceNames.java | 22 +- .../internal/validate/HTMLAttributeValidator.java | 91 +++++++- .../html/core/internal/validate/StringMatcher.java | 232 +++++++++++++++++++++ 4 files changed, 344 insertions(+), 9 deletions(-) create mode 100644 bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/validate/StringMatcher.java diff --git a/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/preferences/HTMLCorePreferenceInitializer.java b/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/preferences/HTMLCorePreferenceInitializer.java index 6eb852a6b1..207744d2bd 100644 --- a/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/preferences/HTMLCorePreferenceInitializer.java +++ b/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/preferences/HTMLCorePreferenceInitializer.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2005, 2012 IBM Corporation and others. + * Copyright (c) 2005, 2014 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,6 +7,8 @@ * * Contributors: * IBM Corporation - initial API and implementation + * Red Hat, Inc. - Bug #426939 - [validator] HTML5 attribute validator + * marks ng-app AngularJS attributes as undefined *******************************************************************************/ package org.eclipse.wst.html.core.internal.preferences; @@ -72,6 +74,10 @@ public class HTMLCorePreferenceInitializer extends AbstractPreferenceInitializer * @param node the Eclipse preference node */ private void initializeValidationPreferences(IEclipsePreferences node) { + // Ignored Attribute names + node.putBoolean(HTMLCorePreferenceNames.IGNORE_ATTRIBUTE_NAMES, HTMLCorePreferenceNames.IGNORE_ATTRIBUTE_NAMES_DEFAULT); + node.put(HTMLCorePreferenceNames.ATTRIBUTE_NAMES_TO_IGNORE, HTMLCorePreferenceNames.ATTRIBUTE_NAMES_TO_IGNORE_DEFAULT); + // Attributes node.putInt(HTMLCorePreferenceNames.ATTRIBUTE_UNDEFINED_NAME, ValidationMessage.WARNING); node.putInt(HTMLCorePreferenceNames.ATTRIBUTE_UNDEFINED_VALUE, ValidationMessage.WARNING); diff --git a/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/preferences/HTMLCorePreferenceNames.java b/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/preferences/HTMLCorePreferenceNames.java index fe7fb60010..6cdc9097cc 100644 --- a/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/preferences/HTMLCorePreferenceNames.java +++ b/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/preferences/HTMLCorePreferenceNames.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2005, 2011 IBM Corporation and others. + * Copyright (c) 2005, 2014 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -8,7 +8,8 @@ * Contributors: * IBM Corporation - initial API and implementation * Jens Lukowski/Innoopract - initial renaming/restructuring - * + * Red Hat, Inc. - Bug #426939 - [validator] HTML5 attribute validator + * marks ng-app AngularJS attributes as undefined *******************************************************************************/ package org.eclipse.wst.html.core.internal.preferences; @@ -238,7 +239,23 @@ public class HTMLCorePreferenceNames { */ public static final int UPPER = 2; + /** + * Default value for the preference #IGNORE_ATTRIBUTE_NAMES + * + * @see #IGNORE_ATTRIBUTE_NAMES + */ + public static final boolean IGNORE_ATTRIBUTE_NAMES_DEFAULT = false; + + /** + * Default value for the preference #ATTRIBUTE_NAMES_TO_IGNORE + * + * @see #ATTRIBUTE_NAMES_TO_IGNORE + */ + public static final String ATTRIBUTE_NAMES_TO_IGNORE_DEFAULT = ""; //$NON-NLS-1$ + public static final String USE_PROJECT_SETTINGS = "use-project-settings";//$NON-NLS-1$ + public static final String IGNORE_ATTRIBUTE_NAMES = "ignoreAttrNames";//$NON-NLS-1$ + public static final String ATTRIBUTE_NAMES_TO_IGNORE = "attrNamesToIgnore";//$NON-NLS-1$ public static final String ATTRIBUTE_UNDEFINED_NAME = "attrUndefName";//$NON-NLS-1$ public static final String ATTRIBUTE_UNDEFINED_VALUE = "attrUndefValue";//$NON-NLS-1$ @@ -252,7 +269,6 @@ public class HTMLCorePreferenceNames { public static final String ATTRIBUTE_OBSOLETE_NAME = "attrObsoleteName";//$NON-NLS-1$ public static final String ATTRIBUTE_VALUE_EQUALS_MISSING = "attrValueEqualsMissing";//$NON-NLS-1$ - public static final String ELEM_UNKNOWN_NAME = "elemUnknownName";//$NON-NLS-1$ public static final String ELEM_INVALID_NAME = "elemInvalidName";//$NON-NLS-1$ public static final String ELEM_START_INVALID_CASE = "elemStartInvalidCase";//$NON-NLS-1$ diff --git a/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/validate/HTMLAttributeValidator.java b/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/validate/HTMLAttributeValidator.java index c4d794fb81..9acadf0f0a 100644 --- a/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/validate/HTMLAttributeValidator.java +++ b/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/validate/HTMLAttributeValidator.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2004, 2013 IBM Corporation and others. + * Copyright (c) 2004, 2014 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,18 +7,35 @@ * * Contributors: * IBM Corporation - initial API and implementation + * Red Hat, Inc. - Bug #426939 - [validator] HTML5 attribute validator + * marks ng-app AngularJS attributes as undefined *******************************************************************************/ package org.eclipse.wst.html.core.internal.validate; import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Set; +import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ProjectScope; import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.preferences.DefaultScope; +import org.eclipse.core.runtime.preferences.IPreferencesService; +import org.eclipse.core.runtime.preferences.IScopeContext; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.wst.html.core.internal.HTMLCorePlugin; import org.eclipse.wst.html.core.internal.document.HTMLDocumentTypeConstants; +import org.eclipse.wst.html.core.internal.preferences.HTMLCorePreferenceNames; import org.eclipse.wst.sse.core.internal.provisional.INodeNotifier; import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; @@ -49,6 +66,9 @@ public class HTMLAttributeValidator extends PrimeValidator { private static final char SINGLE_QUOTE = '\''; private static final char DOUBLE_QUOTE = '\"'; + private IPreferencesService fPreferenceService; + private static Map fIgnorePatterns = new HashMap(); // A storage for ignore patterns (instances of StringMatcher) + // HTML(5) data attributes private static final String ATTR_NAME_DATA = "data-"; //$NON-NLS-1$ private static final int ATTR_NAME_DATA_LENGTH = ATTR_NAME_DATA.length(); @@ -63,6 +83,7 @@ public class HTMLAttributeValidator extends PrimeValidator { */ public HTMLAttributeValidator() { super(); + fPreferenceService = Platform.getPreferencesService(); } /** @@ -153,11 +174,15 @@ public class HTMLAttributeValidator extends PrimeValidator { } if (adec == null) { - if ((attrName.startsWith(ATTR_NAME_DATA) && attrName.length() > ATTR_NAME_DATA_LENGTH) || (attrName.startsWith(ATTR_NAME_USER_AGENT_FEATURE) && attrName.length() > ATTR_NAME_USER_AGENT_FEATURE_LENGTH)) { - DocumentTypeAdapter documentTypeAdapter = (DocumentTypeAdapter) ((INodeNotifier) target.getOwnerDocument()).getAdapterFor(DocumentTypeAdapter.class); - if (documentTypeAdapter != null && documentTypeAdapter.hasFeature(HTMLDocumentTypeConstants.HTML5)) + if ((attrName.startsWith(ATTR_NAME_DATA) && attrName.length() > ATTR_NAME_DATA_LENGTH) || + (attrName.startsWith(ATTR_NAME_USER_AGENT_FEATURE) && attrName.length() > ATTR_NAME_USER_AGENT_FEATURE_LENGTH)) { + if (isHTML5(target)) continue; - } + } + // Check for user-defined exclusions for HTML5 attribute names + if (!shouldValidateAttributeName(target, attrName)) + continue; + // No attr declaration was found. That is, the attr name is // undefined. // but not regard it as undefined name if it includes nested @@ -333,4 +358,60 @@ public class HTMLAttributeValidator extends PrimeValidator { return (c == SINGLE_QUOTE) || (c == DOUBLE_QUOTE); } // D210422 + + private boolean isHTML5(Element target) { + DocumentTypeAdapter documentTypeAdapter = (DocumentTypeAdapter) ((INodeNotifier) target.getOwnerDocument()).getAdapterFor(DocumentTypeAdapter.class); + return (documentTypeAdapter != null && + documentTypeAdapter.hasFeature(HTMLDocumentTypeConstants.HTML5)); + } + + private boolean shouldValidateAttributeName(Element target, String attrName) { + if (!isHTML5(target)) return true; + + Object adapter = (target instanceof IAdaptable ? ((IAdaptable)target).getAdapter(IResource.class) : null); + IProject project = (adapter instanceof IResource ? ((IResource)adapter).getProject() : null); + + Iterator excludedAttributes = getExcludedAttributeNames(project).iterator(); + while (excludedAttributes.hasNext()) { + String excluded = (String)excludedAttributes.next(); + StringMatcher strMatcher = (StringMatcher)fIgnorePatterns.get(excluded); + if (strMatcher == null) { + strMatcher = new StringMatcher(excluded); + fIgnorePatterns.put(excluded, strMatcher); + } + if (strMatcher.match(attrName)) + return false; + } + + return true; + } + + private Set getExcludedAttributeNames(IProject project) { + IScopeContext[] fLookupOrder = new IScopeContext[] {new InstanceScope(), new DefaultScope()}; + if (project != null) { + ProjectScope projectScope = new ProjectScope(project); + if(projectScope.getNode(HTMLCorePlugin.getDefault().getBundle().getSymbolicName()).getBoolean(HTMLCorePreferenceNames.USE_PROJECT_SETTINGS, false)) + fLookupOrder = new IScopeContext[] {projectScope, new InstanceScope(), new DefaultScope()}; + } + + Set result = new HashSet(); + if (fPreferenceService.getBoolean(HTMLCorePlugin.getDefault().getBundle().getSymbolicName(), + HTMLCorePreferenceNames.IGNORE_ATTRIBUTE_NAMES, HTMLCorePreferenceNames.IGNORE_ATTRIBUTE_NAMES_DEFAULT, + fLookupOrder)) { + String ignoreList = fPreferenceService.getString(HTMLCorePlugin.getDefault().getBundle().getSymbolicName(), + HTMLCorePreferenceNames.ATTRIBUTE_NAMES_TO_IGNORE, HTMLCorePreferenceNames.ATTRIBUTE_NAMES_TO_IGNORE_DEFAULT, + fLookupOrder); + + if (ignoreList.trim().isEmpty()) + return result; + + String[] names = ignoreList.split(","); //$NON-NLS-1$ + for (int i = 0; names != null && i < names.length; i++) { + String name = names[i] == null ? null : names[i].trim(); + if (name != null && !name.isEmpty()) + result.add(name.toLowerCase()); + } + } + return result; + } } diff --git a/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/validate/StringMatcher.java b/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/validate/StringMatcher.java new file mode 100644 index 0000000000..425ef995b0 --- /dev/null +++ b/bundles/org.eclipse.wst.html.core/src/org/eclipse/wst/html/core/internal/validate/StringMatcher.java @@ -0,0 +1,232 @@ +/******************************************************************************* + * Copyright (c) 2000, 2014 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + * Red Hat, Inc. - Bug #426939 - [validator] HTML5 attribute validator + * marks ng-app AngularJS attributes as undefined + *******************************************************************************/ +package org.eclipse.wst.html.core.internal.validate; + +import java.util.ArrayList; + +/** + * A string pattern matcher, supporting "*" and "?" wild cards. + * + * @since 3.2 + */ +public class StringMatcher { + private static final char SINGLE_WILD_CARD = '\u0000'; + + /** + * Boundary value beyond which we don't need to search in the text + */ + private int bound = 0; + + private boolean hasLeadingStar; + + private boolean hasTrailingStar; + + private final String pattern; + + private final int patternLength; + + /** + * The pattern split into segments separated by * + */ + private String segments[]; + + /** + * StringMatcher constructor takes in a String object that is a simple + * pattern which may contain '*' for 0 and many characters and + * '?' for exactly one character. + * + * Literal '*' and '?' characters must be escaped in the pattern + * e.g., "\*" means literal "*", etc. + * + * Escaping any other character (including the escape character itself), + * just results in that character in the pattern. + * e.g., "\a" means "a" and "\\" means "\" + * + * If invoking the StringMatcher with string literals in Java, don't forget + * escape characters are represented by "\\". + * + * @param pattern the pattern to match text against + */ + public StringMatcher(String pattern) { + if (pattern == null) + throw new IllegalArgumentException(); + this.pattern = pattern; + patternLength = pattern.length(); + parseWildCards(); + } + + /** + * @param text a simple regular expression that may only contain '?'(s) + * @param start the starting index in the text for search, inclusive + * @param end the stopping point of search, exclusive + * @param p a simple regular expression that may contain '?' + * @return the starting index in the text of the pattern , or -1 if not found + */ + private int findPosition(String text, int start, int end, String p) { + boolean hasWildCard = p.indexOf(SINGLE_WILD_CARD) >= 0; + int plen = p.length(); + for (int i = start, max = end - plen; i <= max; ++i) { + if (hasWildCard) { + if (regExpRegionMatches(text, i, p, 0, plen)) + return i; + } else { + if (text.regionMatches(true, i, p, 0, plen)) + return i; + } + } + return -1; + } + + /** + * Given the starting (inclusive) and the ending (exclusive) positions in the + * text, determine if the given substring matches with aPattern + * @return true if the specified portion of the text matches the pattern + * @param text a String object that contains the substring to match + */ + public boolean match(String text) { + if (text == null) + return false; + final int end = text.length(); + final int segmentCount = segments.length; + if (segmentCount == 0 && (hasLeadingStar || hasTrailingStar)) // pattern contains only '*'(s) + return true; + if (end == 0) + return patternLength == 0; + if (patternLength == 0) + return false; + int currentTextPosition = 0; + if ((end - bound) < 0) + return false; + int segmentIndex = 0; + String current = segments[segmentIndex]; + + /* process first segment */ + if (!hasLeadingStar) { + int currentLength = current.length(); + if (!regExpRegionMatches(text, 0, current, 0, currentLength)) + return false; + segmentIndex++; + currentTextPosition = currentTextPosition + currentLength; + } + if ((segmentCount == 1) && (!hasLeadingStar) && (!hasTrailingStar)) { + // only one segment to match, no wild cards specified + return currentTextPosition == end; + } + /* process middle segments */ + while (segmentIndex < segmentCount) { + current = segments[segmentIndex]; + int currentMatch = findPosition(text, currentTextPosition, end, current); + if (currentMatch < 0) + return false; + currentTextPosition = currentMatch + current.length(); + segmentIndex++; + } + + /* process final segment */ + if (!hasTrailingStar && currentTextPosition != end) { + int currentLength = current.length(); + return regExpRegionMatches(text, end - currentLength, current, 0, currentLength); + } + return segmentIndex == segmentCount; + } + + /** + * Parses the pattern into segments separated by wildcard '*' characters. + */ + private void parseWildCards() { + if (pattern.startsWith("*"))//$NON-NLS-1$ + hasLeadingStar = true; + if (pattern.endsWith("*")) {//$NON-NLS-1$ + /* make sure it's not an escaped wildcard */ + if (patternLength > 1 && pattern.charAt(patternLength - 2) != '\\') { + hasTrailingStar = true; + } + } + + ArrayList temp = new ArrayList(); + + int pos = 0; + StringBuffer buf = new StringBuffer(); + while (pos < patternLength) { + char c = pattern.charAt(pos++); + switch (c) { + case '\\' : + if (pos >= patternLength) { + buf.append(c); + } else { + char next = pattern.charAt(pos++); + /* if it's an escape sequence */ + if (next == '*' || next == '?' || next == '\\') { + buf.append(next); + } else { + /* not an escape sequence, just insert literally */ + buf.append(c); + buf.append(next); + } + } + break; + case '*' : + if (buf.length() > 0) { + /* new segment */ + temp.add(buf.toString()); + bound += buf.length(); + buf.setLength(0); + } + break; + case '?' : + /* append special character representing single match wildcard */ + buf.append(SINGLE_WILD_CARD); + break; + default : + buf.append(c); + } + } + + /* add last buffer to segment list */ + if (buf.length() > 0) { + temp.add(buf.toString()); + bound += buf.length(); + } + segments = (String[])temp.toArray(new String[temp.size()]); + } + + /** + * + * @return boolean + * @param text a String to match + * @param tStart the starting index of match, inclusive + * @param p a simple regular expression that may contain '?' + * @param pStart The start position in the pattern + * @param plen The length of the pattern + */ + private boolean regExpRegionMatches(String text, int tStart, String p, int pStart, int plen) { + while (plen-- > 0) { + char tchar = text.charAt(tStart++); + char pchar = p.charAt(pStart++); + + // process wild cards, skipping single wild cards + if (pchar == SINGLE_WILD_CARD) + continue; + if (pchar == tchar) + continue; + if (Character.toUpperCase(tchar) == Character.toUpperCase(pchar)) + continue; + // comparing after converting to upper case doesn't handle all cases; + // also compare after converting to lower case + if (Character.toLowerCase(tchar) == Character.toLowerCase(pchar)) + continue; + return false; + } + return true; + } +} \ No newline at end of file -- cgit v1.2.3