/*******************************************************************************
* Copyright (c) 2009, 2015, 2016 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
* Zend Technologies
*******************************************************************************/
package org.eclipse.php.core.compiler.ast.nodes;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.dltk.annotations.NonNull;
import org.eclipse.dltk.annotations.Nullable;
import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.ast.ASTVisitor;
import org.eclipse.dltk.ast.references.SimpleReference;
import org.eclipse.dltk.ast.references.TypeReference;
import org.eclipse.dltk.ast.references.VariableReference;
import org.eclipse.php.internal.core.Constants;
import org.eclipse.php.internal.core.typeinference.evaluators.FormalParameterEvaluator;
import org.eclipse.php.internal.core.util.MagicMemberUtil;
import org.eclipse.php.internal.core.util.text.PHPTextSequenceUtilities;
public class PHPDocTag extends ASTNode {
public enum TagKind {
ABSTRACT("abstract"), //$NON-NLS-1$
AUTHOR("author"), //$NON-NLS-1$
DEPRECATED("deprecated"), //$NON-NLS-1$
FINAL("final"), //$NON-NLS-1$
GLOBAL("global"), //$NON-NLS-1$
NAME("name"), //$NON-NLS-1$
RETURN("return"), //$NON-NLS-1$
PARAM("param"), //$NON-NLS-1$
SEE("see"), //$NON-NLS-1$
STATIC("static"), //$NON-NLS-1$
STATICVAR("staticvar"), //$NON-NLS-1$
TODO("todo"), //$NON-NLS-1$
VAR("var"), //$NON-NLS-1$
PACKAGE("package"), //$NON-NLS-1$
ACCESS("access"), //$NON-NLS-1$
CATEGORY("category"), //$NON-NLS-1$
COPYRIGHT("copyright"), //$NON-NLS-1$
DESC("desc"), //$NON-NLS-1$
EXAMPLE("example"), //$NON-NLS-1$
FILESOURCE("filesource"), //$NON-NLS-1$
IGNORE("ignore"), //$NON-NLS-1$
INTERNAL("internal"), //$NON-NLS-1$
LICENSE("license"), //$NON-NLS-1$
LINK("link"), //$NON-NLS-1$
SINCE("since"), //$NON-NLS-1$
SUBPACKAGE("subpackage"), //$NON-NLS-1$
TUTORIAL("tutorial"), //$NON-NLS-1$
USES("uses"), //$NON-NLS-1$
VERSION("version"), //$NON-NLS-1$
THROWS("throws"), //$NON-NLS-1$
PROPERTY("property"), //$NON-NLS-1$
PROPERTY_READ("property-read"), //$NON-NLS-1$
PROPERTY_WRITE("property-write"), //$NON-NLS-1$
METHOD("method"), //$NON-NLS-1$
NAMESPACE("namespace"), //$NON-NLS-1$
INHERITDOC("inheritdoc", "{@inheritdoc}"), //$NON-NLS-1$ //$NON-NLS-2$
EXCEPTION("exception"), //$NON-NLS-1$
MAGIC("magic"); //$NON-NLS-1$
String name;
String value;
private static final class Mapping {
private static final Map<Integer, TagKind> mapIds = new HashMap<Integer, TagKind>();
private static final Map<String, TagKind> mapNames = new TreeMap<String, TagKind>(
String.CASE_INSENSITIVE_ORDER);
private static final Map<String, TagKind> mapValues = new TreeMap<String, TagKind>(
String.CASE_INSENSITIVE_ORDER);
}
private TagKind(String name) {
this(name, '@' + name);
}
private TagKind(String name, String value) {
this.name = name;
this.value = value;
Mapping.mapIds.put(getId(), this);
Mapping.mapNames.put(this.name, this);
Mapping.mapValues.put(this.value, this);
}
// will never be null
@NonNull
public String getName() {
return name;
}
// will never be null
@NonNull
public String getValue() {
return value;
}
// for backward compatibility with PHPDocTagKinds
public int getId() {
// the ordinal value of a TagKind element
// must be the value of its corresponding
// PHPDocTagKinds property, so for example
// getTagKind(PROPERTY_READ).ordinal() is equal to
// PHPDocTagKinds.PROPERTY_READ
return ordinal();
}
// For backward compatibility with PHPDocTagKinds.
// Can be null.
@Nullable
public static TagKind getTagKind(int tagId) {
return Mapping.mapIds.get(tagId);
}
// Search a TagKind by its name.
// The search is case-insensitive.
// Can be null.
@Nullable
public static TagKind getTagKindFromName(String name) {
return Mapping.mapNames.get(name);
}
// Search a TagKind by its value.
// The search is case-insensitive.
// Can be null.
@Nullable
public static TagKind getTagKindFromValue(String value) {
return Mapping.mapValues.get(value);
}
}
private static final String ELLIPSIS_DOLLAR = FormalParameterEvaluator.ELLIPSIS + "$"; //$NON-NLS-1$
private final TagKind tagKind;
private final String matchedTag;
private String value;
private List<Scalar> texts;
private VariableReference variableReference;
private TypeReference singleTypeReference;
private List<TypeReference> typeReferences;
private List<SimpleReference> allReferencesWithOrigOrder;
private List<String> descTexts;
private String trimmedDescText;
public PHPDocTag(int start, int end, TagKind tag, String matchedTag, String value, List<Scalar> texts) {
super(start, end);
if (!(0 <= start && start <= end) || tag == null || matchedTag == null || value == null || texts == null) {
throw new IllegalArgumentException();
}
this.tagKind = tag;
this.matchedTag = matchedTag;
this.value = value;
this.texts = texts;
updateReferences(start, end);
}
/**
* Never null.
*/
@NonNull
public List<Scalar> getTexts() {
return texts;
}
/**
* Never null.
*/
@NonNull
public List<String> getDescTexts() {
return descTexts;
}
/**
* Never null.
*/
@NonNull
public String getTrimmedDescText() {
return trimmedDescText;
}
private String getTrimmedDescText(int wordsToSkip) {
String text = value.trim();
if (wordsToSkip == 0) {
return text;
}
String[] split = MagicMemberUtil.WHITESPACE_SEPERATOR.split(text);
for (int i = 0; i < wordsToSkip; i++) {
int index = text.indexOf(split[i]);
text = text.substring(split[i].length() + index);
}
return text.trim();
}
private List<String> getDescTexts(int wordsToSkip) {
List<String> result = new ArrayList<String>();
for (int i = 0; i < texts.size(); i++) {
String text = texts.get(i).getValue();
if (wordsToSkip <= 0) {
result.add(text);
} else {
if (StringUtils.isBlank(text)) {
continue;
}
List<String> commentWords = Arrays.asList(MagicMemberUtil.WHITESPACE_SEPERATOR.split(text.trim()));
commentWords = removeEmptyString(commentWords);
if (commentWords.size() <= wordsToSkip) {
wordsToSkip = wordsToSkip - commentWords.size();
} else {
text = removeFirstWords(text, commentWords, wordsToSkip);
result.add(text);
wordsToSkip = 0;
}
}
}
return result;
}
@NonNull
public static List<String> removeEmptyString(List<String> commentWords) {
List<String> result = new ArrayList<String>();
for (int i = 0; i < commentWords.size(); i++) {
String word = commentWords.get(i);
if (word.trim().length() != 0) {
result.add(word);
}
}
return result;
}
private String removeFirstWords(String text, List<String> commentWords, int wordSize) {
for (int i = 0; i < wordSize; i++) {
int index = text.indexOf(commentWords.get(i));
text = text.substring(commentWords.get(i).length() + index);
}
return text.trim();
}
private static int getClassStartIndex(String line, int startIndex) {
int i = startIndex;
for (; i < line.length(); ++i) {
if (line.charAt(i) != Constants.TYPE_SEPARATOR_CHAR) {
return i;
}
}
return i;
}
private static int getClassEndIndex(String line, int startIndex) {
int i = startIndex;
for (; i < line.length(); ++i) {
if (line.charAt(i) == Constants.TYPE_SEPARATOR_CHAR) {
return i;
}
}
return i;
}
private void splitSingleTypeReference(TypeReference reference, List<TypeReference> types) {
String word = reference.getName();
int valueStart = reference.sourceStart();
int classStart = getClassStartIndex(word, 0);
int classEnd = getClassEndIndex(word, classStart);
while (classStart < classEnd) {
String className = word.substring(classStart, classEnd);
types.add(new TypeReference(valueStart + classStart, valueStart + classEnd, className));
classStart = getClassStartIndex(word, classEnd);
classEnd = getClassEndIndex(word, classStart);
}
}
protected void updateReferences(int start, int end) {
// (re)set references
variableReference = null;
singleTypeReference = null;
typeReferences = new ArrayList<TypeReference>();
allReferencesWithOrigOrder = new ArrayList<SimpleReference>();
descTexts = new ArrayList<String>();
int valueStart = start + matchedTag.length();
// For all unsupported tags
int wordsToSkip = 0;
if (tagKind == TagKind.RETURN || tagKind == TagKind.VAR || tagKind == TagKind.THROWS
|| tagKind == TagKind.SEE) {
// Read first word
int wordStart = PHPTextSequenceUtilities.readForwardSpaces(value, 0);
int wordEnd = PHPTextSequenceUtilities.readForwardUntilSpaces(value, wordStart);
if (tagKind == TagKind.VAR && wordStart < wordEnd) {
String word = value.substring(wordStart, wordEnd);
if (word.charAt(0) == '$') {
variableReference = new VariableReference(valueStart + wordStart, valueStart + wordEnd, word);
allReferencesWithOrigOrder.add(variableReference);
wordsToSkip++;
// Read next word
wordStart = PHPTextSequenceUtilities.readForwardSpaces(value, wordEnd);
wordEnd = PHPTextSequenceUtilities.readForwardUntilSpaces(value, wordStart);
}
}
if (wordStart < wordEnd) {
String word = value.substring(wordStart, wordEnd);
singleTypeReference = new TypeReference(valueStart + wordStart, valueStart + wordEnd, word);
splitSingleTypeReference(singleTypeReference, typeReferences);
allReferencesWithOrigOrder.addAll(typeReferences);
wordsToSkip++;
// Read next word
wordStart = PHPTextSequenceUtilities.readForwardSpaces(value, wordEnd);
wordEnd = PHPTextSequenceUtilities.readForwardUntilSpaces(value, wordStart);
}
if (tagKind == TagKind.VAR && variableReference == null && wordStart < wordEnd) {
String word = value.substring(wordStart, wordEnd);
if (word.charAt(0) == '$') {
variableReference = new VariableReference(valueStart + wordStart, valueStart + wordEnd, word);
allReferencesWithOrigOrder.add(variableReference);
wordsToSkip++;
}
}
} else if (tagKind == TagKind.PARAM || tagKind == TagKind.PROPERTY || tagKind == TagKind.PROPERTY_READ
|| tagKind == TagKind.PROPERTY_WRITE) {
int firstWordStart = PHPTextSequenceUtilities.readForwardSpaces(value, 0);
int firstWordEnd = PHPTextSequenceUtilities.readForwardUntilSpaces(value, firstWordStart);
if (firstWordStart < firstWordEnd) {
int secondWordStart = PHPTextSequenceUtilities.readForwardSpaces(value, firstWordEnd);
int secondWordEnd = PHPTextSequenceUtilities.readForwardUntilSpaces(value, secondWordStart);
if (secondWordStart < secondWordEnd) {
String firstWord = value.substring(firstWordStart, firstWordEnd);
String secondWord = value.substring(secondWordStart, secondWordEnd);
if (firstWord.charAt(0) == '$' || firstWord.startsWith(ELLIPSIS_DOLLAR)) {
variableReference = new VariableReference(valueStart + firstWordStart,
valueStart + firstWordEnd, firstWord);
singleTypeReference = new TypeReference(valueStart + secondWordStart,
valueStart + secondWordEnd, secondWord);
splitSingleTypeReference(singleTypeReference, typeReferences);
allReferencesWithOrigOrder.add(variableReference);
allReferencesWithOrigOrder.addAll(typeReferences);
// The two words following the tag name were splitted
// into two references
wordsToSkip = 2;
} else if (secondWord.charAt(0) == '$' || secondWord.startsWith("...$")) { //$NON-NLS-1$
variableReference = new VariableReference(valueStart + secondWordStart,
valueStart + secondWordEnd, secondWord);
singleTypeReference = new TypeReference(valueStart + firstWordStart, valueStart + firstWordEnd,
firstWord);
splitSingleTypeReference(singleTypeReference, typeReferences);
allReferencesWithOrigOrder.addAll(typeReferences);
allReferencesWithOrigOrder.add(variableReference);
// The two words following the tag name were splitted
// into two references
wordsToSkip = 2;
}
}
}
}
descTexts = getDescTexts(wordsToSkip);
trimmedDescText = getTrimmedDescText(wordsToSkip);
}
public void traverse(ASTVisitor visitor) throws Exception {
boolean visit = visitor.visit(this);
if (visit) {
for (SimpleReference ref : allReferencesWithOrigOrder) {
ref.traverse(visitor);
}
}
visitor.endvisit(this);
}
public int getKind() {
return ASTNodeKinds.PHP_DOC_TAG;
}
/**
* @return a non-null TagKind element
*/
@NonNull
public TagKind getTagKind() {
return tagKind;
}
@NonNull
public String getMatchedTag() {
return matchedTag;
}
@NonNull
public String getValue() {
return value;
}
/**
* Variable reference, whose name is never empty or blank
*
* @return variable reference (if present), null otherwise
*/
public VariableReference getVariableReference() {
return variableReference;
}
/**
* Type reference, whose name is never empty or blank
*
* @return type reference (not splitted), null otherwise
*/
public TypeReference getSingleTypeReference() {
return singleTypeReference;
}
/**
* Type references, whose names are never empty or blank
*
* @return all type references, empty list otherwise
*/
@NonNull
public List<TypeReference> getTypeReferences() {
return typeReferences;
}
/**
* All references, whose names are never empty or blank
*
* @return all references, empty list otherwise
*/
@NonNull
public List<SimpleReference> getAllReferencesWithOrigOrder() {
return allReferencesWithOrigOrder;
}
public boolean isValidMethodDescriptorTag() {
return isValidParamTag() || isValidPropertiesTag();
}
public boolean isValidPropertiesTag() {
return (tagKind == TagKind.PROPERTY || tagKind == TagKind.PROPERTY_READ || tagKind == TagKind.PROPERTY_WRITE)
&& singleTypeReference != null && variableReference != null;
}
public boolean isValidParamTag() {
return tagKind == TagKind.PARAM && singleTypeReference != null && variableReference != null;
}
public boolean isValidVarTag() {
// NB: the variable reference is optional for @var tags
return tagKind == TagKind.VAR && singleTypeReference != null;
}
public void adjustStart(int start) {
setStart(sourceStart() + start);
setEnd(sourceEnd() + start);
for (Scalar text : texts) {
text.setStart(text.sourceStart() + start);
text.setEnd(text.sourceEnd() + start);
}
updateReferences(sourceStart(), sourceEnd());
}
}