/**
* This file Copyright (c) 2005-2008 Aptana, Inc. This program is
* dual-licensed under both the Aptana Public License and the GNU General
* Public license. You may elect to use one or the other of these licenses.
*
* This program is distributed in the hope that it will be useful, but
* AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or
* NONINFRINGEMENT. Redistribution, except as permitted by whichever of
* the GPL or APL you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or modify this
* program under the terms of the GNU General Public License,
* Version 3, as published by the Free Software Foundation. You should
* have received a copy of the GNU General Public License, Version 3 along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Aptana provides a special exception to allow redistribution of this file
* with certain Eclipse Public Licensed code and certain additional terms
* pursuant to Section 7 of the GPL. You may view the exception and these
* terms on the web at http://www.aptana.com/legal/gpl/.
*
* 2. For the Aptana Public License (APL), this program and the
* accompanying materials are made available under the terms of the APL
* v1.0 which accompanies this distribution, and is available at
* http://www.aptana.com/legal/apl/.
*
* You may view the GPL, Aptana's exception and additional terms, and the
* APL in the file titled license.html at the root of the corresponding
* plugin containing this source file.
*
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.ide.editor.js.validator;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import com.aptana.ide.core.IdeLog;
import com.aptana.ide.editor.js.JSPlugin;
import com.aptana.ide.editor.js.lexing.JSTokenTypes;
import com.aptana.ide.editor.js.parsing.JSMimeType;
import com.aptana.ide.editor.js.parsing.JSParser;
import com.aptana.ide.editor.js.preferences.IPreferenceConstants;
import com.aptana.ide.editors.unified.LanguageRegistry;
import com.aptana.ide.editors.unified.errors.IFileError;
import com.aptana.ide.editors.unified.errors.UnifiedErrorReporter;
import com.aptana.ide.lexer.Lexeme;
import com.aptana.ide.lexer.LexemeList;
import com.aptana.ide.lexer.LexerException;
import com.aptana.ide.lexer.Range;
import com.aptana.ide.parsing.IParseState;
import com.aptana.ide.parsing.IParser;
import com.aptana.ide.parsing.ParserInitializationException;
/**
* Utils for JS validation.
*
* @author munch
*/
public final class JSValidationUtils
{
/**
* Java Script "//validate" comment.
*/
private static final String JS_VALIDATE_COMMENT = "validate"; //$NON-NLS-1$
/**
* Java Script "//novalidate" comment.
*/
private static final String JS_NOVALIDATE_COMMENT = "novalidate"; //$NON-NLS-1$
private static JSParser parser;
private static IParseState parseState;
/**
* Filters errors by "novalidate" comment areas.
*
* @param errors -
* error to filter. must be sorted by position in ascending order.
* @param source -
* JS source code.
* @return filtered errors, sorted by position in ascending order.
* @throws ParserInitializationException
* IF parsing throws this exception.
* @throws LexerException
* IF parsing throws this exception.
*/
public static IFileError[] filterErrorsByNovalidate(IFileError[] errors, String source)
throws ParserInitializationException, LexerException
{
// make sure we have a parser
if (parser == null)
{
parser = new JSParser();
}
// make sure we have a parse state
if (parseState == null)
{
parseState = parser.createParseState(null);
}
// apply edit
parseState.setEditState(source, source, 0, 0);
// parse
parser.parse(parseState);
// grab lexemes
LexemeList lexemes = parseState.getLexemeList();
return filterErrorsByNovalidate(errors, lexemes, source);
}
static ArrayList<ICleanup> cleanup = new ArrayList<ICleanup>();
static
{
IConfigurationElement[] configurationElementsFor = Platform.getExtensionRegistry().getConfigurationElementsFor(
"com.aptana.ide.editor.js.languageCleanup"); //$NON-NLS-1$
for (IConfigurationElement e : configurationElementsFor)
{
try
{
ICleanup createExecutableExtension = (ICleanup) e.createExecutableExtension("class"); //$NON-NLS-1$
cleanup.add(createExecutableExtension);
}
catch (CoreException e1)
{
IdeLog.log(JSPlugin.getDefault(), IStatus.ERROR, "unable to instantiate cleanup object", e1); //$NON-NLS-1$
}
}
}
/**
* @param source
* @param reporter
* @param ranges
* @return filtered errors
*/
protected static IFileError[] filterErrors(String source, UnifiedErrorReporter reporter, ArrayList<Range> ranges)
{
IFileError[] errors = reporter.getErrors();
if (JSPlugin.getDefault().getPreferenceStore().getBoolean(IPreferenceConstants.ENABLE_NO_VALIDATE_COMMENT))
{
IParser ps = LanguageRegistry.getParser(JSMimeType.MimeType);
IParseState createParseState = ps.createParseState(null);
createParseState.setEditState(source, source, 0, 0);
ArrayList<IFileError> ls = new ArrayList<IFileError>();
try
{
ps.parse(createParseState);
}
catch (RuntimeException e1)
{
IdeLog.logError(JSPlugin.getDefault(), "parse exception while lexing cleared JS source", e1); //$NON-NLS-1$
}
catch (LexerException e1)
{
IdeLog.logError(JSPlugin.getDefault(), "lexing exception while lexing cleared JS source", e1); //$NON-NLS-1$
}
catch (java.text.ParseException e)
{
IdeLog.logError(JSPlugin.getDefault(), "parse exception while lexing cleared JS source", e); //$NON-NLS-1$
}
LexemeList lexemeList = createParseState.getLexemeList();
l2: for (IFileError e : errors)
{
for (Range r : ranges)
{
Lexeme ceilingLexeme = lexemeList.getCeilingLexeme(r.getEndingOffset());
int le = ceilingLexeme.getEndingOffset() + 1;
if (e.getOffset() < le + 1 && e.getOffset() > r.getStartingOffset())
{
continue l2;
}
}
ls.add(e);
}
errors = new IFileError[ls.size()];
ls.toArray(errors);
}
return errors;
}
/**
* @param source
* @param toFill
* collection for storing not js ranges
* @return - String
* @throws LexerException
* @throws ParserInitializationException
*/
public static String filterPIInstructions(String source, ArrayList<Range> toFill) throws LexerException,
ParserInitializationException
{
StringBuilder bld = new StringBuilder(source);
for (ICleanup c : cleanup)
{
List<Range> notJsCode = c.getNotJsCode(source);
for (Range r : notJsCode)
{
toFill.add(r);
StringBuilder spaces = new StringBuilder();
int startingOffset = r.getStartingOffset();
int endingOffset = r.getEndingOffset();
boolean found=false;
for (int a=startingOffset;a>=0;a--){
char ch=source.charAt(a);
if (ch=='\r'||ch=='\n'){
startingOffset=a;
found=true;
break;
}
}
if (!found){
startingOffset=0;
}
found=false;
for (int a=endingOffset;a<source.length();a++){
char ch=source.charAt(a);
if (ch=='\r'||ch=='\n'){
endingOffset=a;
found=true;
break;
}
}
if (!found){
endingOffset=source.length()-1;
}
for (int a = startingOffset; a < endingOffset; a++)
{
spaces.append(' ');
}
bld.replace(startingOffset, endingOffset, spaces.toString());
}
}
return bld.toString();
}
/**
* Filters errors by "novalidate" comment areas.
*
* @param errors -
* error to filter. must be sorted by position in ascending order.
* @param lexemes -
* lexemes list.
* @param source -
* source.
* @return filtered errors, sorted by position in ascending order.
*/
public static IFileError[] filterErrorsByNovalidate(IFileError[] errors, LexemeList lexemes, String source)
{
if (lexemes.size() < 2)
{
return errors;
}
List<IFileError> filteredErrors = new LinkedList<IFileError>();
// current unchecked error index
int currentErrorIndex = 0;
// validation state
boolean validating = true;
// start index of the area under validation
int validateStartIndex = 0;
for (int i = 0; i < lexemes.size() - 1; i++)
{
Lexeme currentLexeme = lexemes.get(i);
Lexeme nextLexeme = lexemes.get(i + 1);
if (currentLexeme.typeIndex == JSTokenTypes.COMMENT && nextLexeme.typeIndex == JSTokenTypes.CDO
&& nextLexeme.getText() != null)
{
if (nextLexeme.getText().startsWith(JS_NOVALIDATE_COMMENT) && validating)
{
// entering novalidate area
validating = false;
// copying errors from the area under validation to the result list
currentErrorIndex = copyErrorsFromArea(errors, filteredErrors, currentErrorIndex,
validateStartIndex, currentLexeme.getStartingOffset());
}
else if (nextLexeme.getText().startsWith(JS_VALIDATE_COMMENT) && !validating)
{
// leaving novalidate area
validating = true;
// novalidate area ended, validate area started
validateStartIndex = currentLexeme.getEndingOffset();
// skipping all errors that belongs to the same line, current lexeme does
// if line contains whitespaces only
currentErrorIndex = skipEmptyLineErrors(currentErrorIndex, errors, nextLexeme, source);
}
}
}
if (validating)
{
// copying errors from the area under validation to the result list
copyErrorsFromArea(errors, filteredErrors, currentErrorIndex, validateStartIndex, lexemes
.getAffectedRegion().getEndingOffset());
}
IFileError[] toReturn = new IFileError[filteredErrors.size()];
filteredErrors.toArray(toReturn);
return toReturn;
}
/**
* Skipping all errors that belongs to the same line, current lexeme does if line contains whitespaces only.
*
* @param errorIndex -
* error index to start with.
* @param errors -
* errors.
* @param lexeme -
* current lexeme.
* @param source -
* source
* @return new error index
*/
private static int skipEmptyLineErrors(int errorIndex, IFileError[] errors, Lexeme lexeme, String source)
{
int endLineOffset = getEndLineIndex(lexeme.getEndingOffset(), source);
// checking if only whitespace symbols are between lexeme end and new line
for (int i = lexeme.getEndingOffset(); i < endLineOffset; i++)
{
char ch = source.charAt(i);
// non-whitespace met. getting out.
if (!Character.isWhitespace(ch))
{
return errorIndex;
}
}
// skipping errors that are between lexeme end and newline
for (int i = errorIndex; i < errors.length; i++)
{
IFileError currentError = errors[i];
if (currentError.getOffset() >= endLineOffset)
{
return i;
}
}
return errors.length;
}
/**
* Gets closest new line of EOF offset.
*
* @param startOffset -
* offset to start search from.
* @param source -
* source.
* @return closest new line of end of file offset.
*/
private static int getEndLineIndex(int startOffset, String source)
{
StringReader reader = new StringReader(source);
try
{
reader.skip(startOffset);
int currentChar;
int currentOffset = 0;
int lineNumber = 1;
while ((currentChar = reader.read()) != -1)
{
switch (currentChar)
{
case '\r':
reader.mark(1);
int nextChar = reader.read();
currentOffset++;
if (nextChar != '\n')
{
reader.reset();
currentOffset--;
}
case '\n':
if (currentOffset + 1 < source.length())
{
return currentOffset + startOffset + 1;
}
lineNumber++;
break;
default:
break;
}
currentOffset++;
}
}
catch (IOException ex)
{
// should not happen
IdeLog.logError(JSPlugin.getDefault(), "Exception searching for the line end", ex); //$NON-NLS-1$
}
return source.length();
}
/**
* Copies errors that belong to the area starting from the index specified.
*
* @param from -
* errors source.
* @param to -
* errors destination.
* @param errorsStartIndex -
* errors start index.
* @param areaStartIndex -
* area start index.
* @param areaEndIndex -
* area end index.
* @return index of the first error that was out of the area.
*/
private static int copyErrorsFromArea(IFileError[] from, List<IFileError> to, int errorsStartIndex, int areaStartIndex,
int areaEndIndex)
{
if (errorsStartIndex < from.length)
{
int errorIndex = errorsStartIndex;
for (; errorIndex < from.length; errorIndex++)
{
IFileError currentError = from[errorIndex];
if (currentError.getOffset() >= areaStartIndex)
{
if (currentError.getOffset() <= areaEndIndex)
{
// adding error
to.add(currentError);
}
else
{
// next error is out of scope
break;
}
}
}
errorsStartIndex = errorIndex;
}
return errorsStartIndex;
}
/**
* JSValidationUtils private constructor.
*/
private JSValidationUtils()
{
}
}