/**
* 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 other free and open source software ("FOSS") 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.scriptdoc.parsing;
import java.text.ParseException;
import com.aptana.ide.core.StringUtils;
import com.aptana.ide.editor.js.parsing.JSMimeType;
import com.aptana.ide.editor.scriptdoc.lexing.ScriptDocTokenTypes;
import com.aptana.ide.editors.managers.FileContextManager;
import com.aptana.ide.editors.unified.folding.GenericCommentNode;
import com.aptana.ide.lexer.ILexer;
import com.aptana.ide.lexer.Lexeme;
import com.aptana.ide.lexer.LexemeList;
import com.aptana.ide.lexer.LexerException;
import com.aptana.ide.lexer.TokenCategories;
import com.aptana.ide.metadata.IDocumentation;
import com.aptana.ide.metadata.IDocumentationStore;
import com.aptana.ide.parsing.CodeLocation;
import com.aptana.ide.parsing.ErrorMessage;
import com.aptana.ide.parsing.IParseState;
import com.aptana.ide.parsing.ParserInitializationException;
import com.aptana.ide.parsing.nodes.IParseNode;
/**
* @author Robin Debreuil
*/
public class ScriptDocParser extends ScriptDocParserBase
{
/**
* INDENT_GROUP
*/
public static final String INDENT_GROUP = "indent"; //$NON-NLS-1$
private IDocumentation _rootNode;
private FunctionDocumentation _parsedObject;
private IParseNode _curParent;
private String _curScriptNamespace;
/**
* Parses ScriptDoc comments, converting them to IDocumentation objects.
*
* @throws ParserInitializationException
*/
public ScriptDocParser() throws ParserInitializationException
{
this(ScriptDocMimeType.MimeType);
}
/**
* Parses ScriptDoc comments, converting them to IDocumentation objects.
*
* @throws ParserInitializationException
*/
public ScriptDocParser(String mimeType) throws ParserInitializationException
{
super(mimeType);
this._curScriptNamespace = ""; //$NON-NLS-1$
}
/**
* @see com.aptana.ide.parsing.AbstractParser#parseAll(com.aptana.ide.parsing.nodes.IParseNode)
*/
public synchronized void parseAll(IParseNode parentNode) throws LexerException
{
this._curParent = parentNode;
this.startingIndex = -1;
this.endingIndex = -1;
// make sure our lexer is using our lexeme cache and switch over to our language and default group
ILexer lexer = this.getLexer();
lexer.setLanguageAndGroup(this.getLanguage(), "default"); //$NON-NLS-1$
try
{
this._parsedObject = this.parseDocumentation();
}
catch (ParseException e)
{
lexer.setCurrentOffset(lexer.getEOFOffset());
}
IParseState parseState = this.getParseState();
if (parseState instanceof ScriptDocParseState)
{
ScriptDocParseState scriptDocParseState = (ScriptDocParseState) parseState;
LexemeList lexemes = this.getLexemeList();
Lexeme lastLexeme = lexemes.get(lexemes.size() - 1); // will always have at least one
IDocumentationStore documentationStore = scriptDocParseState.getDocumentationStore();
documentationStore.addScriptDocObject(lexer.getCurrentOffset(), lastLexeme, this._parsedObject);
}
else
{
throw new IllegalStateException(Messages.ScriptDocParser_MustHaveScriptDocParseState);
}
if (endingIndex != -1 && startingIndex != -1)
{
GenericCommentNode node = new GenericCommentNode(startingIndex, endingIndex, "SDCOMMENT", //$NON-NLS-1$
JSMimeType.MimeType);
this.getParseState().addCommentRegion(node);
}
}
/**
* Returns the last parsed Object
*
* @return Returns the last parsed Object
*/
public FunctionDocumentation getParsedObject()
{
return this._parsedObject;
}
/**
* Parse the associated source input text ('source' must be set first).
*
* @return A FunctionDocumentation object from the parsed source.
* @throws ParseException
* @throws LexerException
* @throws LexerException
*/
private FunctionDocumentation parseDocumentation() throws ParseException, LexerException
{
FunctionDocumentation fd = new FunctionDocumentation();
this._rootNode = fd;
boolean wasNull = false;
if (this.currentLexeme == null)
{
advance(); // first parse, first time, with only script doc, can be null
wasNull = true;
}
// case where there were no previous lexemes (first lex in doc is /**).
if (this.currentLexeme == EOS)
{
advance();
if (this.currentLexeme != null && this.currentLexeme != EOS)
{
this._curNode = new ScriptDocParseNode(this.currentLexeme);
this._curNode.includeLexemeInRange(this.currentLexeme);
}
else
{
return fd;
}
}
else
{
this._curNode = new ScriptDocParseNode(this.currentLexeme);
if (!wasNull)
{
advance();
}
}
if (this.currentLexeme != EOS)
{
if (this._curParent != null)
{
this._curParent.appendChild(this._curNode);
}
this._curNode.setDocument(this._parsedObject);
// make sure first token is a start doc
assertAndAdvance(ScriptDocTokenTypes.START_DOCUMENTATION);
// get description if any
fd.setDescription(parseText());
// now parse all the parameters
while (this._holderLexeme != EOS)
{
this.parseFunctionDocumentationSection(fd);
}
}
return fd;
}
/**
* Set the source code to parse
*
* @param source
* The source of this Documentation.
*/
public void setSource(String source)
{
this.getLexer().setSource(source);
}
/**
* parseBaseTags
*
* @param bd
* @return boolean
* @throws LexerException
*/
private boolean parseBaseTags(DocumentationBase bd) throws LexerException
{
boolean result = false;
if (this._holderLexeme.getCategoryIndex() == TokenCategories.KEYWORD)
{
try
{
switch (this._holderLexeme.typeIndex)
{
case ScriptDocTokenTypes.ADVANCED:
advance();
result = true;
break;
case ScriptDocTokenTypes.AUTHOR:
advance();
bd.setAuthor(parseText());
result = true;
break;
case ScriptDocTokenTypes.VERSION:
advance();
bd.setVersion(parseText());
result = true;
break;
case ScriptDocTokenTypes.SEE:
advance();
bd.addSee(parseText());
result = true;
break;
case ScriptDocTokenTypes.SDOC:
advance();
bd.addSDocLocation(parseText());
result = true;
break;
case ScriptDocTokenTypes.NAMESPACE:
advance();
parseNamespace();
result = true;
break;
case ScriptDocTokenTypes.COPYRIGHT:
advance();
parseText(); // ignore for now
result = true;
break;
case ScriptDocTokenTypes.LICENSE:
advance();
parseText(); // ignore for now
result = true;
break;
case ScriptDocTokenTypes.EXAMPLE:
advance();
bd.addExample(parseText());
result = true;
break;
case ScriptDocTokenTypes.OVERVIEW:
advance();
this.parseText();
result = true;
break;
// adding project description here to make the parse generic
case ScriptDocTokenTypes.PROJECT_DESCRIPTION:
advance(); // there is actually a tag for this description (@projectDescription).
bd.setDescription(parseText());
bd.setDocumentType(IDocumentation.TYPE_PROJECT);
result = true;
default:
break;
}
}
catch (ParseException e)
{
skipTag(e);
}
}
else
{
try
{
String desc = parseText();
// We may already have a good description--don't mess it up
if (!StringUtils.EMPTY.equals(desc))
{
bd.setDescription(desc);
}
}
catch (ParseException e)
{
skipTag(e);
}
}
return result;
}
/**
* parsePropertyTags
*
* @param pd
* @return boolean
* @throws LexerException
*/
private boolean parsePropertyTags(PropertyDocumentation pd) throws LexerException
{
boolean found = parseBaseTags(pd);
if (found)
{
return true;
}
boolean result = false;
if (found == false && this._holderLexeme.getCategoryIndex() == TokenCategories.KEYWORD)
{
try
{
switch (this._holderLexeme.typeIndex)
{
case ScriptDocTokenTypes.MEMBER_OF:
advance();
parseMemberOf(pd);
result = true;
break;
case ScriptDocTokenTypes.IGNORE:
advance();
pd.setIsIgnored(true);
result = true;
break;
case ScriptDocTokenTypes.TYPE:
advance();
parseTypeValue(pd);
pd.setDocumentType(IDocumentation.TYPE_PROPERTY);
result = true;
break;
case ScriptDocTokenTypes.SINCE:
advance();
pd.setSince(parseText());
result = true;
break;
case ScriptDocTokenTypes.DEPRECATED:
advance();
pd.setIsDeprecated(true);
pd.setDeprecatedDescription(parseText());
result = true;
break;
case ScriptDocTokenTypes.PRIVATE:
advance();
pd.setIsPrivate(true);
result = true;
break;
case ScriptDocTokenTypes.PROPERTY:
advance();
pd.setDocumentType(IDocumentation.TYPE_PROPERTY);
break;
case ScriptDocTokenTypes.INTERNAL:
advance();
pd.setIsInternal(true);
result = true;
break;
case ScriptDocTokenTypes.NATIVE:
advance();
pd.setIsNative(true);
result = true;
break;
case ScriptDocTokenTypes.ALIAS:
advance();
parseAlias(pd);
result = true;
break;
case ScriptDocTokenTypes.ID:
advance();
parseID(pd);
result = true;
break;
default:
break;
}
}
catch (ParseException e)
{
skipTag(e);
}
}
return result;
}
/**
* parseFunctionDocumentationSection
*
* @param fd
* @throws LexerException
*/
private void parseFunctionDocumentationSection(FunctionDocumentation fd) throws LexerException
{
boolean found = parsePropertyTags(fd);
if (found)
{
return;
}
if (this._holderLexeme.getCategoryIndex() == TokenCategories.KEYWORD)
{
try
{
switch (this._holderLexeme.typeIndex)
{
case ScriptDocTokenTypes.PARAM:
advance();
parseParams(fd);
fd.setDocumentType(IDocumentation.TYPE_FUNCTION);
break;
case ScriptDocTokenTypes.RETURN:
advance();
parseReturnValue(fd);
fd.setDocumentType(IDocumentation.TYPE_FUNCTION);
break;
case ScriptDocTokenTypes.EXCEPTION:
advance();
parseException(fd);
break;
case ScriptDocTokenTypes.CLASS_DESCRIPTION:
advance();
fd.setClassDescription(parseText());
break;
case ScriptDocTokenTypes.CONSTRUCTOR:
advance();
fd.setIsConstructor(true);
fd.setDocumentType(IDocumentation.TYPE_FUNCTION);
break;
case ScriptDocTokenTypes.METHOD:
advance();
fd.setIsMethod(true);
fd.setDocumentType(IDocumentation.TYPE_FUNCTION);
fd.setMethodName(parseText());
break;
case ScriptDocTokenTypes.EXTENDS:
advance();
parseExtends(fd);
fd.setDocumentType(IDocumentation.TYPE_FUNCTION);
break;
case ScriptDocTokenTypes.IGNORE:
advance();
fd.setIsIgnored(true);
break;
default:
skipTag(new ParseException(Messages.ScriptDocParser_InvalidSyntax
+ this._holderLexeme.getType(), 0));
break;
}
}
catch (ParseException e)
{
skipTag(e);
}
}
else if (this._holderLexeme.typeIndex == ScriptDocTokenTypes.END_DOCUMENTATION)
{
this._curNode.includeLexemeInRange(this.currentLexeme);
advance();
}
else
{
skipTag(new ParseException(Messages.ScriptDocParser_InvalidSyntaxForStatement
+ this._holderLexeme.getType(), 0));
}
}
/**
* parseAlias
*
* @param pd
* @throws ParseException
* @throws LexerException
*/
private void parseAlias(PropertyDocumentation pd) throws ParseException, LexerException
{
pd.getAliases().addType(getIdentifier());
}
/**
* parseID
*
* @param pd
* @throws ParseException
* @throws LexerException
*/
private void parseID(PropertyDocumentation pd) throws ParseException, LexerException
{
LexemeList lexemes = this.getLexemeList();
Lexeme start = this.currentLexeme;
String id = getIdentifier();
Lexeme end = lexemes.get(lexemes.size() - 1);
String dot = this._curScriptNamespace.equals("") ? "" : "."; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
String fullname = this._curScriptNamespace + dot + id;
String uri = FileContextManager.getURIFromFileIndex(this.getParseState().getFileIndex());
pd.setID(fullname, new CodeLocation(uri, start, end));
}
/**
* parseNamespace
*
* @throws ParseException
* @throws LexerException
*/
private void parseNamespace() throws ParseException, LexerException
{
this._curScriptNamespace = getIdentifier();
// TODO: need to allow namespace definitions to describe themselves (so save this text).
this.parseText();
}
/**
* parseReturnValue
*
* @param pd
* @throws ParseException
* @throws LexerException
*/
private void parseReturnValue(PropertyDocumentation pd) throws ParseException, LexerException
{
parseIntoTypedDescription(pd.getReturn(), false);
}
/**
* parseTypeValue
*
* @param pd
* @throws ParseException
* @throws LexerException
*/
private void parseTypeValue(PropertyDocumentation pd) throws ParseException, LexerException
{
boolean hasType = this._holderLexeme.typeIndex == ScriptDocTokenTypes.LCURLY;
boolean isIdent = (this._holderLexeme.typeIndex == ScriptDocTokenTypes.IDENTIFIER)
|| (this._holderLexeme.typeIndex == ScriptDocTokenTypes.TEXT)
|| (this._holderLexeme.typeIndex == ScriptDocTokenTypes.ELLIPSIS);
if (hasType || !isIdent)
{
// this is the normal sdoc case
// @type {type} description or
// @type description
parseIntoTypedDescription(pd.getReturn(), false);
}
else
{
// this is the jsdoc case
// @type type
// but still might be the sdoc
// @type description
// we will choose the jsdoc way if there is only a single identifier compatible word (ex. YAHOO.xxx)
String type = getIdentifier();
boolean hasFollowText = (this._holderLexeme.typeIndex == ScriptDocTokenTypes.IDENTIFIER)
|| (this._holderLexeme.typeIndex == ScriptDocTokenTypes.TEXT);
if (hasFollowText)
{
String text = type + parseText();
pd.getReturn().setDescription(text);
}
else
{
pd.getReturn().addType(type);
}
}
}
/**
* parseParams
*
* @param fd
* @throws ParseException
* @throws LexerException
*/
private void parseParams(FunctionDocumentation fd) throws ParseException, LexerException
{
TypedDescription td = new TypedDescription();
parseIntoTypedDescription(td, true);
fd.addParam(td);
}
/**
* parseExtends
*
* @param fd
* @throws ParseException
* @throws LexerException
*/
private void parseExtends(FunctionDocumentation fd) throws ParseException, LexerException
{
parseIntoTypedDescription(fd.getExtends(), false);
}
/**
* parseException
*
* @param fd
* @throws ParseException
* @throws LexerException
*/
private void parseException(FunctionDocumentation fd) throws ParseException, LexerException
{
TypedDescription td = new TypedDescription();
parseIntoTypedDescription(td, false);
fd.addException(td);
}
/**
* parseMemberOf
*
* @param pd
* @throws ParseException
* @throws LexerException
*/
private void parseMemberOf(PropertyDocumentation pd) throws ParseException, LexerException
{
parseIntoTypedDescription(pd.getMemberOf(), false);
}
/**
* parseIntoTypedDescription
*
* @param td
* @param includeName
* @throws ParseException
* @throws LexerException
*/
private void parseIntoTypedDescription(TypedDescription td, boolean includeName) throws ParseException,
LexerException
{
// curlies are always optional
boolean hasType = this._holderLexeme.typeIndex == ScriptDocTokenTypes.LCURLY;
if (hasType)
{
assertAndAdvance(ScriptDocTokenTypes.LCURLY);
if (this._holderLexeme.typeIndex == ScriptDocTokenTypes.IDENTIFIER
|| this._holderLexeme.typeIndex == ScriptDocTokenTypes.ELLIPSIS)
{
// identifier includes dots
td.addType(getIdentifier());
// advance();
}
else
{
throwParseError(Messages.ScriptDocParser_InvalidID);
}
while (this._holderLexeme != EOS
&& (this._holderLexeme.typeIndex == ScriptDocTokenTypes.COMMA
|| this._holderLexeme.typeIndex == ScriptDocTokenTypes.PIPE || this._holderLexeme.typeIndex == ScriptDocTokenTypes.FORWARD_SLASH))
{
advance();
td.addType(getIdentifier());
}
assertAndAdvance(ScriptDocTokenTypes.RCURLY);
}
// add name, if any
if (includeName)
{
td.setName(getIdentifier());
}
// add text
td.setDescription(parseText());
}
/**
* getIdentifier
*
* @return String
* @throws ParseException
* @throws LexerException
*/
private String getIdentifier() throws ParseException, LexerException
{
String result = ""; //$NON-NLS-1$
boolean isOptional = false;
if (this._holderLexeme.typeIndex == ScriptDocTokenTypes.LBRACKET)
{
isOptional = true;
result += "["; //$NON-NLS-1$
advance();
}
if (this._holderLexeme.typeIndex == ScriptDocTokenTypes.IDENTIFIER
|| this._holderLexeme.typeIndex == ScriptDocTokenTypes.TEXT)
{
result += this._holderLexeme.getText();
advance();
}
else if (this._holderLexeme.typeIndex == ScriptDocTokenTypes.ELLIPSIS)
{
result = this._holderLexeme.getText();
advance();
}
else
{
throwParseError(Messages.ScriptDocParser_InvalidIdInComment);
}
if (isOptional)
{
result += "]"; //$NON-NLS-1$
assertAndAdvance(ScriptDocTokenTypes.RBRACKET);
}
return result;
}
/**
* parseText
*
* @return String
* @throws ParseException
* @throws LexerException
*/
private String parseText() throws ParseException, LexerException
{
StringBuilder text = new StringBuilder();
loop: while (this._holderLexeme != EOS)
{
switch (this._holderLexeme.getCategoryIndex())
{
case TokenCategories.WHITESPACE:
break;
case TokenCategories.LITERAL:
String hText = this._holderLexeme.getText();
if (hText.startsWith("@")) //$NON-NLS-1$
{
break loop;
}
text.append(hText);
text.append(" "); //$NON-NLS-1$
break;
case TokenCategories.KEYWORD:
// some keywords will be allowed (ex. @link)
if (this._holderLexeme.typeIndex == ScriptDocTokenTypes.LINK
|| this._holderLexeme.typeIndex == ScriptDocTokenTypes.SINCE)
{
text.append(this._holderLexeme.getText());
}
else
{
break loop;
}
break;
case TokenCategories.PUNCTUATOR:
if (this._holderLexeme.typeIndex == ScriptDocTokenTypes.END_DOCUMENTATION)
{
this._curNode.includeLexemeInRange(this.currentLexeme);
break loop;
}
else
{
text.append(this._holderLexeme.getText());
text.append(" "); //$NON-NLS-1$
}
break;
default:
text.append(this._holderLexeme.getText());
text.append(" "); //$NON-NLS-1$
break;
}
advance();
}
return text.toString();
}
/**
* getFollowText
*
* @return String
*/
private String getFollowText()
{
ILexer lexer = this.getLexer();
if (lexer.getSource().length() > lexer.getCurrentOffset())
{
int len = Math.min(6, lexer.getSource().length() - lexer.getCurrentOffset());
return "\"" + lexer.getSource().substring(lexer.getCurrentOffset(), lexer.getCurrentOffset() + len) + "\""; //$NON-NLS-1$ //$NON-NLS-2$
}
else
{
return "end of document"; //$NON-NLS-1$
}
}
/**
* assertAndAdvance
*
* @param type
* @throws ParseException
* @throws LexerException
*/
private void assertAndAdvance(int type) throws ParseException, LexerException
{
this.assertType(type);
this.advance();
}
/**
* assertType
*
* @param type
* @throws ParseException
*/
private void assertType(int type) throws ParseException
{
if (this._holderLexeme.typeIndex != type)
{
String targetType = ScriptDocTokenTypes.getName(type);
String actualType = ScriptDocTokenTypes.getName(this._holderLexeme.typeIndex);
if (this._holderLexeme == EOS)
{
actualType = getFollowText();
}
this.throwParseError(Messages.ScriptDocParser_Expected + targetType + Messages.ScriptDocParser_Found
+ actualType);
}
}
/**
* Advance until next valid section
*
* @throws LexerException
* @throws LexerException
*/
private void skipTag(ParseException e) throws LexerException
{
LexemeList lexemes = this.getLexemeList();
Lexeme targetLexeme = null;
// skip until next valid keyword or eos
if (this._holderLexeme == EOS && lexemes.size() > 1)
{
targetLexeme = lexemes.get(lexemes.size() - 2);
// en.add();
}
else
{
// en.add(curLexeme);
advance();
while (this._holderLexeme != EOS)
{
targetLexeme = this._holderLexeme;
if (this._holderLexeme.getCategoryIndex() != TokenCategories.KEYWORD)
{
// curLexeme.setCommandNode(en);
// en.add(curLexeme);
advance();
}
else
{
if (this._holderLexeme.typeIndex == ScriptDocTokenTypes.LINK)
{
// curLexeme.setCommandNode(en);
// en.add(curLexeme);
advance();
}
else
{
break;
}
}
}
}
ErrorMessage en = new ErrorMessage(e.getMessage(), targetLexeme);
if (e != null)
{
this._rootNode.addError(en);
}
}
/**
* Throw a parse exception
*
* @param message
* The exception message
* @throws ParseException
*/
protected void throwParseError(String message) throws ParseException
{
// determine line number
LexemeList lexemes = this.getLexemeList();
int lastValid = (this._holderLexeme == EOS) ? lexemes.size() - 2 : lexemes.size() - 1;
if (lastValid < 0)
{
message = Messages.ScriptDocParser_PrematureEndOfDoc;
}
else
{
String position;
if (this._holderLexeme != EOS)
{
position = " [" + this._holderLexeme.getText() + "]"; //$NON-NLS-1$ //$NON-NLS-2$
}
else
{
position = " [" + getFollowText() + "]"; //$NON-NLS-1$ //$NON-NLS-2$
}
message = Messages.ScriptDocParser_ParseError + position + ": " + message; //$NON-NLS-1$
}
throw new ParseException(message, -1);
}
}