mirror of
https://github.com/github/codeql.git
synced 2025-12-31 08:06:36 +01:00
1893 lines
55 KiB
Java
1893 lines
55 KiB
Java
package com.semmle.js.parser;
|
|
|
|
import com.semmle.js.ast.Comment;
|
|
import com.semmle.js.ast.Position;
|
|
import com.semmle.js.ast.SourceLocation;
|
|
import com.semmle.js.ast.jsdoc.AllLiteral;
|
|
import com.semmle.js.ast.jsdoc.ArrayType;
|
|
import com.semmle.js.ast.jsdoc.FieldType;
|
|
import com.semmle.js.ast.jsdoc.FunctionType;
|
|
import com.semmle.js.ast.jsdoc.JSDocComment;
|
|
import com.semmle.js.ast.jsdoc.JSDocTag;
|
|
import com.semmle.js.ast.jsdoc.JSDocTypeExpression;
|
|
import com.semmle.js.ast.jsdoc.NameExpression;
|
|
import com.semmle.js.ast.jsdoc.NonNullableType;
|
|
import com.semmle.js.ast.jsdoc.NullLiteral;
|
|
import com.semmle.js.ast.jsdoc.NullableLiteral;
|
|
import com.semmle.js.ast.jsdoc.NullableType;
|
|
import com.semmle.js.ast.jsdoc.OptionalType;
|
|
import com.semmle.js.ast.jsdoc.ParameterType;
|
|
import com.semmle.js.ast.jsdoc.RecordType;
|
|
import com.semmle.js.ast.jsdoc.RestType;
|
|
import com.semmle.js.ast.jsdoc.TypeApplication;
|
|
import com.semmle.js.ast.jsdoc.UndefinedLiteral;
|
|
import com.semmle.js.ast.jsdoc.UnionType;
|
|
import com.semmle.js.ast.jsdoc.VoidLiteral;
|
|
import com.semmle.util.data.Pair;
|
|
import com.semmle.util.exception.Exceptions;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
/** A Java port of <a href="https://github.com/Constellation/doctrine">doctrine</a>. */
|
|
public class JSDocParser {
|
|
private String source;
|
|
|
|
/** Parse the given string as a JSDoc comment. */
|
|
public JSDocComment parse(Comment comment) {
|
|
source = comment.getText().substring(1);
|
|
JSDocTagParser p = new JSDocTagParser();
|
|
Pair<String, List<JSDocTagParser.Tag>> r = p.new TagParser(null).parse(source);
|
|
List<JSDocTag> tags = new ArrayList<>();
|
|
for (JSDocTagParser.Tag tag : r.snd()) {
|
|
String title = tag.title;
|
|
String description = tag.description;
|
|
String name = tag.name;
|
|
int startLine = tag.startLine;
|
|
int startColumn = tag.startColumn;
|
|
|
|
JSDocTypeExpression jsdocType = tag.type;
|
|
|
|
int realStartLine = comment.getLoc().getStart().getLine() + startLine;
|
|
int realStartColumn =
|
|
(startLine == 0 ? comment.getLoc().getStart().getColumn() + 3 : 0) + startColumn;
|
|
SourceLocation loc =
|
|
new SourceLocation(
|
|
source,
|
|
new Position(realStartLine, realStartColumn, -1),
|
|
new Position(realStartLine, realStartColumn + 1 + title.length(), -1));
|
|
tags.add(new JSDocTag(loc, title, description, name, jsdocType, tag.errors));
|
|
}
|
|
return new JSDocComment(comment, r.fst(), tags);
|
|
}
|
|
|
|
/** Specification of Doctrine AST types for JSDoc type expressions. */
|
|
private static final Map<Class<? extends JSDocTypeExpression>, List<String>> spec =
|
|
new LinkedHashMap<Class<? extends JSDocTypeExpression>, List<String>>();
|
|
|
|
static {
|
|
spec.put(AllLiteral.class, Arrays.<String>asList());
|
|
spec.put(ArrayType.class, Arrays.asList("elements"));
|
|
spec.put(FieldType.class, Arrays.asList("key", "value"));
|
|
spec.put(FunctionType.class, Arrays.asList("this", "new", "params", "result"));
|
|
spec.put(NameExpression.class, Arrays.asList("name"));
|
|
spec.put(NonNullableType.class, Arrays.asList("expression", "prefix"));
|
|
spec.put(NullableLiteral.class, Arrays.<String>asList());
|
|
spec.put(NullLiteral.class, Arrays.<String>asList());
|
|
spec.put(NullableType.class, Arrays.asList("expression", "prefix"));
|
|
spec.put(OptionalType.class, Arrays.asList("expression"));
|
|
spec.put(ParameterType.class, Arrays.asList("name", "expression"));
|
|
spec.put(RecordType.class, Arrays.asList("fields"));
|
|
spec.put(RestType.class, Arrays.asList("expression"));
|
|
spec.put(TypeApplication.class, Arrays.asList("expression", "applications"));
|
|
spec.put(UndefinedLiteral.class, Arrays.<String>asList());
|
|
spec.put(UnionType.class, Arrays.asList("elements"));
|
|
spec.put(VoidLiteral.class, Arrays.<String>asList());
|
|
}
|
|
|
|
private static String sliceSource(String source, int index, int last) {
|
|
if (index >= source.length()) return "";
|
|
if (last > source.length()) last = source.length();
|
|
return source.substring(index, last);
|
|
}
|
|
|
|
private static boolean isLineTerminator(int ch) {
|
|
return ch == '\n' || ch == '\r' || ch == '\u2028' || ch == '\u2029';
|
|
}
|
|
|
|
private static boolean isWhiteSpace(char ch) {
|
|
return Character.isWhitespace(ch) && !isLineTerminator(ch) || ch == '\u00a0';
|
|
}
|
|
|
|
private static boolean isDecimalDigit(char ch) {
|
|
return "0123456789".indexOf(ch) >= 0;
|
|
}
|
|
|
|
private static boolean isHexDigit(char ch) {
|
|
return "0123456789abcdefABCDEF".indexOf(ch) >= 0;
|
|
}
|
|
|
|
private static boolean isOctalDigit(char ch) {
|
|
return "01234567".indexOf(ch) >= 0;
|
|
}
|
|
|
|
private static boolean isASCIIAlphanumeric(char ch) {
|
|
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9');
|
|
}
|
|
|
|
private static boolean isIdentifierStart(char ch) {
|
|
return (ch == '\\') || Character.isJavaIdentifierStart(ch);
|
|
}
|
|
|
|
private static boolean isIdentifierPart(char ch) {
|
|
return (ch == '\\') || Character.isJavaIdentifierPart(ch);
|
|
}
|
|
|
|
private static boolean isTypeName(char ch) {
|
|
return "><(){}[],:*|?!=".indexOf(ch) == -1 && !isWhiteSpace(ch) && !isLineTerminator(ch);
|
|
}
|
|
|
|
private static boolean isParamTitle(String title) {
|
|
return title.equals("param") || title.equals("argument") || title.equals("arg");
|
|
}
|
|
|
|
private static boolean isProperty(String title) {
|
|
return title.equals("property") || title.equals("prop");
|
|
}
|
|
|
|
private static boolean isNameParameterRequired(String title) {
|
|
return isParamTitle(title)
|
|
|| isProperty(title)
|
|
|| title.equals("alias")
|
|
|| title.equals("this")
|
|
|| title.equals("mixes")
|
|
|| title.equals("requires");
|
|
}
|
|
|
|
private static boolean isAllowedName(String title) {
|
|
return isNameParameterRequired(title) || title.equals("const") || title.equals("constant");
|
|
}
|
|
|
|
private static boolean isAllowedNested(String title) {
|
|
return isProperty(title) || isParamTitle(title);
|
|
}
|
|
|
|
private static boolean isTypeParameterRequired(String title) {
|
|
return isParamTitle(title)
|
|
|| title.equals("define")
|
|
|| title.equals("enum")
|
|
|| title.equals("implements")
|
|
|| title.equals("return")
|
|
|| title.equals("this")
|
|
|| title.equals("type")
|
|
|| title.equals("typedef")
|
|
|| title.equals("returns")
|
|
|| isProperty(title);
|
|
}
|
|
|
|
// Consider deprecation instead using 'isTypeParameterRequired' and 'Rules' declaration to pick
|
|
// when a type is optional/required
|
|
// This would require changes to 'parseType'
|
|
private static boolean isAllowedType(String title) {
|
|
return isTypeParameterRequired(title)
|
|
|| title.equals("throws")
|
|
|| title.equals("const")
|
|
|| title.equals("constant")
|
|
|| title.equals("namespace")
|
|
|| title.equals("member")
|
|
|| title.equals("var")
|
|
|| title.equals("module")
|
|
|| title.equals("constructor")
|
|
|| title.equals("class")
|
|
|| title.equals("extends")
|
|
|| title.equals("augments")
|
|
|| title.equals("public")
|
|
|| title.equals("private")
|
|
|| title.equals("protected");
|
|
}
|
|
|
|
private static <T> T throwError(String message) throws ParseError {
|
|
throw new ParseError(message, null);
|
|
}
|
|
|
|
private enum Token {
|
|
ILLEGAL, // ILLEGAL
|
|
DOT, // .
|
|
DOT_LT, // .<
|
|
REST, // ...
|
|
LT, // <
|
|
GT, // >
|
|
LPAREN, // (
|
|
RPAREN, // )
|
|
LBRACE, // {
|
|
RBRACE, // }
|
|
LBRACK, // [
|
|
RBRACK, // ]
|
|
COMMA, // ,
|
|
COLON, // :
|
|
STAR, // *
|
|
PIPE, // |
|
|
QUESTION, // ?
|
|
BANG, // !
|
|
EQUAL, // =
|
|
NAME, // name token
|
|
STRING, // string
|
|
NUMBER, // number
|
|
EOF
|
|
};
|
|
|
|
private class TypeExpressionParser {
|
|
String source;
|
|
int length;
|
|
int previous, index;
|
|
Token token;
|
|
Object value;
|
|
|
|
private class Context {
|
|
int _previous, _index;
|
|
Token _token;
|
|
Object _value;
|
|
|
|
Context(int previous, int index, Token token, Object value) {
|
|
this._previous = previous;
|
|
this._index = index;
|
|
this._token = token;
|
|
this._value = value;
|
|
}
|
|
|
|
void restore() {
|
|
previous = this._previous;
|
|
index = this._index;
|
|
token = this._token;
|
|
value = this._value;
|
|
}
|
|
}
|
|
|
|
Context save() {
|
|
return new Context(previous, index, token, value);
|
|
}
|
|
|
|
private SourceLocation loc() {
|
|
return new SourceLocation(pos());
|
|
}
|
|
|
|
private Position pos() {
|
|
return new Position(1, index + 1, index);
|
|
}
|
|
|
|
private <T extends JSDocTypeExpression> T finishNode(T node) {
|
|
SourceLocation loc = node.getLoc();
|
|
Position end = pos();
|
|
loc.setSource(inputSubstring(loc.getStart().getOffset(), end.getOffset()));
|
|
loc.setEnd(end);
|
|
return node;
|
|
}
|
|
|
|
private String inputSubstring(int start, int end) {
|
|
if (start >= source.length()) return "";
|
|
if (end > source.length()) end = source.length();
|
|
return source.substring(start, end);
|
|
}
|
|
|
|
private int advance() {
|
|
if (index >= source.length()) return -1;
|
|
return source.charAt(index++);
|
|
}
|
|
|
|
private String scanHexEscape(char prefix) {
|
|
int i, len, ch, code = 0;
|
|
|
|
len = (prefix == 'u') ? 4 : 2;
|
|
for (i = 0; i < len; ++i) {
|
|
if (index < length && isHexDigit(source.charAt(index))) {
|
|
ch = advance();
|
|
code = code * 16 + "0123456789abcdef".indexOf(Character.toLowerCase(ch));
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
return new String(Character.toChars(code));
|
|
}
|
|
|
|
private Token scanString() throws ParseError {
|
|
StringBuilder str = new StringBuilder();
|
|
int quote, ch, code, restore; // TODO review removal octal = false
|
|
String unescaped;
|
|
quote = source.charAt(index);
|
|
++index;
|
|
|
|
while (index < length) {
|
|
ch = advance();
|
|
|
|
if (ch == quote) {
|
|
quote = -1;
|
|
break;
|
|
} else if (ch == '\\') {
|
|
ch = advance();
|
|
if (!isLineTerminator(ch)) {
|
|
switch (ch) {
|
|
case 'n':
|
|
str.append('\n');
|
|
break;
|
|
case 'r':
|
|
str.append('\r');
|
|
break;
|
|
case 't':
|
|
str.append('\t');
|
|
break;
|
|
case 'u':
|
|
case 'x':
|
|
restore = index;
|
|
unescaped = scanHexEscape((char) ch);
|
|
if (!unescaped.isEmpty()) {
|
|
str.append(unescaped);
|
|
} else {
|
|
index = restore;
|
|
str.append((char) ch);
|
|
}
|
|
break;
|
|
case 'b':
|
|
str.append('\b');
|
|
break;
|
|
case 'f':
|
|
str.append('\f');
|
|
break;
|
|
case 'v':
|
|
str.append('\u000b');
|
|
break;
|
|
|
|
default:
|
|
if (isOctalDigit((char) ch)) {
|
|
code = "01234567".indexOf(ch);
|
|
|
|
// \0 is not octal escape sequence
|
|
// Deprecating unused code. TODO review removal
|
|
// if (code != 0) {
|
|
// octal = true;
|
|
// }
|
|
|
|
if (index < length && isOctalDigit(source.charAt(index))) {
|
|
// TODO Review Removal octal = true;
|
|
code = code * 8 + "01234567".indexOf(advance());
|
|
|
|
// 3 digits are only allowed when string starts
|
|
// with 0, 1, 2, 3
|
|
if ("0123".indexOf(ch) >= 0
|
|
&& index < length
|
|
&& isOctalDigit(source.charAt(index))) {
|
|
code = code * 8 + "01234567".indexOf(advance());
|
|
}
|
|
}
|
|
str.append(Character.toChars(code));
|
|
} else {
|
|
str.append((char) ch);
|
|
}
|
|
break;
|
|
}
|
|
} else {
|
|
if (ch == '\r' && index < length && source.charAt(index) == '\n') {
|
|
++index;
|
|
}
|
|
}
|
|
} else if (isLineTerminator(ch)) {
|
|
break;
|
|
} else {
|
|
str.append((char) ch);
|
|
}
|
|
}
|
|
|
|
if (quote != -1) {
|
|
throwError("unexpected quote");
|
|
}
|
|
|
|
value = str.toString();
|
|
return Token.STRING;
|
|
}
|
|
|
|
private Token scanNumber() throws ParseError {
|
|
StringBuilder number = new StringBuilder();
|
|
boolean isFloat = false;
|
|
char ch = '\0';
|
|
|
|
if (ch != '.') {
|
|
int next = advance();
|
|
number.append((char) next);
|
|
ch = index < length ? source.charAt(index) : '\0';
|
|
|
|
if (next == '0') {
|
|
if (ch == 'x' || ch == 'X') {
|
|
number.append((char) advance());
|
|
while (index < length) {
|
|
ch = source.charAt(index);
|
|
if (!isHexDigit(ch)) {
|
|
break;
|
|
}
|
|
number.append((char) advance());
|
|
}
|
|
|
|
if (number.length() <= 2) {
|
|
// only 0x
|
|
throwError("unexpected token");
|
|
}
|
|
|
|
if (index < length) {
|
|
ch = source.charAt(index);
|
|
if (isIdentifierStart(ch)) {
|
|
throwError("unexpected token");
|
|
}
|
|
}
|
|
try {
|
|
value = Integer.parseInt(number.toString(), 16);
|
|
} catch (NumberFormatException nfe) {
|
|
Exceptions.ignore(nfe, "Precise exception content is unimportant");
|
|
throwError("Invalid hexadecimal constant " + number);
|
|
}
|
|
return Token.NUMBER;
|
|
}
|
|
|
|
if (isOctalDigit(ch)) {
|
|
number.append((char) advance());
|
|
while (index < length) {
|
|
ch = source.charAt(index);
|
|
if (!isOctalDigit(ch)) {
|
|
break;
|
|
}
|
|
number.append((char) advance());
|
|
}
|
|
|
|
if (index < length) {
|
|
ch = source.charAt(index);
|
|
if (isIdentifierStart(ch) || isDecimalDigit(ch)) {
|
|
throwError("unexpected token");
|
|
}
|
|
}
|
|
try {
|
|
value = Integer.parseInt(number.toString(), 8);
|
|
} catch (NumberFormatException nfe) {
|
|
Exceptions.ignore(nfe, "Precise exception content is unimportant");
|
|
throwError("Invalid octal constant " + number);
|
|
}
|
|
return Token.NUMBER;
|
|
}
|
|
|
|
if (isDecimalDigit(ch)) {
|
|
throwError("unexpected token");
|
|
}
|
|
}
|
|
|
|
while (index < length) {
|
|
ch = source.charAt(index);
|
|
if (!isDecimalDigit(ch)) {
|
|
break;
|
|
}
|
|
number.append((char) advance());
|
|
}
|
|
}
|
|
|
|
if (ch == '.') {
|
|
isFloat = true;
|
|
number.append((char) advance());
|
|
while (index < length) {
|
|
ch = source.charAt(index);
|
|
if (!isDecimalDigit(ch)) {
|
|
break;
|
|
}
|
|
number.append((char) advance());
|
|
}
|
|
}
|
|
|
|
if (ch == 'e' || ch == 'E') {
|
|
isFloat = true;
|
|
number.append((char) advance());
|
|
|
|
ch = index < length ? source.charAt(index) : '\0';
|
|
if (ch == '+' || ch == '-') {
|
|
number.append((char) advance());
|
|
}
|
|
|
|
ch = index < length ? source.charAt(index) : '\0';
|
|
if (isDecimalDigit(ch)) {
|
|
number.append((char) advance());
|
|
while (index < length) {
|
|
ch = source.charAt(index);
|
|
if (!isDecimalDigit(ch)) {
|
|
break;
|
|
}
|
|
number.append((char) advance());
|
|
}
|
|
} else {
|
|
throwError("unexpected token");
|
|
}
|
|
}
|
|
|
|
if (index < length) {
|
|
ch = source.charAt(index);
|
|
if (isIdentifierStart(ch)) {
|
|
throwError("unexpected token");
|
|
}
|
|
}
|
|
|
|
String num = number.toString();
|
|
try {
|
|
if (isFloat) value = Double.parseDouble(num);
|
|
else value = Integer.parseInt(num);
|
|
} catch (NumberFormatException nfe) {
|
|
Exceptions.ignore(nfe, "Precise exception content is unimportant");
|
|
throwError("Invalid numeric literal " + num);
|
|
}
|
|
return Token.NUMBER;
|
|
}
|
|
|
|
private Token scanTypeName() {
|
|
char ch, ch2;
|
|
|
|
value = new String(Character.toChars(advance()));
|
|
while (index < length && isTypeName(source.charAt(index))) {
|
|
ch = source.charAt(index);
|
|
if (ch == '.') {
|
|
if ((index + 1) < length) {
|
|
ch2 = source.charAt(index + 1);
|
|
if (ch2 == '<') {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
value += new String(Character.toChars(advance()));
|
|
}
|
|
return Token.NAME;
|
|
}
|
|
|
|
private Token next() throws ParseError {
|
|
char ch;
|
|
|
|
previous = index;
|
|
|
|
while (index < length && isWhiteSpace(source.charAt(index))) {
|
|
advance();
|
|
}
|
|
if (index >= length) {
|
|
token = Token.EOF;
|
|
return token;
|
|
}
|
|
|
|
ch = source.charAt(index);
|
|
switch (ch) {
|
|
case '"':
|
|
token = scanString();
|
|
return token;
|
|
|
|
case ':':
|
|
advance();
|
|
token = Token.COLON;
|
|
return token;
|
|
|
|
case ',':
|
|
advance();
|
|
token = Token.COMMA;
|
|
return token;
|
|
|
|
case '(':
|
|
advance();
|
|
token = Token.LPAREN;
|
|
return token;
|
|
|
|
case ')':
|
|
advance();
|
|
token = Token.RPAREN;
|
|
return token;
|
|
|
|
case '[':
|
|
advance();
|
|
token = Token.LBRACK;
|
|
return token;
|
|
|
|
case ']':
|
|
advance();
|
|
token = Token.RBRACK;
|
|
return token;
|
|
|
|
case '{':
|
|
advance();
|
|
token = Token.LBRACE;
|
|
return token;
|
|
|
|
case '}':
|
|
advance();
|
|
token = Token.RBRACE;
|
|
return token;
|
|
|
|
case '.':
|
|
advance();
|
|
if (index < length) {
|
|
ch = source.charAt(index);
|
|
if (ch == '<') {
|
|
advance();
|
|
token = Token.DOT_LT;
|
|
return token;
|
|
}
|
|
|
|
if (ch == '.' && index + 1 < length && source.charAt(index + 1) == '.') {
|
|
advance();
|
|
advance();
|
|
token = Token.REST;
|
|
return token;
|
|
}
|
|
|
|
if (isDecimalDigit(ch)) {
|
|
token = scanNumber();
|
|
return token;
|
|
}
|
|
}
|
|
token = Token.DOT;
|
|
return token;
|
|
|
|
case '<':
|
|
advance();
|
|
token = Token.LT;
|
|
return token;
|
|
|
|
case '>':
|
|
advance();
|
|
token = Token.GT;
|
|
return token;
|
|
|
|
case '*':
|
|
advance();
|
|
token = Token.STAR;
|
|
return token;
|
|
|
|
case '|':
|
|
advance();
|
|
token = Token.PIPE;
|
|
return token;
|
|
|
|
case '?':
|
|
advance();
|
|
token = Token.QUESTION;
|
|
return token;
|
|
|
|
case '!':
|
|
advance();
|
|
token = Token.BANG;
|
|
return token;
|
|
|
|
case '=':
|
|
advance();
|
|
token = Token.EQUAL;
|
|
return token;
|
|
|
|
default:
|
|
if (isDecimalDigit(ch)) {
|
|
token = scanNumber();
|
|
return token;
|
|
}
|
|
|
|
// type string permits following case,
|
|
//
|
|
// namespace.module.MyClass
|
|
//
|
|
// this reduced 1 token TK_NAME
|
|
if (isTypeName(ch)) {
|
|
token = scanTypeName();
|
|
return token;
|
|
}
|
|
|
|
token = Token.ILLEGAL;
|
|
return token;
|
|
}
|
|
}
|
|
|
|
private void consume(Token target, String text) throws ParseError {
|
|
if (token != target) throwError(text == null ? "consumed token not matched" : text);
|
|
next();
|
|
}
|
|
|
|
private void consume(Token target) throws ParseError {
|
|
consume(target, null);
|
|
}
|
|
|
|
private void expect(Token target) throws ParseError {
|
|
if (token != target) {
|
|
throwError("unexpected token");
|
|
}
|
|
next();
|
|
}
|
|
|
|
// UnionType := '(' TypeUnionList ')'
|
|
//
|
|
// TypeUnionList :=
|
|
// <<empty>>
|
|
// | NonemptyTypeUnionList
|
|
//
|
|
// NonemptyTypeUnionList :=
|
|
// TypeExpression
|
|
// | TypeExpression '|' NonemptyTypeUnionList
|
|
private JSDocTypeExpression parseUnionType() throws ParseError {
|
|
SourceLocation loc = loc();
|
|
List<JSDocTypeExpression> elements = new ArrayList<>();
|
|
consume(Token.LPAREN, "UnionType should start with (");
|
|
if (token != Token.RPAREN) {
|
|
while (true) {
|
|
elements.add(parseTypeExpression());
|
|
if (token == Token.RPAREN) {
|
|
break;
|
|
}
|
|
expect(Token.PIPE);
|
|
}
|
|
}
|
|
consume(Token.RPAREN, "UnionType should end with )");
|
|
return finishNode(new UnionType(loc, elements));
|
|
}
|
|
|
|
// ArrayType := '[' ElementTypeList ']'
|
|
//
|
|
// ElementTypeList :=
|
|
// <<empty>>
|
|
// | TypeExpression
|
|
// | '...' TypeExpression
|
|
// | TypeExpression ',' ElementTypeList
|
|
private JSDocTypeExpression parseArrayType() throws ParseError {
|
|
SourceLocation loc = loc();
|
|
List<JSDocTypeExpression> elements = new ArrayList<>();
|
|
consume(Token.LBRACK, "ArrayType should start with [");
|
|
while (token != Token.RBRACK) {
|
|
loc = loc();
|
|
if (token == Token.REST) {
|
|
consume(Token.REST);
|
|
elements.add(finishNode(new RestType(loc, parseTypeExpression())));
|
|
break;
|
|
} else {
|
|
elements.add(parseTypeExpression());
|
|
}
|
|
if (token != Token.RBRACK) {
|
|
expect(Token.COMMA);
|
|
}
|
|
}
|
|
expect(Token.RBRACK);
|
|
return finishNode(new ArrayType(loc, elements));
|
|
}
|
|
|
|
private String parseFieldName() throws ParseError {
|
|
Object v = value;
|
|
if (token == Token.NAME || token == Token.STRING) {
|
|
next();
|
|
return v.toString();
|
|
}
|
|
|
|
if (token == Token.NUMBER) {
|
|
consume(Token.NUMBER);
|
|
return v.toString();
|
|
}
|
|
|
|
return throwError("unexpected token");
|
|
}
|
|
|
|
// FieldType :=
|
|
// FieldName
|
|
// | FieldName ':' TypeExpression
|
|
//
|
|
// FieldName :=
|
|
// NameExpression
|
|
// | StringLiteral
|
|
// | NumberLiteral
|
|
// | ReservedIdentifier
|
|
private FieldType parseFieldType() throws ParseError {
|
|
String key;
|
|
SourceLocation loc = loc();
|
|
key = parseFieldName();
|
|
if (token == Token.COLON) {
|
|
consume(Token.COLON);
|
|
return finishNode(new FieldType(loc, key, parseTypeExpression()));
|
|
}
|
|
return finishNode(new FieldType(loc, key, null));
|
|
}
|
|
|
|
// RecordType := '{' FieldTypeList '}'
|
|
//
|
|
// FieldTypeList :=
|
|
// <<empty>>
|
|
// | FieldType
|
|
// | FieldType ',' FieldTypeList
|
|
private JSDocTypeExpression parseRecordType() throws ParseError {
|
|
List<FieldType> fields = new ArrayList<>();
|
|
SourceLocation loc = loc();
|
|
consume(Token.LBRACE, "RecordType should start with {");
|
|
if (token == Token.COMMA) {
|
|
consume(Token.COMMA);
|
|
} else {
|
|
while (token != Token.RBRACE) {
|
|
fields.add(parseFieldType());
|
|
if (token != Token.RBRACE) {
|
|
expect(Token.COMMA);
|
|
}
|
|
}
|
|
}
|
|
expect(Token.RBRACE);
|
|
return finishNode(new RecordType(loc, fields));
|
|
}
|
|
|
|
private JSDocTypeExpression parseNameExpression() throws ParseError {
|
|
Object name = value;
|
|
SourceLocation loc = loc();
|
|
expect(Token.NAME);
|
|
return finishNode(new NameExpression(loc, name.toString()));
|
|
}
|
|
|
|
// TypeExpressionList :=
|
|
// TopLevelTypeExpression
|
|
// | TopLevelTypeExpression ',' TypeExpressionList
|
|
private List<JSDocTypeExpression> parseTypeExpressionList() throws ParseError {
|
|
List<JSDocTypeExpression> elements = new ArrayList<>();
|
|
|
|
elements.add(parseTop());
|
|
while (token == Token.COMMA) {
|
|
consume(Token.COMMA);
|
|
elements.add(parseTop());
|
|
}
|
|
return elements;
|
|
}
|
|
|
|
// TypeName :=
|
|
// NameExpression
|
|
// | NameExpression TypeApplication
|
|
//
|
|
// TypeApplication :=
|
|
// '.<' TypeExpressionList '>'
|
|
// | '<' TypeExpressionList '>' // this is extension of doctrine
|
|
private JSDocTypeExpression parseTypeName() throws ParseError {
|
|
JSDocTypeExpression expr;
|
|
List<JSDocTypeExpression> applications;
|
|
SourceLocation loc = loc();
|
|
expr = parseNameExpression();
|
|
if (token == Token.DOT_LT || token == Token.LT) {
|
|
next();
|
|
applications = parseTypeExpressionList();
|
|
expect(Token.GT);
|
|
return finishNode(new TypeApplication(loc, expr, applications));
|
|
}
|
|
return expr;
|
|
}
|
|
|
|
// ResultType :=
|
|
// <<empty>>
|
|
// | ':' void
|
|
// | ':' TypeExpression
|
|
//
|
|
// BNF is above
|
|
// but, we remove <<empty>> pattern, so token is always TypeToken::COLON
|
|
private JSDocTypeExpression parseResultType() throws ParseError {
|
|
consume(Token.COLON, "ResultType should start with :");
|
|
SourceLocation loc = loc();
|
|
if (token == Token.NAME && value.equals("void")) {
|
|
consume(Token.NAME);
|
|
return finishNode(new VoidLiteral(loc));
|
|
}
|
|
return parseTypeExpression();
|
|
}
|
|
|
|
// ParametersType :=
|
|
// RestParameterType
|
|
// | NonRestParametersType
|
|
// | NonRestParametersType ',' RestParameterType
|
|
//
|
|
// RestParameterType :=
|
|
// '...'
|
|
// '...' Identifier
|
|
//
|
|
// NonRestParametersType :=
|
|
// ParameterType ',' NonRestParametersType
|
|
// | ParameterType
|
|
// | OptionalParametersType
|
|
//
|
|
// OptionalParametersType :=
|
|
// OptionalParameterType
|
|
// | OptionalParameterType, OptionalParametersType
|
|
//
|
|
// OptionalParameterType := ParameterType=
|
|
//
|
|
// ParameterType := TypeExpression | Identifier ':' TypeExpression
|
|
//
|
|
// Identifier is "new" or "this"
|
|
private List<JSDocTypeExpression> parseParametersType() throws ParseError {
|
|
List<JSDocTypeExpression> params = new ArrayList<>();
|
|
boolean normal = true;
|
|
JSDocTypeExpression expr;
|
|
boolean rest = false;
|
|
|
|
while (token != Token.RPAREN) {
|
|
if (token == Token.REST) {
|
|
// RestParameterType
|
|
consume(Token.REST);
|
|
rest = true;
|
|
}
|
|
|
|
SourceLocation loc = loc();
|
|
expr = parseTypeExpression();
|
|
if (expr instanceof NameExpression && token == Token.COLON) {
|
|
// Identifier ':' TypeExpression
|
|
consume(Token.COLON);
|
|
expr =
|
|
finishNode(
|
|
new ParameterType(loc, ((NameExpression) expr).getName(), parseTypeExpression()));
|
|
}
|
|
if (token == Token.EQUAL) {
|
|
consume(Token.EQUAL);
|
|
expr = finishNode(new OptionalType(loc, expr));
|
|
normal = false;
|
|
} else {
|
|
if (!normal) {
|
|
throwError("unexpected token");
|
|
}
|
|
}
|
|
if (rest) {
|
|
expr = finishNode(new RestType(new SourceLocation(loc), expr));
|
|
}
|
|
params.add(expr);
|
|
if (token != Token.RPAREN) {
|
|
expect(Token.COMMA);
|
|
}
|
|
}
|
|
return params;
|
|
}
|
|
|
|
// FunctionType := 'function' FunctionSignatureType
|
|
//
|
|
// FunctionSignatureType :=
|
|
// | TypeParameters '(' ')' ResultType
|
|
// | TypeParameters '(' ParametersType ')' ResultType
|
|
// | TypeParameters '(' 'this' ':' TypeName ')' ResultType
|
|
// | TypeParameters '(' 'this' ':' TypeName ',' ParametersType ')' ResultType
|
|
private JSDocTypeExpression parseFunctionType() throws ParseError {
|
|
SourceLocation loc = loc();
|
|
boolean isNew;
|
|
JSDocTypeExpression thisBinding;
|
|
List<JSDocTypeExpression> params;
|
|
JSDocTypeExpression result;
|
|
consume(Token.NAME);
|
|
|
|
// Google Closure Compiler is not implementing TypeParameters.
|
|
// So we do not. if we don't get '(', we see it as error.
|
|
expect(Token.LPAREN);
|
|
|
|
isNew = false;
|
|
params = new ArrayList<JSDocTypeExpression>();
|
|
thisBinding = null;
|
|
if (token != Token.RPAREN) {
|
|
// ParametersType or 'this'
|
|
if (token == Token.NAME && (value.equals("this") || value.equals("new"))) {
|
|
// 'this' or 'new'
|
|
// 'new' is Closure Compiler extension
|
|
isNew = value.equals("new");
|
|
consume(Token.NAME);
|
|
expect(Token.COLON);
|
|
thisBinding = parseTypeName();
|
|
if (token == Token.COMMA) {
|
|
consume(Token.COMMA);
|
|
params = parseParametersType();
|
|
}
|
|
} else {
|
|
params = parseParametersType();
|
|
}
|
|
}
|
|
|
|
expect(Token.RPAREN);
|
|
|
|
result = null;
|
|
if (token == Token.COLON) {
|
|
result = parseResultType();
|
|
}
|
|
|
|
return finishNode(new FunctionType(loc, thisBinding, isNew, params, result));
|
|
}
|
|
|
|
// BasicTypeExpression :=
|
|
// '*'
|
|
// | 'null'
|
|
// | 'undefined'
|
|
// | TypeName
|
|
// | FunctionType
|
|
// | UnionType
|
|
// | RecordType
|
|
// | ArrayType
|
|
private JSDocTypeExpression parseBasicTypeExpression() throws ParseError {
|
|
Context context;
|
|
SourceLocation loc;
|
|
switch (token) {
|
|
case STAR:
|
|
loc = loc();
|
|
consume(Token.STAR);
|
|
return finishNode(new AllLiteral(loc));
|
|
|
|
case LPAREN:
|
|
return parseUnionType();
|
|
|
|
case LBRACK:
|
|
return parseArrayType();
|
|
|
|
case LBRACE:
|
|
return parseRecordType();
|
|
|
|
case NAME:
|
|
if (value.equals("null")) {
|
|
loc = loc();
|
|
consume(Token.NAME);
|
|
return finishNode(new NullLiteral(loc));
|
|
}
|
|
|
|
if (value.equals("undefined")) {
|
|
loc = loc();
|
|
consume(Token.NAME);
|
|
return finishNode(new UndefinedLiteral(loc));
|
|
}
|
|
|
|
context = save();
|
|
if (value.equals("function")) {
|
|
try {
|
|
return parseFunctionType();
|
|
} catch (ParseError e) {
|
|
context.restore();
|
|
}
|
|
}
|
|
|
|
return parseTypeName();
|
|
|
|
default:
|
|
return throwError("unexpected token");
|
|
}
|
|
}
|
|
|
|
// TypeExpression :=
|
|
// BasicTypeExpression
|
|
// | '?' BasicTypeExpression
|
|
// | '!' BasicTypeExpression
|
|
// | BasicTypeExpression '?'
|
|
// | BasicTypeExpression '!'
|
|
// | '?'
|
|
// | BasicTypeExpression '[]'
|
|
private JSDocTypeExpression parseTypeExpression() throws ParseError {
|
|
JSDocTypeExpression expr;
|
|
SourceLocation loc = loc();
|
|
|
|
if (token == Token.QUESTION) {
|
|
consume(Token.QUESTION);
|
|
if (token == Token.COMMA
|
|
|| token == Token.EQUAL
|
|
|| token == Token.RBRACE
|
|
|| token == Token.RPAREN
|
|
|| token == Token.PIPE
|
|
|| token == Token.EOF
|
|
|| token == Token.RBRACK) {
|
|
return finishNode(new NullableLiteral(loc));
|
|
}
|
|
return finishNode(new NullableType(loc, parseBasicTypeExpression(), true));
|
|
}
|
|
|
|
if (token == Token.BANG) {
|
|
consume(Token.BANG);
|
|
return finishNode(new NonNullableType(loc, parseBasicTypeExpression(), true));
|
|
}
|
|
|
|
expr = parseBasicTypeExpression();
|
|
if (token == Token.BANG) {
|
|
consume(Token.BANG);
|
|
return finishNode(new NonNullableType(loc, expr, false));
|
|
}
|
|
|
|
if (token == Token.QUESTION) {
|
|
consume(Token.QUESTION);
|
|
return finishNode(new NullableType(loc, expr, false));
|
|
}
|
|
|
|
if (token == Token.LBRACK) {
|
|
consume(Token.LBRACK);
|
|
consume(Token.RBRACK, "expected an array-style type declaration (' + value + '[])");
|
|
List<JSDocTypeExpression> expressions = new ArrayList<>();
|
|
expressions.add(expr);
|
|
return finishNode(new TypeApplication(loc, new NameExpression(loc, "Array"), expressions));
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
// TopLevelTypeExpression :=
|
|
// TypeExpression
|
|
// | TypeUnionList
|
|
//
|
|
// This rule is Google Closure Compiler extension, not ES4
|
|
// like,
|
|
// { number | string }
|
|
// If strict to ES4, we should write it as
|
|
// { (number|string) }
|
|
private JSDocTypeExpression parseTop() throws ParseError {
|
|
JSDocTypeExpression expr;
|
|
List<JSDocTypeExpression> elements = new ArrayList<JSDocTypeExpression>();
|
|
SourceLocation loc = loc();
|
|
|
|
expr = parseTypeExpression();
|
|
if (token != Token.PIPE) {
|
|
return expr;
|
|
}
|
|
|
|
elements.add(expr);
|
|
consume(Token.PIPE);
|
|
while (true) {
|
|
elements.add(parseTypeExpression());
|
|
if (token != Token.PIPE) {
|
|
break;
|
|
}
|
|
consume(Token.PIPE);
|
|
}
|
|
|
|
return finishNode(new UnionType(loc, elements));
|
|
}
|
|
|
|
private JSDocTypeExpression parseTopParamType() throws ParseError {
|
|
JSDocTypeExpression expr;
|
|
SourceLocation loc = loc();
|
|
|
|
if (token == Token.REST) {
|
|
consume(Token.REST);
|
|
return finishNode(new RestType(loc, parseTop()));
|
|
}
|
|
|
|
expr = parseTop();
|
|
if (token == Token.EQUAL) {
|
|
consume(Token.EQUAL);
|
|
return finishNode(new OptionalType(loc, expr));
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
private JSDocTypeExpression parseType(String src) throws ParseError {
|
|
JSDocTypeExpression expr;
|
|
|
|
source = src;
|
|
length = source.length();
|
|
index = 0;
|
|
previous = 0;
|
|
|
|
next();
|
|
expr = parseTop();
|
|
|
|
if (token != Token.EOF) {
|
|
throwError("not reach to EOF");
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
|
|
private JSDocTypeExpression parseParamType(String src) throws ParseError {
|
|
JSDocTypeExpression expr;
|
|
|
|
source = src;
|
|
length = source.length();
|
|
index = 0;
|
|
previous = 0;
|
|
|
|
next();
|
|
expr = parseTopParamType();
|
|
|
|
if (token != Token.EOF) {
|
|
throwError("not reach to EOF");
|
|
}
|
|
|
|
return expr;
|
|
}
|
|
}
|
|
|
|
private TypeExpressionParser typed = new TypeExpressionParser();
|
|
|
|
private class JSDocTagParser {
|
|
int index, lineNumber, lineStart, length;
|
|
String source;
|
|
boolean recoverable = true, sloppy = false;
|
|
|
|
private int skipStars(int index) {
|
|
while (index < length
|
|
&& isWhiteSpace(source.charAt(index))
|
|
&& !isLineTerminator(source.charAt(index))) {
|
|
index += 1;
|
|
}
|
|
while (index < length && source.charAt(index) == '*') {
|
|
index += 1;
|
|
}
|
|
while (index < length
|
|
&& isWhiteSpace(source.charAt(index))
|
|
&& !isLineTerminator(source.charAt(index))) {
|
|
index += 1;
|
|
}
|
|
return index;
|
|
}
|
|
|
|
private char advance() {
|
|
char ch = source.charAt(index);
|
|
index += 1;
|
|
if (isLineTerminator(ch) && !(ch == '\r' && index < length && source.charAt(index) == '\n')) {
|
|
lineNumber += 1;
|
|
lineStart = index;
|
|
index = skipStars(index);
|
|
}
|
|
return ch;
|
|
}
|
|
|
|
private String scanTitle() {
|
|
StringBuilder title = new StringBuilder();
|
|
// waste '@'
|
|
advance();
|
|
|
|
while (index < length && isASCIIAlphanumeric(source.charAt(index))) {
|
|
title.append(advance());
|
|
}
|
|
|
|
return title.toString();
|
|
}
|
|
|
|
private int seekContent() {
|
|
char ch;
|
|
boolean waiting = false;
|
|
int last = index;
|
|
|
|
while (last < length) {
|
|
ch = source.charAt(last);
|
|
if (isLineTerminator(ch)
|
|
&& !(ch == '\r' && last + 1 < length && source.charAt(last + 1) == '\n')) {
|
|
lineNumber += 1;
|
|
lineStart = last + 1;
|
|
last = skipStars(last + 1) - 1;
|
|
waiting = true;
|
|
} else if (waiting) {
|
|
if (ch == '@') {
|
|
break;
|
|
}
|
|
if (!isWhiteSpace(ch)) {
|
|
waiting = false;
|
|
}
|
|
}
|
|
last += 1;
|
|
}
|
|
return last;
|
|
}
|
|
|
|
// type expression may have nest brace, such as,
|
|
// { { ok: string } }
|
|
//
|
|
// therefore, scanning type expression with balancing braces.
|
|
private JSDocTypeExpression parseType(String title, int last) throws ParseError {
|
|
char ch;
|
|
int brace;
|
|
StringBuilder type;
|
|
boolean direct = false;
|
|
|
|
// search '{'
|
|
while (index < last) {
|
|
ch = source.charAt(index);
|
|
if (isWhiteSpace(ch)) {
|
|
advance();
|
|
} else if (ch == '{') {
|
|
advance();
|
|
break;
|
|
} else {
|
|
// this is direct pattern
|
|
direct = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!direct) {
|
|
// type expression { is found
|
|
brace = 1;
|
|
type = new StringBuilder();
|
|
while (index < last) {
|
|
ch = source.charAt(index);
|
|
if (isLineTerminator(ch)) {
|
|
advance();
|
|
} else {
|
|
if (ch == '}') {
|
|
brace -= 1;
|
|
if (brace == 0) {
|
|
advance();
|
|
break;
|
|
}
|
|
} else if (ch == '{') {
|
|
brace += 1;
|
|
}
|
|
type.append(advance());
|
|
}
|
|
}
|
|
|
|
if (brace != 0) {
|
|
// braces is not balanced
|
|
return throwError("Braces are not balanced");
|
|
}
|
|
|
|
try {
|
|
if (isParamTitle(title)) {
|
|
return typed.parseParamType(type.toString());
|
|
}
|
|
return typed.parseType(type.toString());
|
|
} catch (ParseError e) {
|
|
// parse failed
|
|
return null;
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private String scanIdentifier(int last) {
|
|
StringBuilder identifier = new StringBuilder();
|
|
if (!(index < length && isIdentifierStart(source.charAt(index)))) {
|
|
return null;
|
|
}
|
|
identifier.append(advance());
|
|
while (index < last && isIdentifierPart(source.charAt(index))) {
|
|
identifier.append(advance());
|
|
}
|
|
return identifier.toString();
|
|
}
|
|
|
|
private void skipWhiteSpace(int last) {
|
|
while (index < last
|
|
&& (isWhiteSpace(source.charAt(index)) || isLineTerminator(source.charAt(index)))) {
|
|
advance();
|
|
}
|
|
}
|
|
|
|
private String parseName(int last, boolean allowBrackets, boolean allowNestedParams) {
|
|
StringBuilder name = new StringBuilder();
|
|
boolean useBrackets = false;
|
|
|
|
skipWhiteSpace(last);
|
|
|
|
if (index >= last) {
|
|
return null;
|
|
}
|
|
|
|
if (allowBrackets && source.charAt(index) == '[') {
|
|
useBrackets = true;
|
|
name.append(advance());
|
|
}
|
|
|
|
if (!isIdentifierStart(source.charAt(index))) {
|
|
return null;
|
|
}
|
|
|
|
name.append(scanIdentifier(last));
|
|
|
|
if (allowNestedParams) {
|
|
while (index < last
|
|
&& (source.charAt(index) == '.'
|
|
|| source.charAt(index) == '#'
|
|
|| source.charAt(index) == '~')) {
|
|
name.append(source.charAt(index));
|
|
index += 1;
|
|
name.append(scanIdentifier(last));
|
|
}
|
|
}
|
|
|
|
if (useBrackets) {
|
|
// do we have a default value for this?
|
|
if (index < last && source.charAt(index) == '=') {
|
|
|
|
// consume the '='' symbol
|
|
name.append(advance());
|
|
// scan in the default value
|
|
while (index < last && source.charAt(index) != ']') {
|
|
name.append(advance());
|
|
}
|
|
}
|
|
|
|
if (index >= last || source.charAt(index) != ']') {
|
|
// we never found a closing ']'
|
|
return null;
|
|
}
|
|
|
|
// collect the last ']'
|
|
name.append(advance());
|
|
}
|
|
|
|
return name.toString();
|
|
}
|
|
|
|
boolean skipToTag() {
|
|
while (index < length && source.charAt(index) != '@') {
|
|
advance();
|
|
}
|
|
if (index >= length) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private class Tag {
|
|
public String description;
|
|
public String title;
|
|
List<String> errors = new ArrayList<>();
|
|
JSDocTypeExpression type;
|
|
String name;
|
|
public int startLine;
|
|
public int startColumn;
|
|
}
|
|
|
|
private class TagParser {
|
|
String _title;
|
|
Tag _tag;
|
|
int _last;
|
|
String _extra_name;
|
|
|
|
TagParser(String title) {
|
|
this._title = title;
|
|
this._tag = new Tag();
|
|
this._tag.description = null;
|
|
this._tag.title = title;
|
|
this._last = 0;
|
|
// space to save special information for title parsers.
|
|
this._extra_name = null;
|
|
}
|
|
|
|
// addError(err, ...)
|
|
public boolean addError(String errorText, Object... args) {
|
|
this._tag.errors.add(String.format(errorText, args));
|
|
return recoverable;
|
|
}
|
|
|
|
public boolean parseType() {
|
|
// type required titles
|
|
if (isTypeParameterRequired(this._title)) {
|
|
try {
|
|
this._tag.type = JSDocTagParser.this.parseType(this._title, this._last);
|
|
if (this._tag.type == null) {
|
|
if (!isParamTitle(this._title)) {
|
|
if (!this.addError("Missing or invalid tag type")) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
} catch (ParseError error) {
|
|
this._tag.type = null;
|
|
if (!this.addError(error.getMessage())) {
|
|
return false;
|
|
}
|
|
}
|
|
} else if (isAllowedType(this._title)) {
|
|
// optional types
|
|
try {
|
|
this._tag.type = JSDocTagParser.this.parseType(this._title, this._last);
|
|
} catch (ParseError e) {
|
|
// For optional types, lets drop the thrown error when we hit the end of the file
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private boolean _parseNamePath(boolean optional) {
|
|
String name =
|
|
JSDocTagParser.this.parseName(this._last, sloppy && isParamTitle(this._title), true);
|
|
if (name == null) {
|
|
if (!optional) {
|
|
if (!this.addError("Missing or invalid tag name")) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
this._tag.name = name;
|
|
return true;
|
|
}
|
|
|
|
public boolean parseNamePath() {
|
|
return _parseNamePath(false);
|
|
}
|
|
|
|
public boolean parseNamePathOptional() {
|
|
return this._parseNamePath(true);
|
|
}
|
|
|
|
public boolean parseName() {
|
|
String[] assign;
|
|
String name;
|
|
|
|
// param, property requires name
|
|
if (isAllowedName(this._title)) {
|
|
this._tag.name =
|
|
JSDocTagParser.this.parseName(
|
|
this._last, sloppy && isParamTitle(this._title), isAllowedNested(this._title));
|
|
if (this._tag.name == null) {
|
|
if (!isNameParameterRequired(this._title)) {
|
|
return true;
|
|
}
|
|
|
|
// it's possible the name has already been parsed but interpreted as a type
|
|
// it's also possible this is a sloppy declaration, in which case it will be
|
|
// fixed at the end
|
|
if (isParamTitle(this._title)
|
|
&& this._tag.type != null
|
|
&& this._tag.type instanceof NameExpression) {
|
|
this._extra_name = ((NameExpression) this._tag.type).getName();
|
|
this._tag.name = ((NameExpression) this._tag.type).getName();
|
|
this._tag.type = null;
|
|
} else {
|
|
if (!this.addError("Missing or invalid tag name")) {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
name = this._tag.name;
|
|
if (name.charAt(0) == '[' && name.charAt(name.length() - 1) == ']') {
|
|
// extract the default value if there is one
|
|
// example: @param {string} [somebody=John Doe] description
|
|
assign = name.substring(1, name.length() - 1).split("=");
|
|
this._tag.name = assign[0];
|
|
|
|
// convert to an optional type
|
|
if (this._tag.type != null && !(this._tag.type instanceof OptionalType)) {
|
|
Position start = new Position(_tag.startLine, _tag.startColumn, _tag.startColumn);
|
|
Position end = new Position(_tag.startLine, _tag.startColumn, _tag.startColumn);
|
|
SourceLocation loc = new SourceLocation(_extra_name, start, end);
|
|
this._tag.type = new OptionalType(loc, this._tag.type);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private boolean parseDescription() {
|
|
String description = sliceSource(source, index, this._last).trim();
|
|
if (!description.isEmpty()) {
|
|
if (description.matches("(?s)^-\\s+.*")) {
|
|
description = description.substring(2);
|
|
}
|
|
description = description.replaceAll("(?m)^\\s*\\*+\\s*", "");
|
|
this._tag.description = description;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private final Set<String> kinds = new LinkedHashSet<>();
|
|
|
|
{
|
|
kinds.add("class");
|
|
kinds.add("constant");
|
|
kinds.add("event");
|
|
kinds.add("external");
|
|
kinds.add("file");
|
|
kinds.add("function");
|
|
kinds.add("member");
|
|
kinds.add("mixin");
|
|
kinds.add("module");
|
|
kinds.add("namespace");
|
|
kinds.add("typedef");
|
|
}
|
|
|
|
private boolean parseKind() {
|
|
String kind = sliceSource(source, index, this._last).trim();
|
|
if (!kinds.contains(kind)) {
|
|
if (!this.addError("Invalid kind name \'%s\'", kind)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private boolean parseAccess() {
|
|
String access = sliceSource(source, index, this._last).trim();
|
|
if (!access.equals("private") && !access.equals("protected") && !access.equals("public")) {
|
|
if (!this.addError("Invalid access name \'%s\'", access)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private boolean parseVariation() {
|
|
double variation;
|
|
String text = sliceSource(source, index, this._last).trim();
|
|
try {
|
|
variation = Double.parseDouble(text);
|
|
} catch (NumberFormatException nfe) {
|
|
variation = Double.NaN;
|
|
}
|
|
if (Double.isNaN(variation)) {
|
|
if (!this.addError("Invalid variation \'%s\'", text)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private boolean ensureEnd() {
|
|
String shouldBeEmpty = sliceSource(source, index, this._last).trim();
|
|
if (!shouldBeEmpty.matches("^[\\s*]*$")) {
|
|
if (!this.addError("Unknown content \'%s\'", shouldBeEmpty)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private boolean epilogue() {
|
|
String description;
|
|
|
|
description = this._tag.description;
|
|
// un-fix potentially sloppy declaration
|
|
if (isParamTitle(this._title)
|
|
&& this._tag.type == null
|
|
&& description != null
|
|
&& description.startsWith("[")) {
|
|
if (_extra_name != null) {
|
|
Position start = new Position(_tag.startLine, _tag.startColumn, _tag.startColumn);
|
|
Position end = new Position(_tag.startLine, _tag.startColumn, _tag.startColumn);
|
|
SourceLocation loc = new SourceLocation(_extra_name, start, end);
|
|
this._tag.type = new NameExpression(loc, _extra_name);
|
|
}
|
|
this._tag.name = null;
|
|
|
|
if (!sloppy) {
|
|
if (!this.addError("Missing or invalid tag name")) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private Tag parse() {
|
|
int oldLineNumber, oldLineStart, newLineNumber, newLineStart;
|
|
|
|
// empty title
|
|
if (this._title == null || this._title.isEmpty()) {
|
|
if (!this.addError("Missing or invalid title")) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Seek to content last index.
|
|
oldLineNumber = lineNumber;
|
|
oldLineStart = lineStart;
|
|
this._last = seekContent();
|
|
newLineNumber = lineNumber;
|
|
newLineStart = lineStart;
|
|
lineNumber = oldLineNumber;
|
|
lineStart = oldLineStart;
|
|
|
|
switch (this._title) {
|
|
// http://usejsdoc.org/tags-access.html
|
|
case "access":
|
|
if (!parseAccess()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-alias.html
|
|
case "alias":
|
|
if (!parseNamePath() || !ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-augments.html
|
|
case "augments":
|
|
if (!parseType() || !parseNamePathOptional() || !ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-constructor.html
|
|
case "constructor":
|
|
if (!parseType() || !parseNamePathOptional() || !ensureEnd()) return null;
|
|
break;
|
|
// Synonym: http://usejsdoc.org/tags-constructor.html
|
|
case "class":
|
|
if (!parseType() || !parseNamePathOptional() || !ensureEnd()) return null;
|
|
break;
|
|
// Synonym: http://usejsdoc.org/tags-extends.html
|
|
case "extends":
|
|
if (!parseType() || !parseNamePathOptional() || !ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-deprecated.html
|
|
case "deprecated":
|
|
if (!parseDescription()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-global.html
|
|
case "global":
|
|
if (!ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-inner.html
|
|
case "inner":
|
|
if (!ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-instance.html
|
|
case "instance":
|
|
if (!ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-kind.html
|
|
case "kind":
|
|
if (!parseKind()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-mixes.html
|
|
case "mixes":
|
|
if (!parseNamePath() || !ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-mixin.html
|
|
case "mixin":
|
|
if (!parseNamePathOptional() || !ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-member.html
|
|
case "member":
|
|
if (!parseType() || !parseNamePathOptional() || !ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-method.html
|
|
case "method":
|
|
if (!parseNamePathOptional() || !ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-module.html
|
|
case "module":
|
|
if (!parseType() || !parseNamePathOptional() || !ensureEnd()) return null;
|
|
break;
|
|
// Synonym: http://usejsdoc.org/tags-method.html
|
|
case "func":
|
|
if (!parseNamePathOptional() || !ensureEnd()) return null;
|
|
break;
|
|
// Synonym: http://usejsdoc.org/tags-method.html
|
|
case "function":
|
|
if (!parseNamePathOptional() || !ensureEnd()) return null;
|
|
break;
|
|
// Synonym: http://usejsdoc.org/tags-member.html
|
|
case "var":
|
|
if (!parseType() || !parseNamePathOptional() || !ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-name.html
|
|
case "name":
|
|
if (!parseNamePath() || !ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-namespace.html
|
|
case "namespace":
|
|
if (!parseType() || !parseNamePathOptional() || !ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-private.html
|
|
case "private":
|
|
if (!parseType() || !parseDescription()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-protected.html
|
|
case "protected":
|
|
if (!parseType() || !parseDescription()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-public.html
|
|
case "public":
|
|
if (!parseType() || !parseDescription()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-readonly.html
|
|
case "readonly":
|
|
if (!ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-requires.html
|
|
case "requires":
|
|
if (!parseNamePath() || !ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-since.html
|
|
case "since":
|
|
if (!parseDescription()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-static.html
|
|
case "static":
|
|
if (!ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-summary.html
|
|
case "summary":
|
|
if (!parseDescription()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-this.html
|
|
case "this":
|
|
if (!parseNamePath() || !ensureEnd()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-todo.html
|
|
case "todo":
|
|
if (!parseDescription()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-variation.html
|
|
case "variation":
|
|
if (!parseVariation()) return null;
|
|
break;
|
|
// http://usejsdoc.org/tags-version.html
|
|
case "version":
|
|
if (!parseDescription()) return null;
|
|
break;
|
|
// default sequences
|
|
default:
|
|
if (!parseType() || !parseName() || !parseDescription() || !epilogue()) return null;
|
|
break;
|
|
}
|
|
|
|
// Seek global index to end of this tag.
|
|
index = this._last;
|
|
lineNumber = newLineNumber;
|
|
lineStart = newLineStart;
|
|
return this._tag;
|
|
}
|
|
|
|
private Tag parseTag() {
|
|
String title;
|
|
Tag res;
|
|
TagParser parser;
|
|
int startColumn;
|
|
int startLine;
|
|
|
|
// skip to tag
|
|
if (!skipToTag()) {
|
|
return null;
|
|
}
|
|
|
|
startLine = lineNumber;
|
|
startColumn = index - lineStart;
|
|
|
|
// scan title
|
|
title = scanTitle();
|
|
|
|
// construct tag parser
|
|
parser = new TagParser(title);
|
|
res = parser.parse();
|
|
|
|
if (res != null) {
|
|
res.startLine = startLine;
|
|
res.startColumn = startColumn;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
//
|
|
// Parse JSDoc
|
|
//
|
|
|
|
String scanJSDocDescription() {
|
|
StringBuilder description = new StringBuilder();
|
|
char ch;
|
|
boolean atAllowed;
|
|
|
|
atAllowed = true;
|
|
while (index < length) {
|
|
ch = source.charAt(index);
|
|
|
|
if (atAllowed && ch == '@') {
|
|
break;
|
|
}
|
|
|
|
if (isLineTerminator(ch)) {
|
|
atAllowed = true;
|
|
} else if (atAllowed && !isWhiteSpace(ch)) {
|
|
atAllowed = false;
|
|
}
|
|
|
|
description.append(advance());
|
|
}
|
|
return description.toString().trim();
|
|
}
|
|
|
|
public Pair<String, List<Tag>> parse(String comment) {
|
|
List<Tag> tags = new ArrayList<>();
|
|
Tag tag;
|
|
String description;
|
|
|
|
source = comment;
|
|
|
|
length = source.length();
|
|
index = 0;
|
|
lineNumber = 0;
|
|
lineStart = 0;
|
|
recoverable = true;
|
|
sloppy = true;
|
|
|
|
description = scanJSDocDescription();
|
|
|
|
while (true) {
|
|
tag = parseTag();
|
|
if (tag == null) {
|
|
break;
|
|
}
|
|
tags.add(tag);
|
|
}
|
|
|
|
return Pair.make(description, tags);
|
|
}
|
|
}
|
|
}
|
|
}
|