/******************************************************************************* * Copyright (c) 2010 xored software, Inc. * * 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: * 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; } } }