| /******************************************************************************* |
| * Copyright (c) 2010 xored software, Inc. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * xored software, Inc. - initial API and Implementation (Alex Panchenko) |
| *******************************************************************************/ |
| package org.eclipse.dltk.internal.javascript.ti; |
| |
| import java.text.ParseException; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| |
| import org.antlr.runtime.ANTLRStringStream; |
| import org.antlr.runtime.CharStream; |
| import org.eclipse.dltk.annotations.NonNull; |
| import org.eclipse.dltk.annotations.Nullable; |
| import org.eclipse.dltk.ast.ASTNode; |
| import org.eclipse.dltk.compiler.problem.IProblemCategory; |
| import org.eclipse.dltk.compiler.problem.IProblemIdentifier; |
| import org.eclipse.dltk.compiler.problem.ProblemCategoryManager; |
| import org.eclipse.dltk.javascript.ast.BinaryOperation; |
| import org.eclipse.dltk.javascript.ast.CallExpression; |
| import org.eclipse.dltk.javascript.ast.Comment; |
| import org.eclipse.dltk.javascript.ast.FunctionStatement; |
| import org.eclipse.dltk.javascript.ast.IVariableStatement; |
| import org.eclipse.dltk.javascript.ast.JSNode; |
| import org.eclipse.dltk.javascript.ast.PropertyExpression; |
| import org.eclipse.dltk.javascript.ast.PropertyInitializer; |
| import org.eclipse.dltk.javascript.ast.Statement; |
| import org.eclipse.dltk.javascript.ast.VariableDeclaration; |
| import org.eclipse.dltk.javascript.ast.VariableStatement; |
| import org.eclipse.dltk.javascript.core.JavaScriptLanguageUtil; |
| import org.eclipse.dltk.javascript.core.JavaScriptNature; |
| import org.eclipse.dltk.javascript.parser.JSParser; |
| import org.eclipse.dltk.javascript.parser.JSProblemIdentifier; |
| import org.eclipse.dltk.javascript.parser.JSProblemReporter; |
| import org.eclipse.dltk.javascript.parser.ProblemReporter; |
| import org.eclipse.dltk.javascript.parser.jsdoc.JSDocTag; |
| import org.eclipse.dltk.javascript.parser.jsdoc.JSDocTags; |
| import org.eclipse.dltk.javascript.parser.jsdoc.SimpleJSDocParser; |
| import org.eclipse.dltk.javascript.typeinference.ReferenceLocation; |
| import org.eclipse.dltk.javascript.typeinfo.IModelBuilder; |
| import org.eclipse.dltk.javascript.typeinfo.ITypeChecker; |
| import org.eclipse.dltk.javascript.typeinfo.ITypeInfoContext; |
| import org.eclipse.dltk.javascript.typeinfo.JSDocParseException; |
| import org.eclipse.dltk.javascript.typeinfo.JSDocTypeParser; |
| import org.eclipse.dltk.javascript.typeinfo.model.JSType; |
| import org.eclipse.dltk.javascript.typeinfo.model.Member; |
| import org.eclipse.dltk.javascript.typeinfo.model.ParameterKind; |
| import org.eclipse.dltk.javascript.typeinfo.model.RecordProperty; |
| import org.eclipse.dltk.javascript.typeinfo.model.RecordType; |
| import org.eclipse.dltk.javascript.typeinfo.model.TypeInfoModelFactory; |
| import org.eclipse.dltk.javascript.typeinfo.model.Visibility; |
| import org.eclipse.emf.common.util.EList; |
| import org.eclipse.osgi.util.NLS; |
| |
| /** |
| * Implements support for javadocs tags . |
| * |
| * @see http://jsdoc.sourceforge.net/ |
| * @see http://code.google.com/p/jsdoc-toolkit/wiki/TagType |
| * @see http://code.google.com/p/jsdoc-toolkit/wiki/TagParam |
| */ |
| public class JSDocSupport implements IModelBuilder { |
| |
| public static final String PARAM_DOTS = "..."; |
| public static final String PARAM_OPTIONAL = "="; |
| |
| public String getFeatureId() { |
| return JSDocSupport.class.getName(); |
| } |
| |
| public int priorityFor(ITypeInfoContext context) { |
| return PRIORITY_DEFAULT; |
| } |
| |
| public static JSDocTags parse(Comment comment) { |
| return new SimpleJSDocParser().parse(comment.getText(), |
| comment.sourceStart()); |
| } |
| |
| public enum JSDocFunctionContext { |
| DECLARATION { |
| @Override |
| String[] getReturnTags() { |
| return JSDocTag.RETURN_TAGS; |
| } |
| }, |
| EXPRESSION { |
| @Override |
| String[] getReturnTags() { |
| return RETURN_ONLY_TAGS; |
| } |
| }; |
| |
| static final String[] RETURN_ONLY_TAGS = { JSDocTag.RETURNS, |
| JSDocTag.RETURN }; |
| |
| abstract String[] getReturnTags(); |
| } |
| |
| public void processMethod(FunctionStatement statement, IMethod method, |
| JSProblemReporter reporter, ITypeChecker typeChecker) { |
| Comment comment = getComment(statement); |
| if (comment == null) { |
| return; |
| } |
| final JSDocTags tags = parse(comment); |
| processMethod(method, |
| statement.isDeclaration() ? JSDocFunctionContext.DECLARATION |
| : JSDocFunctionContext.EXPRESSION, tags, reporter, |
| typeChecker); |
| } |
| |
| public final void processMethod(IMethod method, final JSDocTags tags, |
| JSProblemReporter reporter, ITypeChecker typeChecker) { |
| processMethod(method, JSDocFunctionContext.EXPRESSION, tags, reporter, |
| typeChecker); |
| } |
| |
| public void processMethod(IMethod method, JSDocFunctionContext context, |
| final JSDocTags tags, JSProblemReporter reporter, |
| ITypeChecker typeChecker) { |
| if (method.getType() == null) { |
| parseType(method, tags, context.getReturnTags(), reporter, |
| typeChecker); |
| } |
| parseParams(method, tags, reporter, typeChecker); |
| parseThis(method, tags, reporter, typeChecker); |
| parseDeprecation(method, tags, reporter); |
| parseAccessModifiers(method, tags, reporter); |
| parseConstructor(method, tags, reporter); |
| parseExtends(method, tags, reporter, typeChecker); |
| parseThrows(method, tags, reporter, typeChecker); |
| parseSuppressWarnings(method, tags, reporter); |
| } |
| |
| private void parseExtends(IMethod method, JSDocTags tags, |
| JSProblemReporter reporter, ITypeChecker typeChecker) { |
| final JSDocTag extendsTag = tags.get(JSDocTag.EXTENDS); |
| if (extendsTag != null) { |
| final JSType type = parseType(extendsTag, false, reporter); |
| if (type != null) { |
| if (typeChecker != null) |
| typeChecker.checkType(type, extendsTag); |
| method.setExtendsType(type); |
| } |
| validateSingleTag(tags, JSDocTag.EXTENDS, reporter); |
| } |
| } |
| |
| protected void parseSuppressWarnings(IElement element, JSDocTags tags, |
| JSProblemReporter reporter) { |
| final List<JSDocTag> suppressWarnings = tags |
| .list(JSDocTag.SUPPRESS_WARNINGS); |
| if (!suppressWarnings.isEmpty()) { |
| for (JSDocTag tag : suppressWarnings) { |
| processSuppressWarnings(tag, new CountingReporter(reporter), |
| element); |
| } |
| } |
| } |
| |
| private void parseThrows(IMethod method, JSDocTags tags, |
| JSProblemReporter reporter, ITypeChecker typeChecker) { |
| if (typeChecker != null) { |
| List<JSDocTag> throwsTags = tags.list(JSDocTag.THROWS); |
| for (JSDocTag throwsTag : throwsTags) { |
| String value = throwsTag.value(); |
| String[] split = value.split(" "); |
| if (split.length > 0) { |
| if (isBraced(split[0])) { |
| JSType type = translateTypeName(cutBraces(split[0]), |
| throwsTag, reporter); |
| typeChecker.checkType(type, throwsTag); |
| } |
| } |
| } |
| } |
| |
| } |
| |
| public static Comment getCallComment(CallExpression call) { |
| ASTNode node = call; |
| while (node != null) { |
| if (node instanceof JSNode) { |
| final Comment doc = ((JSNode) node).getDocumentation(); |
| if (doc != null) { |
| return doc; |
| } |
| } |
| if (node instanceof Statement) { |
| break; |
| } |
| if (!(node instanceof JSNode)) { |
| break; |
| } |
| node = ((JSNode) node).getParent(); |
| } |
| return null; |
| } |
| |
| @Deprecated |
| public static Comment getFunctionComment(FunctionStatement statement) { |
| return getComment(statement); |
| } |
| |
| public static Comment getComment(JSNode statement) { |
| Comment documentation = statement.getDocumentation(); |
| if (documentation != null) { |
| return documentation; |
| } |
| final ASTNode parent = statement.getParent(); |
| if (parent instanceof BinaryOperation) { |
| final BinaryOperation binary = (BinaryOperation) parent; |
| if (binary.getOperation() == JSParser.ASSIGN |
| && binary.getRightExpression() == statement) { |
| documentation = binary.getLeftExpression().getDocumentation(); |
| if (documentation != null) { |
| return documentation; |
| } |
| } |
| } else if (parent instanceof PropertyInitializer) { |
| final PropertyInitializer property = (PropertyInitializer) parent; |
| if (property.getValue() == statement) { |
| documentation = property.getName().getDocumentation(); |
| if (documentation != null) { |
| return documentation; |
| } |
| } |
| } else if (parent instanceof VariableDeclaration) { |
| final VariableDeclaration variable = (VariableDeclaration) parent; |
| if ((variable.getInitializer() == statement || variable |
| .getIdentifier() == statement) |
| && variable.getParent() instanceof VariableStatement) { |
| return ((VariableStatement) variable.getParent()) |
| .getDocumentation(); |
| } |
| } else if (parent instanceof PropertyExpression) { |
| return getComment((PropertyExpression) parent); |
| } |
| return null; |
| } |
| |
| private void parseConstructor(IMethod method, JSDocTags tags, |
| JSProblemReporter reporter) { |
| if (tags.get(JSDocTag.CONSTRUCTOR) != null) { |
| method.setConstructor(true); |
| validateSingleTag(tags, JSDocTag.CONSTRUCTOR, reporter); |
| } |
| } |
| |
| public static class NamedValue<T> { |
| public final String name; |
| public final T value; |
| |
| private NamedValue(String name, T value) { |
| this.name = name; |
| this.value = value; |
| } |
| |
| public static <T> NamedValue<T> of(String name, T value) { |
| return new NamedValue<T>(name, value); |
| } |
| } |
| |
| protected static final List<NamedValue<Visibility>> STANDARD_ACCESS_MODIFIERS; |
| |
| static { |
| final List<NamedValue<Visibility>> modifiers = new ArrayList<NamedValue<Visibility>>( |
| 3); |
| modifiers.add(NamedValue.of(JSDocTag.PUBLIC, Visibility.PUBLIC)); |
| modifiers.add(NamedValue.of(JSDocTag.PROTECTED, Visibility.PROTECTED)); |
| modifiers.add(NamedValue.of(JSDocTag.PRIVATE, Visibility.PRIVATE)); |
| STANDARD_ACCESS_MODIFIERS = modifiers; |
| } |
| |
| protected List<NamedValue<Visibility>> getSupportedAccessModifiers() { |
| return STANDARD_ACCESS_MODIFIERS; |
| } |
| |
| public void parseAccessModifiers(IMember member, JSDocTags tags, |
| JSProblemReporter reporter) { |
| final List<NamedValue<Visibility>> accessModifiers = getSupportedAccessModifiers(); |
| for (int i = 0; i < accessModifiers.size(); ++i) { |
| final NamedValue<Visibility> pair = accessModifiers.get(i); |
| final JSDocTag tag = tags.get(pair.name); |
| if (tag != null) { |
| member.setVisibility(pair.value); |
| int extraTags = tags.count(pair.name) - 1; |
| for (int j = i + 1; extraTags == 0 |
| && j < accessModifiers.size(); ++j) { |
| extraTags += tags.count(accessModifiers.get(j).name); |
| } |
| if (extraTags > 0) { |
| final List<JSDocTag> all = new ArrayList<JSDocTag>(); |
| all.addAll(tags.list(pair.name)); |
| all.remove(tag); |
| for (int j = i + 1; j < accessModifiers.size(); ++j) { |
| all.addAll(tags.list(accessModifiers.get(j).name)); |
| } |
| for (JSDocTag t : all) { |
| reportProblem(reporter, JSDocProblem.IGNORED_TAG, t, |
| t.name(), tag.name()); |
| } |
| } |
| break; |
| } |
| } |
| } |
| |
| private void validateSingleTag(JSDocTags tags, String tagName, |
| JSProblemReporter reporter) { |
| if (reporter != null && tags.count(tagName) > 1) { |
| final List<JSDocTag> t = tags.list(tagName); |
| for (JSDocTag tag : t.subList(1, t.size())) { |
| reportProblem(reporter, JSDocProblem.DUPLICATE_TAG, tag, |
| tag.name()); |
| } |
| } |
| } |
| |
| public void processVariable(VariableDeclaration declaration, |
| IVariable variable, JSProblemReporter reporter, |
| ITypeChecker typeChecker) { |
| Comment comment = declaration.getDocumentation(); |
| if (comment == null) { |
| final IVariableStatement statement = declaration.getStatement(); |
| final List<VariableDeclaration> vars = statement.getVariables(); |
| if (!vars.isEmpty() && vars.get(0) == declaration) { |
| comment = statement.getDocumentation(); |
| if (comment == null) { |
| return; |
| } |
| } else { |
| return; |
| } |
| } |
| final JSDocTags tags = parse(comment); |
| if (variable.getType() == null) { |
| parseType(variable, tags, TYPE_TAGS, reporter, typeChecker); |
| } |
| parseTypeDef(variable, tags, reporter, typeChecker); |
| parseDeprecation(variable, tags, reporter); |
| parseAccessModifiers(variable, tags, reporter); |
| parseSuppressWarnings(variable, tags, reporter); |
| } |
| |
| private void parseTypeDef(IVariable variable, JSDocTags tags, |
| JSProblemReporter reporter, ITypeChecker typeChecker) { |
| final JSDocTag typeDefTag = tags.get(JSDocTag.TYPEDEF); |
| if (typeDefTag != null) { |
| final JSType type = parseType(typeDefTag, false, reporter); |
| if (type != null) { |
| if (!(type instanceof RecordType)) { |
| reportProblem(reporter, JSDocProblem.UNSUPPORTED_TYPEDEF, |
| typeDefTag, type.getName()); |
| } else { |
| if (typeChecker != null) |
| typeChecker.checkType(type, typeDefTag); |
| variable.setTypeDef(type); |
| } |
| } |
| validateSingleTag(tags, JSDocTag.TYPEDEF, reporter); |
| if (reporter != null && tags.count(JSDocTag.TYPE) > 0) { |
| reportProblem(reporter, JSDocProblem.TYPE_WITH_TYPEDEF, |
| typeDefTag, typeDefTag.name()); |
| } |
| } |
| } |
| |
| private void parseThis(IMethod method, JSDocTags tags, |
| JSProblemReporter reporter, ITypeChecker typeChecker) { |
| final JSDocTag thisTag = tags.get(JSDocTag.THIS); |
| if (thisTag != null) { |
| final JSType type = parseType(thisTag, false, reporter); |
| if (type != null) { |
| if (typeChecker != null) |
| typeChecker.checkType(type, thisTag); |
| method.setThisType(type); |
| } |
| validateSingleTag(tags, JSDocTag.THIS, reporter); |
| } |
| } |
| |
| public void parseDeprecation(IMember member, JSDocTags tags, |
| JSProblemReporter reporter) { |
| if (tags.get(JSDocTag.DEPRECATED) != null) { |
| member.setDeprecated(true); |
| validateSingleTag(tags, JSDocTag.DEPRECATED, reporter); |
| } |
| } |
| |
| protected static class ParamInfo { |
| public String type; |
| public boolean varargs; |
| public boolean optional; |
| |
| void clear() { |
| type = null; |
| varargs = false; |
| optional = false; |
| } |
| } |
| |
| protected void parseParams(IMethod method, JSDocTags tags, |
| JSProblemReporter reporter, ITypeChecker typeChecker) { |
| final List<JSDocTag> paramTags = tags.list(JSDocTag.PARAM); |
| if (paramTags.isEmpty()) { |
| return; |
| } |
| int problemCount = 0; |
| final Map<String, RecordType> objectPropertiesTypes = new HashMap<String, RecordType>(); |
| final Set<String> processedParams = new HashSet<String>(); |
| final ParamInfo pp = new ParamInfo(); |
| for (JSDocTag tag : paramTags) { |
| pp.clear(); |
| final TagTokenizer st = new TagTokenizer(tag); |
| if (st.hasMoreTokens()) { |
| final String token = st.peek(); |
| if (isBraced(token)) { |
| String type = cutBraces(token); |
| if (type.startsWith(PARAM_DOTS)) { |
| pp.varargs = true; |
| type = type.substring(PARAM_DOTS.length()); |
| } else if (type.endsWith(PARAM_DOTS)) { |
| pp.varargs = true; |
| type = type.substring(0, |
| type.length() - PARAM_DOTS.length()); |
| } else if (type.endsWith(PARAM_OPTIONAL)) { |
| type = type.substring(0, |
| type.length() - PARAM_OPTIONAL.length()); |
| pp.optional = true; |
| } |
| pp.type = type; |
| st.nextToken(); |
| } |
| } |
| if (st.hasMoreTokens()) { |
| String paramName = st.nextToken(); |
| if (paramName.startsWith("[") && paramName.endsWith("]")) { |
| pp.optional = true; |
| paramName = paramName.substring(1, paramName.length() - 1); |
| int defaultValueSeperatorIndex = paramName.indexOf('='); |
| if (defaultValueSeperatorIndex != -1) { |
| paramName = paramName.substring(0, |
| defaultValueSeperatorIndex); |
| } |
| } |
| if (!pp.varargs && paramName.endsWith(PARAM_DOTS)) { |
| // just in case, so next condition doesn't use ".." as |
| // property name. |
| pp.varargs = true; |
| paramName = paramName.substring(0, paramName.length() |
| - PARAM_DOTS.length()); |
| } |
| String propertyName = null; |
| int propertiesObjectIndex = paramName.lastIndexOf('.'); |
| if (propertiesObjectIndex != -1) { |
| // http://code.google.com/p/jsdoc-toolkit/wiki/TagParam |
| // = Parameters With Properties = |
| propertyName = paramName |
| .substring(propertiesObjectIndex + 1); |
| if (JavaScriptLanguageUtil.isValidIdentifier(propertyName)) { |
| String objectName = paramName.substring(0, |
| propertiesObjectIndex); |
| RecordType propertiesType = objectPropertiesTypes |
| .get(objectName); |
| if (propertiesType == null) { |
| propertiesType = TypeInfoModelFactory.eINSTANCE |
| .createRecordType(); |
| propertiesType.setTypeName('{' + objectName + '}'); |
| objectPropertiesTypes.put(objectName, |
| propertiesType); |
| final IParameter param = method |
| .getParameter(objectName); |
| if (param != null) { |
| param.setType(propertiesType); |
| if (pp.optional) |
| param.setKind(ParameterKind.OPTIONAL); |
| } else { |
| int index = objectName.lastIndexOf('.'); |
| if (index == -1) { |
| ++problemCount; |
| reportProblem(reporter, |
| JSDocProblem.UNKNOWN_PARAM, tag, |
| objectName); |
| } else { |
| RecordType parentRecordType = objectPropertiesTypes |
| .get(objectName.substring(0, index)); |
| if (parentRecordType != null) { |
| String memberName = objectName |
| .substring(index + 1); |
| EList<Member> members = parentRecordType |
| .getMembers(); |
| for (Member member : members) { |
| if (member.getName().equals( |
| memberName)) { |
| member.setType(propertiesType); |
| break; |
| } |
| } |
| } |
| } |
| } |
| } |
| final RecordProperty property = TypeInfoModelFactory.eINSTANCE |
| .createRecordProperty(); |
| property.setName(propertyName); |
| if (pp.type != null) { |
| JSType type = translateTypeName(pp.type, tag, |
| reporter); |
| if (typeChecker != null) |
| typeChecker.checkType(type, tag); |
| property.setType(type); |
| } |
| property.setOptional(pp.optional); |
| propertiesType.getMembers().add(property); |
| continue; |
| } |
| } |
| if (method.getParameter(paramName) != null |
| && !processedParams.add(paramName)) { |
| ++problemCount; |
| reportProblem(reporter, JSDocProblem.DUPLICATE_PARAM, tag, |
| paramName); |
| continue; |
| } |
| IParameter parameter = method.getParameter(paramName); |
| if (parameter != null) { |
| if (!pp.optional && st.hasMoreTokens() |
| && st.nextToken().equals("optional")) { |
| pp.optional = true; |
| } |
| updateParameter(tag, parameter, pp, reporter, typeChecker); |
| } else if (pp.varargs) { |
| /* |
| * create virtual parameter for varargs as most of the time |
| * passed values are accessed via "arguments" object and |
| * parameter declaration in code is not required. |
| */ |
| parameter = method.createParameter(); |
| parameter.setName(paramName); |
| updateParameter(tag, parameter, pp, reporter, typeChecker); |
| method.getParameters().add(parameter); |
| processedParams.add(paramName); |
| } else { |
| ++problemCount; |
| reportProblem(reporter, JSDocProblem.UNKNOWN_PARAM, tag, |
| paramName); |
| } |
| } else { |
| ++problemCount; |
| reportProblem(reporter, JSDocProblem.MISSING_PARAMETER_NAME, |
| tag); |
| } |
| } |
| if (problemCount == 0 && reporter != null) { |
| for (IParameter parameter : method.getParameters()) { |
| if (!processedParams.contains(parameter.getName()) |
| && !objectPropertiesTypes.containsKey(parameter |
| .getName())) { |
| final ReferenceLocation location = parameter.getLocation(); |
| reporter.reportProblem( |
| JSDocProblem.PARAMETER_MISSING_ANNOTATION, |
| JSDocProblem.PARAMETER_MISSING_ANNOTATION |
| .formatMessage(parameter.getName()), |
| location.getNameStart(), location.getNameEnd()); |
| } |
| } |
| } |
| } |
| |
| protected void updateParameter(JSDocTag tag, final IParameter parameter, |
| final ParamInfo pp, JSProblemReporter reporter, |
| ITypeChecker typeChecker) { |
| if (pp.type != null && parameter.getType() == null) { |
| JSType type = translateTypeName(pp.type, tag, reporter); |
| if (typeChecker != null) |
| typeChecker.checkType(type, tag); |
| parameter.setType(type); |
| } |
| if (pp.varargs) { |
| parameter.setKind(ParameterKind.VARARGS); |
| } else if (pp.optional) { |
| parameter.setKind(ParameterKind.OPTIONAL); |
| } |
| } |
| |
| public static final String[] TYPE_TAGS = { JSDocTag.TYPE }; |
| |
| public static class TagTokenizer { |
| private final String content; |
| private final int end; |
| private int position; |
| private int tokenStart; |
| |
| public TagTokenizer(JSDocTag tag) { |
| this(tag.value()); |
| } |
| |
| public TagTokenizer(String content) { |
| this.content = content; |
| this.end = content.length(); |
| this.position = skipDelimiters(0); |
| } |
| |
| /** |
| * The token loaded by {@link #peek()} if not <code>null</code>. |
| */ |
| private String current; |
| |
| /** |
| * Checks if there are more tokens available from this tokenizer. |
| */ |
| public boolean hasMoreTokens() { |
| return current != null || position < end; |
| } |
| |
| /** |
| * Returns the first character of the next token without removing it |
| * from this tokenizer or <code>\0</code> if there are no more tokens. |
| */ |
| public char peekChar() { |
| if (current == null) { |
| if (position < end) { |
| return content.charAt(position); |
| } else { |
| return '\0'; |
| } |
| } else if (current.length() != 0) { |
| return current.charAt(0); |
| } else { |
| return '\0'; |
| } |
| } |
| |
| /** |
| * Returns the next token without removing it from this tokenizer or |
| * <code>null</code> if there are no more tokens. |
| */ |
| public String peek() { |
| if (current == null) { |
| if (position < end) { |
| current = fetchNextToken(); |
| } else { |
| return null; |
| } |
| } |
| return current; |
| } |
| |
| /** |
| * Returns the next token from this tokenizer. |
| * |
| * @throws NoSuchElementException |
| * if no more tokens available. |
| */ |
| public String nextToken() { |
| if (current != null) { |
| final String token = current; |
| current = null; |
| return token; |
| } |
| return fetchNextToken(); |
| } |
| |
| /** |
| * Returns the starting position of the last fetched token. |
| */ |
| public int getTokenStart() { |
| return tokenStart; |
| } |
| |
| private int skipDelimiters(int pos) { |
| while (pos < end && Character.isWhitespace(content.charAt(pos))) { |
| ++pos; |
| } |
| return pos; |
| } |
| |
| private String fetchNextToken() { |
| if (position == end) { |
| throw new NoSuchElementException(); |
| } |
| tokenStart = position; |
| int braceCount = 0; |
| while (position < end) { |
| if (Character.isWhitespace(content.charAt(position)) |
| && braceCount == 0) { |
| break; |
| } else if (content.charAt(position) == '{') { |
| ++braceCount; |
| } else if (content.charAt(position) == '}') { |
| --braceCount; |
| } |
| ++position; |
| } |
| final String token = content.substring(tokenStart, position); |
| position = skipDelimiters(position); |
| return token; |
| } |
| } |
| |
| /** |
| * @see St |
| * @param context |
| * @param member |
| * @param comment |
| */ |
| public void parseType(IElement member, JSDocTags tags, String[] tagNames, |
| JSProblemReporter reporter, ITypeChecker typeChecker) { |
| final JSDocTag tag = tags.get(tagNames); |
| if (tag != null) { |
| if (reporter != null) { |
| final int count = tags.count(tagNames); |
| if (count > 1) { |
| for (JSDocTag t : tags.list(tagNames).subList(1, count)) { |
| if (t.name().equals(tag.name())) { |
| reportProblem(reporter, JSDocProblem.DUPLICATE_TAG, |
| t, t.name()); |
| } else { |
| reportProblem( |
| reporter, |
| JSDocProblem.DUPLICATE_TAG, |
| JSDocProblem.DUPLICATE_TAG.formatMessage(t |
| .name()) |
| + " (was " |
| + tag.name() |
| + ")", t); |
| } |
| } |
| } |
| } |
| final JSType type = parseType(tag, requireBraces(tag.name()), |
| reporter); |
| if (type != null) { |
| if (typeChecker != null) |
| typeChecker.checkType(type, tag); |
| member.setType(type); |
| } |
| } |
| } |
| |
| public JSType parseType(JSDocTag tag, boolean requireBraces, |
| JSProblemReporter reporter) { |
| final TagTokenizer st = new TagTokenizer(tag); |
| if (st.hasMoreTokens()) { |
| final String typeName = st.nextToken(); |
| if (!requireBraces || isBraced(typeName)) { |
| return translateTypeName(cutBraces(typeName), tag, reporter); |
| } |
| } else if (!requireBraces) { |
| reportProblem(reporter, JSDocProblem.WRONG_TYPE_SYNTAX, |
| "Missing type name", tag); |
| } |
| return null; |
| } |
| |
| protected boolean requireBraces(String tagName) { |
| return JSDocTag.RETURN.equals(tagName) |
| || JSDocTag.RETURNS.equals(tagName); |
| } |
| |
| private void reportProblem(JSProblemReporter reporter, |
| JSProblemIdentifier problemIdentifier, JSDocTag tag, Object... args) { |
| if (reporter != null) { |
| reporter.reportProblem(problemIdentifier, |
| problemIdentifier.formatMessage(args), tag.start(), |
| tag.end()); |
| } |
| } |
| |
| private void reportProblem(JSProblemReporter reporter, |
| JSProblemIdentifier problemIdentifier, String message, JSDocTag tag) { |
| if (reporter != null) { |
| reporter.reportProblem(problemIdentifier, message, tag.start(), |
| tag.end()); |
| } |
| } |
| |
| protected JSType translateTypeName(String typeName, JSDocTag tag, |
| JSProblemReporter reporter) { |
| JSDocTypeParser parser = createTypeParser(); |
| try { |
| return parser.parse(typeName); |
| } catch (JSDocParseException e) { |
| if (reporter != null) { |
| reporter.reportProblem(e.problemId, e.getMessage(), |
| tag.start(), tag.end()); |
| } |
| return null; |
| } catch (ParseException e) { |
| if (reporter != null) { |
| String message = e.getMessage(); |
| if (e.getErrorOffset() >= 0) { |
| message += " after " |
| + typeName.substring(0, e.getErrorOffset()); |
| } |
| reporter.reportProblem(JSDocProblem.WRONG_TYPE_SYNTAX, message, |
| tag.start(), tag.end()); |
| } |
| return null; |
| } |
| } |
| |
| public JSDocTypeParser createTypeParser() { |
| return new JSDocTypeParser(); |
| } |
| |
| public static String cutBraces(String typeName) { |
| if (isBraced(typeName)) { |
| typeName = typeName.substring(1, typeName.length() - 1); |
| } |
| return typeName; |
| } |
| |
| public static boolean isBraced(String typeName) { |
| final int length = typeName.length(); |
| return length > 2 && typeName.charAt(0) == '{' |
| && typeName.charAt(length - 1) == '}'; |
| } |
| |
| private static class CountingReporter implements ProblemReporter { |
| @Nullable |
| final JSProblemReporter reporter; |
| int problemCount; |
| |
| public CountingReporter(@Nullable JSProblemReporter reporter) { |
| this.reporter = reporter; |
| } |
| |
| public void reportProblem(IProblemIdentifier identifier, |
| String message, int start, int end) { |
| ++problemCount; |
| if (reporter != null) { |
| reporter.reportProblem(identifier, message, start, end); |
| } |
| } |
| } |
| |
| private void processSuppressWarnings(JSDocTag tag, |
| @NonNull CountingReporter reporter, IElement element) { |
| final CharStream input = new ANTLRStringStream(tag.value()); |
| final boolean hasParenthesis = input.LT(1) == '('; |
| if (hasParenthesis) { |
| input.consume(); |
| } |
| for (;;) { |
| int ch = input.LT(1); |
| while (Character.isWhitespace(ch)) { |
| input.consume(); |
| ch = input.LT(1); |
| } |
| if (ch == '"' || ch == '\'') { |
| final char quote = (char) ch; |
| input.consume(); |
| final int start = input.index(); |
| for (;;) { |
| ch = input.LT(1); |
| if (ch == quote) { |
| suppressWarning(tag, reporter, element, input, start); |
| input.consume(); |
| break; |
| } else if (ch == CharStream.EOF) { |
| reporter.reportProblem( |
| JSDocProblem.WRONG_SUPPRESS_WARNING, "Closing " |
| + quote + " expected", tag.start(), |
| tag.end()); |
| break; |
| } |
| input.consume(); |
| } |
| } else { |
| final int start = input.index(); |
| for (;;) { |
| ch = input.LT(1); |
| if (ch == ',' || ch == ')' || ch == CharStream.EOF |
| || Character.isWhitespace(ch)) { |
| suppressWarning(tag, reporter, element, input, start); |
| break; |
| } |
| input.consume(); |
| } |
| } |
| ch = input.LT(1); |
| while (ch != CharStream.EOF && Character.isWhitespace(ch)) { |
| input.consume(); |
| ch = input.LT(1); |
| } |
| if (ch != ',') { |
| break; |
| } |
| input.consume(); |
| } |
| if (hasParenthesis) { |
| if (input.LT(1) == ')') { |
| input.consume(); |
| } else { |
| reporter.reportProblem(JSDocProblem.WRONG_SUPPRESS_WARNING, |
| "Closing ) expected", tag.start(), tag.end()); |
| } |
| } |
| if (reporter.problemCount != 0 && input.LT(1) != CharStream.EOF) { |
| reporter.reportProblem(JSDocProblem.WRONG_SUPPRESS_WARNING, |
| "Unexpected content", tag.start(), tag.end()); |
| } |
| } |
| |
| private void suppressWarning(JSDocTag tag, |
| @NonNull CountingReporter reporter, IElement element, |
| final CharStream input, final int start) { |
| final String categoryId = input.substring(start, input.index() - 1); |
| if (categoryId.length() != 0) { |
| final IProblemCategory category = getCategory(categoryId); |
| if (category != null) { |
| element.addSuppressedWarning(category); |
| } else { |
| reporter.reportProblem(JSDocProblem.WRONG_SUPPRESS_WARNING, NLS |
| .bind("Unsupported {0}({1})", |
| JSDocTag.SUPPRESS_WARNINGS, categoryId), tag |
| .start(), tag.end()); |
| } |
| } else { |
| reporter.reportProblem(JSDocProblem.WRONG_SUPPRESS_WARNING, |
| "warning identifier expected", tag.start(), tag.end()); |
| } |
| } |
| |
| protected IProblemCategory getCategory(final String categoryId) { |
| return ProblemCategoryManager.getInstance().getCategory( |
| JavaScriptNature.NATURE_ID, JSDocTag.SUPPRESS_WARNINGS, |
| categoryId); |
| } |
| |
| /** |
| * Value type for the results of the high-level JSDoc tag parsing. |
| * |
| * @see JSDocSupport#parseParameter(JSDocTag) |
| * @see JSDocSupport#parseOptionalType(JSDocTag) |
| * @see JSDocSupport#parseType(JSDocTag) |
| */ |
| public static class TypedElementNode { |
| /** |
| * Type expression, potentially with surrounding braces or |
| * <code>null</code> |
| */ |
| @Nullable |
| private final String type; |
| /** |
| * Offset of the type token or <code>-1</code> if no type |
| */ |
| private final int typeOffset; |
| |
| /** |
| * Answers if type expression is surrounded with braces. |
| */ |
| boolean isBraced() { |
| return true; |
| } |
| |
| TypedElementNode(String type, int typeOffset) { |
| this.type = type; |
| this.typeOffset = typeOffset; |
| } |
| |
| /** |
| * Returns type expression without surrounding braces or |
| * <code>null</code> |
| */ |
| @Nullable |
| public String getTypeExpression() { |
| if (type != null) { |
| return isBraced() ? type.substring(1, type.length() - 1) : type; |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Returns the starting offset of the actual type expression (i.e. |
| * without surrounding braces) or <code>-1</code> if no type expression. |
| */ |
| public int getTypeExpressionStart() { |
| return typeOffset >= 0 ? typeOffset + (isBraced() ? 1 : 0) : -1; |
| } |
| |
| /** |
| * Checks if specified position is contained in the type declaration. |
| */ |
| public boolean isInType(int position) { |
| return type != null |
| && (isBraced() ? typeOffset < position |
| && position < typeOffset + type.length() |
| : typeOffset <= position |
| && position <= typeOffset + type.length()); |
| } |
| } |
| |
| public static class ParameterNode extends TypedElementNode { |
| @NonNull |
| public final String name; |
| public final int offset; |
| |
| ParameterNode(String type, int typeOffset, String name, int offset) { |
| super(type, typeOffset); |
| this.name = name; |
| this.offset = offset; |
| } |
| |
| public boolean isInParameter(int position) { |
| return offset <= position && offset + name.length() >= position; |
| } |
| } |
| |
| public static class TypeNode extends TypedElementNode { |
| private final boolean braced; |
| |
| @Override |
| boolean isBraced() { |
| return braced; |
| } |
| |
| TypeNode(String type, int typeOffset, boolean braced) { |
| super(type, typeOffset); |
| this.braced = braced; |
| } |
| } |
| |
| /** |
| * High level parsing of JSDoc @param tag |
| */ |
| public static ParameterNode parseParameter(JSDocTag tag) { |
| final TagTokenizer tokenizer = new TagTokenizer(tag); |
| if (!tokenizer.hasMoreTokens()) { |
| return null; |
| } |
| int typeOffset = -1; |
| String type = null; |
| String token = tokenizer.nextToken(); |
| if (JSDocSupport.isBraced(token)) { // skip type name |
| if (!tokenizer.hasMoreTokens()) { |
| return null; |
| } |
| typeOffset = tokenizer.getTokenStart(); |
| type = token; |
| token = tokenizer.nextToken(); |
| } |
| int tokenStart = tokenizer.getTokenStart(); |
| // optional parameter |
| if (token.startsWith("[") && token.endsWith("]")) { |
| token = token.substring(1, token.length() - 1); |
| ++tokenStart; |
| // default value separator |
| int separator = token.indexOf('='); |
| if (separator != -1) { |
| token = token.substring(0, separator); |
| } |
| } |
| int propertyIndex = token.indexOf('.'); |
| if (propertyIndex != -1) { |
| token = token.substring(0, propertyIndex); |
| } |
| return new ParameterNode(type, typeOffset, token, tokenStart); |
| } |
| |
| /** |
| * Parses optional type expression (like the one in @return/@returns |
| * tags) from the specified tag value. |
| */ |
| public static TypedElementNode parseOptionalType(JSDocTag tag) { |
| final TagTokenizer tokenizer = new TagTokenizer(tag); |
| if (tokenizer.peekChar() == '{' && tokenizer.hasMoreTokens()) { |
| final String type = tokenizer.nextToken(); |
| return new TypedElementNode(type, tokenizer.getTokenStart()); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Parses the type expression of @type tag. |
| */ |
| public static TypeNode parseType(JSDocTag tag) { |
| final TagTokenizer tokenizer = new TagTokenizer(tag); |
| if (tokenizer.hasMoreTokens()) { |
| final String type = tokenizer.nextToken(); |
| return new TypeNode(type, tokenizer.getTokenStart(), isBraced(type)); |
| } else { |
| return null; |
| } |
| } |
| |
| } |