From 1b9e674b368b36df35606fe7011141530bda04e4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 24 Jan 2026 03:29:48 +0200 Subject: [PATCH] Proper parsing for generic JSDoc tags like "@word this is a tag" Removed deprecated/since tags, parsed them as generics instead --- .../interpreter/hover/TokenHoverInfo.java | 63 +++-- .../interpreter/js_parser/DTSJSDocParser.java | 17 +- .../interpreter/jsdoc/JSDocDeprecatedTag.java | 34 --- .../script/interpreter/jsdoc/JSDocInfo.java | 19 -- .../script/interpreter/jsdoc/JSDocParser.java | 255 +++++++++++++----- .../interpreter/jsdoc/JSDocSinceTag.java | 30 --- 6 files changed, 227 insertions(+), 191 deletions(-) delete mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocDeprecatedTag.java delete mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSinceTag.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index d3fa8d020..40963b165 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -5,12 +5,11 @@ import noppes.npcs.client.gui.util.script.interpreter.field.AssignmentInfo; import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; -import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocDeprecatedTag; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocReturnTag; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocSeeTag; -import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocSinceTag; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocTag; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocTypeTag; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; @@ -1224,28 +1223,6 @@ public class TokenHoverInfo { jsDocLines.add(returnLine); } - JSDocDeprecatedTag deprecatedTag = jsDoc.getDeprecatedTag(); - if (deprecatedTag != null) { - DocumentationLine deprecatedLine = new DocumentationLine(); - deprecatedLine.addSegment("@deprecated", TokenType.JSDOC_TAG.getHexColor()); - - if (deprecatedTag.hasReason()) { - deprecatedLine.addText(" - "); - deprecatedLine.addText(deprecatedTag.getReason()); - } - - jsDocLines.add(deprecatedLine); - } - - JSDocSinceTag sinceTag = jsDoc.getSinceTag(); - if (sinceTag != null) { - DocumentationLine sinceLine = new DocumentationLine(); - sinceLine.addSegment("Since:", TokenType.JSDOC_TAG.getHexColor()); - sinceLine.addText(" " + sinceTag.getVersion()); - - jsDocLines.add(sinceLine); - } - List seeTags = jsDoc.getSeeTags(); if (seeTags != null && !seeTags.isEmpty()) { for (JSDocSeeTag seeTag : seeTags) { @@ -1263,6 +1240,44 @@ public class TokenHoverInfo { jsDocLines.add(seeLine); } } + + List allTags = jsDoc.getAllTags(); + if (allTags != null && !allTags.isEmpty()) { + for (JSDocTag tag : allTags) { + String tagName = tag.getTagName(); + if (tagName == null) { + continue; + } + + String normalized = tagName.toLowerCase(); + if ("type".equals(normalized) + || "param".equals(normalized) + || "return".equals(normalized) + || "returns".equals(normalized) + || "see".equals(normalized)) { + continue; + } + + DocumentationLine tagLine = new DocumentationLine(); + //capitalise first letter in tag name + String capTagName = tagName.substring(0, 1).toUpperCase() + tagName.substring(1); + tagLine.addSegment(capTagName + ":", TokenType.JSDOC_TAG.getHexColor()); + + if (tag.hasType()) { + tagLine.addSegment(" {", TokenType.JSDOC_TYPE.getHexColor()); + tagLine.addSegment(tag.getTypeName(), TokenType.getColor(tag.getTypeInfo())); + tagLine.addSegment("}", TokenType.JSDOC_TYPE.getHexColor()); + } + + String tagDesc = tag.getDescription(); + if (tagDesc != null && !tagDesc.isEmpty()) { + tagLine.addText(" - "); + tagLine.addText(tagDesc.trim()); + } + + jsDocLines.add(tagLine); + } + } } private void extractJavadoc(Method method) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java index e3c1dd8bb..a54e64fe3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java @@ -13,7 +13,7 @@ public class DTSJSDocParser { "/\\*\\*\\s*(.*?)\\*/", Pattern.DOTALL); private static final Pattern TAG_PATTERN = Pattern.compile( - "@(\\w+)(?:\\s+(.*))?"); + "@(\\w+)(?:\\s+\\{([^}]+)\\})?(?:\\s+(.*))?"); private static final Pattern PARAM_PATTERN = Pattern.compile( "@param(?:\\s+\\{([^}]+)\\})?(?:\\s+(\\w+))?(?:\\s+(.*))?"); @@ -136,23 +136,20 @@ public class DTSJSDocParser { Matcher tagMatcher = TAG_PATTERN.matcher(line); if (tagMatcher.find()) { String tagName = tagMatcher.group(1); - String rest = tagMatcher.group(2); + String typeName = tagMatcher.group(2); + String rest = tagMatcher.group(3); switch (tagName) { - case "since": - JSDocSinceTag sinceTag = JSDocSinceTag.createSimple(rest != null ? rest.trim() : ""); - info.setSinceTag(sinceTag); - return sinceTag; - case "deprecated": - JSDocDeprecatedTag deprecatedTag = JSDocDeprecatedTag.createSimple(rest != null ? rest.trim() : ""); - info.setDeprecatedTag(deprecatedTag); - return deprecatedTag; case "see": JSDocSeeTag seeTag = JSDocSeeTag.createSimple(rest != null ? rest.trim() : ""); info.addSeeTag(seeTag); return seeTag; default: JSDocTag genericTag = new JSDocTag(tagName, -1, -1, -1); + if (typeName != null) { + typeName = typeName.trim(); + genericTag.setType(typeName, null, -1, -1); + } genericTag.setDescription(rest != null ? rest.trim() : ""); info.addTag(genericTag); return genericTag; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocDeprecatedTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocDeprecatedTag.java deleted file mode 100644 index f5f32d503..000000000 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocDeprecatedTag.java +++ /dev/null @@ -1,34 +0,0 @@ -package noppes.npcs.client.gui.util.script.interpreter.jsdoc; - -public class JSDocDeprecatedTag extends JSDocTag { - - private final String reason; - - public JSDocDeprecatedTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, String reason) { - super(tagName, atSignOffset, tagNameStart, tagNameEnd); - this.reason = reason; - this.setDescription(reason); - } - - public static JSDocDeprecatedTag create(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, - String reason) { - return new JSDocDeprecatedTag(tagName, atSignOffset, tagNameStart, tagNameEnd, reason); - } - - public static JSDocDeprecatedTag createSimple(String reason) { - return new JSDocDeprecatedTag("deprecated", -1, -1, -1, reason); - } - - public String getReason() { - return reason; - } - - public boolean hasReason() { - return reason != null && !reason.isEmpty(); - } - - @Override - public String toString() { - return "JSDocDeprecatedTag{@deprecated" + (hasReason() ? " " + reason : "") + "}"; - } -} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java index b497da434..899239884 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java @@ -19,8 +19,6 @@ public class JSDocInfo { private JSDocTypeTag typeTag; private final List paramTags = new ArrayList<>(); private JSDocReturnTag returnTag; - private JSDocDeprecatedTag deprecatedTag; - private JSDocSinceTag sinceTag; private final List seeTags = new ArrayList<>(); private String description; private final List allTags = new ArrayList<>(); @@ -46,15 +44,6 @@ public class JSDocInfo { allTags.add(returnTag); } - public void setDeprecatedTag(JSDocDeprecatedTag deprecatedTag) { - this.deprecatedTag = deprecatedTag; - allTags.add(deprecatedTag); - } - - public void setSinceTag(JSDocSinceTag sinceTag) { - this.sinceTag = sinceTag; - allTags.add(sinceTag); - } public void addSeeTag(JSDocSeeTag seeTag) { this.seeTags.add(seeTag); @@ -102,12 +91,6 @@ public class JSDocInfo { return returnTag != null ? returnTag.getTypeInfo() : null; } - public JSDocDeprecatedTag getDeprecatedTag() { return deprecatedTag; } - public boolean isDeprecated() { return deprecatedTag != null; } - - public JSDocSinceTag getSinceTag() { return sinceTag; } - public boolean hasSince() { return sinceTag != null; } - public List getSeeTags() { return Collections.unmodifiableList(seeTags); } public boolean hasSeeTags() { return !seeTags.isEmpty(); } @@ -120,8 +103,6 @@ public class JSDocInfo { if (typeTag != null) sb.append("type=").append(typeTag.getTypeName()); if (!paramTags.isEmpty()) sb.append(", params=").append(paramTags.size()); if (returnTag != null) sb.append(", return=").append(returnTag.getTypeName()); - if (deprecatedTag != null) sb.append(", deprecated"); - if (sinceTag != null) sb.append(", since=").append(sinceTag.getVersion()); if (!seeTags.isEmpty()) sb.append(", see=").append(seeTags.size()); sb.append("}"); return sb.toString(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java index 2d97d257e..8692965db 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java @@ -18,16 +18,16 @@ public class JSDocParser { "/\\*\\*([\\s\\S]*?)\\*/", Pattern.MULTILINE); private static final Pattern TYPE_TAG_PATTERN = Pattern.compile( - "@(type)\\s*\\{([^}]+)\\}(?:\\s*-?\\s*([^@\\n]*))?", Pattern.CASE_INSENSITIVE); + "^@(type)\\s*(?:\\{([^}]+)\\})?(?:\\s*-?\\s*(.*))?$", Pattern.CASE_INSENSITIVE); private static final Pattern PARAM_TAG_PATTERN = Pattern.compile( - "@(param)\\s*(?:\\{([^}]+)\\})?\\s*(\\w+)(?:\\s*-?\\s*([^@\\n]*))?", Pattern.CASE_INSENSITIVE); + "^@param\\s*(?:\\{([^}]+)\\})?\\s*(\\w+)?(?:\\s*-?\\s*(.*))?$", Pattern.CASE_INSENSITIVE); private static final Pattern RETURN_TAG_PATTERN = Pattern.compile( - "@(returns?)\\s*(?:\\{([^}]+)\\})?(?:\\s*-?\\s*([^@\\n]*))?", Pattern.CASE_INSENSITIVE); + "^@(returns?)\\s*(?:\\{([^}]+)\\})?(?:\\s*-?\\s*(.*))?$", Pattern.CASE_INSENSITIVE); - private static final Pattern ANY_TAG_PATTERN = Pattern.compile( - "@(\\w+)", Pattern.CASE_INSENSITIVE); + private static final Pattern GENERIC_TAG_PATTERN = Pattern.compile( + "^@(\\w+)(?:\\s*\\{([^}]+)\\})?(?:\\s*-?\\s*(.*))?$", Pattern.CASE_INSENSITIVE); private final ScriptDocument document; @@ -50,10 +50,53 @@ public class JSDocParser { int contentOffset = commentStart + 3; - parseTypeTag(content, contentOffset, info); - parseParamTags(content, contentOffset, info); - parseReturnTag(content, contentOffset, info); - extractDescription(content, info); + StringBuilder descriptionBuilder = new StringBuilder(); + boolean foundFirstTag = false; + JSDocTag lastTag = null; + + int position = 0; + while (position <= content.length()) { + int lineEnd = content.indexOf('\n', position); + if (lineEnd == -1) { + lineEnd = content.length(); + } + + String rawLine = content.substring(position, lineEnd); + if (rawLine.endsWith("\r")) { + rawLine = rawLine.substring(0, rawLine.length() - 1); + } + + CleanLine cleanLine = cleanLine(rawLine, contentOffset + position); + String line = cleanLine.text; + + if (!line.isEmpty()) { + if (line.startsWith("@")) { + foundFirstTag = true; + lastTag = parseTagLine(line, cleanLine.offset, info); + } else if (!foundFirstTag) { + if (descriptionBuilder.length() > 0) { + descriptionBuilder.append(" "); + } + descriptionBuilder.append(line); + } else if (lastTag != null) { + String existing = lastTag.getDescription(); + if (existing == null || existing.isEmpty()) { + lastTag.setDescription(line); + } else { + lastTag.setDescription(existing + " " + line); + } + } + } + + if (lineEnd == content.length()) { + break; + } + position = lineEnd + 1; + } + + if (descriptionBuilder.length() > 0) { + info.setDescription(descriptionBuilder.toString().trim()); + } return info; } @@ -97,39 +140,16 @@ public class JSDocParser { return parse(comment, startCommentPos); } - private void parseTypeTag(String content, int contentOffset, JSDocInfo info) { - Matcher m = TYPE_TAG_PATTERN.matcher(content); - if (m.find()) { - int atSignOffset = contentOffset + m.start(); - int tagNameStart = contentOffset + m.start(1); - int tagNameEnd = contentOffset + m.end(1); + private JSDocTag parseTagLine(String line, int lineOffset, JSDocInfo info) { + Matcher paramMatcher = PARAM_TAG_PATTERN.matcher(line); + if (paramMatcher.matches()) { + String typeName = paramMatcher.group(1); + String paramName = paramMatcher.group(2); + String description = normalizeDescription(paramMatcher.group(3)); - String typeName = m.group(2).trim(); - String description = m.group(3); - - int braceStart = content.indexOf('{', m.start()); - int braceEnd = content.indexOf('}', braceStart); - int typeStart = contentOffset + braceStart + 1; - int typeEnd = contentOffset + braceEnd; - - TypeInfo typeInfo = resolveType(typeName); - - JSDocTypeTag tag = JSDocTypeTag.create(atSignOffset, tagNameStart, tagNameEnd, - typeName, typeInfo, typeStart, typeEnd, description != null ? description.trim() : null); - info.setTypeTag(tag); - } - } - - private void parseParamTags(String content, int contentOffset, JSDocInfo info) { - Matcher m = PARAM_TAG_PATTERN.matcher(content); - while (m.find()) { - int atSignOffset = contentOffset + m.start(); - int tagNameStart = contentOffset + m.start(1); - int tagNameEnd = contentOffset + m.end(1); - - String typeName = m.group(2); - String paramName = m.group(3); - String description = m.group(4); + int atSignOffset = lineOffset + paramMatcher.start(); + int tagNameStart = lineOffset + 1; + int tagNameEnd = tagNameStart + "param".length(); int typeStart = -1; int typeEnd = -1; @@ -137,34 +157,35 @@ public class JSDocParser { if (typeName != null) { typeName = typeName.trim(); - int braceStart = content.indexOf('{', m.start()); - int braceEnd = content.indexOf('}', braceStart); - typeStart = contentOffset + braceStart + 1; - typeEnd = contentOffset + braceEnd; + typeStart = lineOffset + paramMatcher.start(1); + typeEnd = lineOffset + paramMatcher.end(1); typeInfo = resolveType(typeName); } - int paramNameStart = contentOffset + m.start(3); - int paramNameEnd = contentOffset + m.end(3); + int paramNameStart = -1; + int paramNameEnd = -1; + if (paramName != null) { + paramNameStart = lineOffset + paramMatcher.start(2); + paramNameEnd = lineOffset + paramMatcher.end(2); + } JSDocParamTag tag = JSDocParamTag.create(atSignOffset, tagNameStart, tagNameEnd, typeName, typeInfo, typeStart, typeEnd, paramName, paramNameStart, paramNameEnd, - description != null ? description.trim() : null); + description); info.addParamTag(tag); + return tag; } - } - private void parseReturnTag(String content, int contentOffset, JSDocInfo info) { - Matcher m = RETURN_TAG_PATTERN.matcher(content); - if (m.find()) { - String tagName = m.group(1); - int atSignOffset = contentOffset + m.start(); - int tagNameStart = contentOffset + m.start(1); - int tagNameEnd = contentOffset + m.end(1); + Matcher returnMatcher = RETURN_TAG_PATTERN.matcher(line); + if (returnMatcher.matches()) { + String tagName = returnMatcher.group(1); + String typeName = returnMatcher.group(2); + String description = normalizeDescription(returnMatcher.group(3)); - String typeName = m.group(2); - String description = m.group(3); + int atSignOffset = lineOffset + returnMatcher.start(); + int tagNameStart = lineOffset + returnMatcher.start(1); + int tagNameEnd = lineOffset + returnMatcher.end(1); int typeStart = -1; int typeEnd = -1; @@ -172,41 +193,127 @@ public class JSDocParser { if (typeName != null) { typeName = typeName.trim(); - int braceStart = content.indexOf('{', m.start()); - int braceEnd = content.indexOf('}', braceStart); - typeStart = contentOffset + braceStart + 1; - typeEnd = contentOffset + braceEnd; + typeStart = lineOffset + returnMatcher.start(2); + typeEnd = lineOffset + returnMatcher.end(2); typeInfo = resolveType(typeName); } JSDocReturnTag tag = JSDocReturnTag.create(tagName, atSignOffset, tagNameStart, tagNameEnd, typeName, typeInfo, typeStart, typeEnd, - description != null ? description.trim() : null); + description); info.setReturnTag(tag); + return tag; } + + Matcher typeMatcher = TYPE_TAG_PATTERN.matcher(line); + if (typeMatcher.matches()) { + String typeName = typeMatcher.group(2); + String description = normalizeDescription(typeMatcher.group(3)); + + int atSignOffset = lineOffset + typeMatcher.start(); + int tagNameStart = lineOffset + typeMatcher.start(1); + int tagNameEnd = lineOffset + typeMatcher.end(1); + + int typeStart = -1; + int typeEnd = -1; + TypeInfo typeInfo = null; + + if (typeName != null) { + typeName = typeName.trim(); + typeStart = lineOffset + typeMatcher.start(2); + typeEnd = lineOffset + typeMatcher.end(2); + typeInfo = resolveType(typeName); + } + + JSDocTypeTag tag = JSDocTypeTag.create(atSignOffset, tagNameStart, tagNameEnd, + typeName, typeInfo, typeStart, typeEnd, description); + info.setTypeTag(tag); + return tag; + } + + Matcher genericMatcher = GENERIC_TAG_PATTERN.matcher(line); + if (genericMatcher.matches()) { + String tagName = genericMatcher.group(1); + String typeName = genericMatcher.group(2); + String description = normalizeDescription(genericMatcher.group(3)); + + int atSignOffset = lineOffset + genericMatcher.start(); + int tagNameStart = lineOffset + genericMatcher.start(1); + int tagNameEnd = lineOffset + genericMatcher.end(1); + + JSDocTag tag = new JSDocTag(tagName, atSignOffset, tagNameStart, tagNameEnd); + + if (typeName != null) { + typeName = typeName.trim(); + int typeStart = lineOffset + genericMatcher.start(2); + int typeEnd = lineOffset + genericMatcher.end(2); + TypeInfo typeInfo = resolveType(typeName); + tag.setType(typeName, typeInfo, typeStart, typeEnd); + } + + if (description != null && !description.isEmpty()) { + tag.setDescription(description); + } + + info.addTag(tag); + return tag; + } + + return null; } - private void extractDescription(String content, JSDocInfo info) { - Matcher m = ANY_TAG_PATTERN.matcher(content); - String description; + private String normalizeDescription(String description) { + if (description == null) { + return null; + } + String trimmed = description.trim(); + return trimmed.isEmpty() ? null : trimmed; + } - if (m.find()) { - description = content.substring(0, m.start()); - } else { - description = content; + private CleanLine cleanLine(String line, int lineStartOffset) { + int index = 0; + while (index < line.length() && Character.isWhitespace(line.charAt(index))) { + index++; } - description = description.replaceAll("(?m)^\\s*\\*\\s?", "").trim(); - - if (!description.isEmpty()) { - info.setDescription(description); + if (index + 2 < line.length() && line.startsWith("/**", index)) { + index += 3; + while (index < line.length() && Character.isWhitespace(line.charAt(index))) { + index++; + } } + + if (index < line.length() && line.charAt(index) == '*') { + index++; + if (index < line.length() && line.charAt(index) == ' ') { + index++; + } + } + + String cleaned = line.substring(index); + int end = cleaned.length(); + while (end > 0 && Character.isWhitespace(cleaned.charAt(end - 1))) { + end--; + } + cleaned = cleaned.substring(0, end); + + return new CleanLine(cleaned, lineStartOffset + index); } private TypeInfo resolveType(String typeName) { return document.resolveType(typeName); } + private static class CleanLine { + private final String text; + private final int offset; + + private CleanLine(String text, int offset) { + this.text = text; + this.offset = offset; + } + } + public static List findAllJSDocComments(String source) { List positions = new ArrayList<>(); Matcher m = JSDOC_PATTERN.matcher(source); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSinceTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSinceTag.java deleted file mode 100644 index 6c9005b0b..000000000 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSinceTag.java +++ /dev/null @@ -1,30 +0,0 @@ -package noppes.npcs.client.gui.util.script.interpreter.jsdoc; - -public class JSDocSinceTag extends JSDocTag { - - private final String version; - - public JSDocSinceTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, String version) { - super(tagName, atSignOffset, tagNameStart, tagNameEnd); - this.version = version; - this.setDescription(version); - } - - public static JSDocSinceTag create(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, - String version) { - return new JSDocSinceTag(tagName, atSignOffset, tagNameStart, tagNameEnd, version); - } - - public static JSDocSinceTag createSimple(String version) { - return new JSDocSinceTag("since", -1, -1, -1, version); - } - - public String getVersion() { - return version; - } - - @Override - public String toString() { - return "JSDocSinceTag{@since " + version + "}"; - } -}