8288660: JavaDoc should be more helpful if it doesn't recognize a tag
Reviewed-by: jjg
This commit is contained in:
parent
ba1a46392f
commit
a01b3fb8e9
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@ -1174,13 +1174,17 @@ public class JavacTrees extends DocTrees {
|
||||
printMessage(kind, msg, ((DCTree) t).pos((DCDocComment) c), root);
|
||||
}
|
||||
|
||||
public void printMessage(Diagnostic.Kind kind, CharSequence msg) {
|
||||
printMessage(kind, msg, (JCDiagnostic.DiagnosticPosition) null, null);
|
||||
}
|
||||
|
||||
private void printMessage(Diagnostic.Kind kind, CharSequence msg,
|
||||
JCDiagnostic.DiagnosticPosition pos,
|
||||
com.sun.source.tree.CompilationUnitTree root) {
|
||||
JavaFileObject oldSource = null;
|
||||
JavaFileObject newSource = null;
|
||||
|
||||
newSource = root.getSourceFile();
|
||||
newSource = root == null ? null : root.getSourceFile();
|
||||
if (newSource == null) {
|
||||
pos = null;
|
||||
} else {
|
||||
|
@ -64,6 +64,7 @@ import jdk.javadoc.internal.doclets.toolkit.Messages;
|
||||
import jdk.javadoc.internal.doclets.toolkit.Resources;
|
||||
import jdk.javadoc.internal.doclets.toolkit.util.CommentHelper;
|
||||
import jdk.javadoc.internal.doclets.toolkit.util.Utils;
|
||||
import jdk.javadoc.internal.doclint.DocLint;
|
||||
|
||||
import static com.sun.source.doctree.DocTree.Kind.AUTHOR;
|
||||
import static com.sun.source.doctree.DocTree.Kind.EXCEPTION;
|
||||
@ -123,13 +124,6 @@ public class TagletManager {
|
||||
*/
|
||||
private final Set<String> standardTags;
|
||||
|
||||
/**
|
||||
* Keep track of standard tags in lowercase to compare for better
|
||||
* error messages when a tag like {@code @docRoot} is mistakenly spelled
|
||||
* lowercase {@code @docroot}.
|
||||
*/
|
||||
private final Set<String> standardTagsLowercase;
|
||||
|
||||
/**
|
||||
* Keep track of overridden standard tags.
|
||||
*/
|
||||
@ -185,7 +179,6 @@ public class TagletManager {
|
||||
overriddenStandardTags = new HashSet<>();
|
||||
potentiallyConflictingTags = new HashSet<>();
|
||||
standardTags = new HashSet<>();
|
||||
standardTagsLowercase = new HashSet<>();
|
||||
unseenCustomTags = new HashSet<>();
|
||||
allTaglets = new LinkedHashMap<>();
|
||||
this.config = config;
|
||||
@ -363,10 +356,12 @@ public class TagletManager {
|
||||
if (!allTaglets.containsKey(name)) {
|
||||
if (!config.isDocLintSyntaxGroupEnabled()) {
|
||||
var ch = utils.getCommentHelper(element);
|
||||
if (standardTagsLowercase.contains(Utils.toLowerCase(name))) {
|
||||
messages.warning(ch.getDocTreePath(tag), "doclet.UnknownTagLowercase", ch.getTagName(tag));
|
||||
List<String> suggestions = DocLint.suggestSimilar(allTaglets.keySet(), name);
|
||||
if (!suggestions.isEmpty()) {
|
||||
messages.warning(ch.getDocTreePath(tag), "doclet.UnknownTagWithHint",
|
||||
String.join(", ", suggestions)); // TODO: revisit after 8041488
|
||||
} else {
|
||||
messages.warning(ch.getDocTreePath(tag), "doclet.UnknownTag", ch.getTagName(tag));
|
||||
messages.warning(ch.getDocTreePath(tag), "doclet.UnknownTag");
|
||||
}
|
||||
}
|
||||
continue; // unknown tag
|
||||
@ -660,7 +655,6 @@ public class TagletManager {
|
||||
String name = taglet.getName();
|
||||
allTaglets.put(name, taglet);
|
||||
standardTags.add(name);
|
||||
standardTagsLowercase.add(Utils.toLowerCase(name));
|
||||
}
|
||||
|
||||
private void addStandardTaglet(Taglet taglet, DocTree.Kind alias) {
|
||||
@ -668,7 +662,6 @@ public class TagletManager {
|
||||
String name = alias.tagName;
|
||||
allTaglets.put(name, taglet);
|
||||
standardTags.add(name);
|
||||
standardTagsLowercase.add(Utils.toLowerCase(name));
|
||||
}
|
||||
|
||||
public boolean isKnownCustomTag(String tagName) {
|
||||
|
@ -116,8 +116,8 @@ doclet.Since=Since:
|
||||
doclet.Throws=Throws:
|
||||
doclet.Version=Version:
|
||||
doclet.Factory=Factory:
|
||||
doclet.UnknownTag={0} is an unknown tag.
|
||||
doclet.UnknownTagLowercase={0} is an unknown tag -- same as a known tag except for case.
|
||||
doclet.UnknownTag=unknown tag. Unregistered custom tag?
|
||||
doclet.UnknownTagWithHint=unknown tag. Mistyped @{0} or an unregistered custom tag?
|
||||
doclet.inheritDocBadSupertype=cannot find the overridden method
|
||||
doclet.inheritDocWithinInappropriateTag=@inheritDoc cannot be used within this tag
|
||||
doclet.inheritDocNoDoc=overridden methods do not document exception type {0}
|
||||
|
@ -1147,8 +1147,15 @@ public class Checker extends DocTreePathScanner<Void, Void> {
|
||||
|| k == DocTree.Kind.UNKNOWN_INLINE_TAG;
|
||||
assert !getStandardTags().contains(tagName);
|
||||
// report an unknown tag only if custom tags are set, see 8314213
|
||||
if (env.customTags != null && !env.customTags.contains(tagName))
|
||||
env.messages.error(SYNTAX, tree, "dc.tag.unknown", tagName);
|
||||
if (env.customTags != null && !env.customTags.contains(tagName)) {
|
||||
var suggestions = DocLint.suggestSimilar(env.customTags, tagName);
|
||||
if (suggestions.isEmpty()) {
|
||||
env.messages.error(SYNTAX, tree, "dc.unknown.javadoc.tag");
|
||||
} else {
|
||||
env.messages.error(SYNTAX, tree, "dc.unknown.javadoc.tag.with.hint",
|
||||
String.join(", ", suggestions)); // TODO: revisit after 8041488
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> getStandardTags() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@ -29,6 +29,8 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
@ -61,6 +63,7 @@ import com.sun.tools.javac.main.JavaCompiler;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
import com.sun.tools.javac.util.DefinedBy;
|
||||
import com.sun.tools.javac.util.DefinedBy.Api;
|
||||
import com.sun.tools.javac.util.StringUtils.DamerauLevenshteinDistance;
|
||||
|
||||
/**
|
||||
* Multi-function entry point for the doc check utility.
|
||||
@ -373,6 +376,29 @@ public class DocLint extends com.sun.tools.doclint.DocLint {
|
||||
Env env;
|
||||
Checker checker;
|
||||
|
||||
public static List<String> suggestSimilar(Collection<String> knownTags, String unknownTag) {
|
||||
final double MIN_SIMILARITY = 2.0 / 3;
|
||||
record Pair(String tag, double similarity) { }
|
||||
return knownTags.stream()
|
||||
.distinct() // filter duplicates in known, otherwise they will result in duplicates in suggested
|
||||
.map(t -> new Pair(t, similarity(t, unknownTag)))
|
||||
.sorted(Comparator.comparingDouble(Pair::similarity).reversed() /* more similar first */)
|
||||
// .peek(p -> System.out.printf("%.3f, (%s ~ %s)%n", p.similarity, p.tag, unknownTag)) // debug
|
||||
.takeWhile(p -> Double.compare(p.similarity, MIN_SIMILARITY) >= 0)
|
||||
.map(Pair::tag)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// a value in [0, 1] range: the closer the value is to 1, the more similar
|
||||
// the strings are
|
||||
private static double similarity(String a, String b) {
|
||||
// Normalize the distance so that similarity between "x" and "y" is
|
||||
// less than that of "ax" and "ay". Use the greater of two lengths
|
||||
// as normalizer, as it's an upper bound for the distance.
|
||||
return 1.0 - ((double) DamerauLevenshteinDistance.of(a, b))
|
||||
/ Math.max(a.length(), b.length());
|
||||
}
|
||||
|
||||
public boolean isValidOption(String opt) {
|
||||
if (opt.equals(XMSGS_OPTION))
|
||||
return true;
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@ -41,6 +41,7 @@ import javax.tools.Diagnostic;
|
||||
|
||||
import com.sun.source.doctree.DocTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.tools.javac.api.JavacTrees;
|
||||
import com.sun.tools.javac.util.StringUtils;
|
||||
import jdk.javadoc.internal.doclint.Env.AccessKind;
|
||||
|
||||
@ -96,6 +97,10 @@ public class Messages {
|
||||
report(group, Diagnostic.Kind.WARNING, tree, code, args);
|
||||
}
|
||||
|
||||
void note(Group group, String code, Object... args) {
|
||||
report(group, Diagnostic.Kind.NOTE, code, args);
|
||||
}
|
||||
|
||||
void setOptions(String opts) {
|
||||
options.setOptions(opts);
|
||||
}
|
||||
@ -112,6 +117,18 @@ public class Messages {
|
||||
stats.report(out);
|
||||
}
|
||||
|
||||
protected void report(Group group, Diagnostic.Kind dkind, String code, Object... args) {
|
||||
if (options.isEnabled(group, env.currAccess)) {
|
||||
if (dkind == Diagnostic.Kind.WARNING && env.suppressWarnings(group)) {
|
||||
return;
|
||||
}
|
||||
String msg = (code == null) ? (String) args[0] : localize(code, args);
|
||||
((JavacTrees) env.trees).printMessage(dkind, msg);
|
||||
|
||||
stats.record(group, dkind, code);
|
||||
}
|
||||
}
|
||||
|
||||
protected void report(Group group, Diagnostic.Kind dkind, DocTree tree, String code, Object... args) {
|
||||
if (options.isEnabled(group, env.currAccess)) {
|
||||
if (dkind == Diagnostic.Kind.WARNING && env.suppressWarnings(group)) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
#
|
||||
# Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved.
|
||||
# Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
#
|
||||
# This code is free software; you can redistribute it and/or modify it
|
||||
@ -84,6 +84,8 @@ dc.tag.p.in.pre= unexpected use of <p> inside <pre> element
|
||||
dc.tag.requires.heading = heading not found for </{0}>
|
||||
dc.tag.self.closing = self-closing element not allowed
|
||||
dc.tag.start.unmatched = end tag missing: </{0}>
|
||||
dc.unknown.javadoc.tag = unknown tag. Unregistered custom tag?
|
||||
dc.unknown.javadoc.tag.with.hint = unknown tag. Mistyped @{0} or an unregistered custom tag?
|
||||
dc.tag.unknown = unknown tag: {0}
|
||||
dc.tag.not.supported.html5 = tag not supported in HTML5: {0}
|
||||
dc.text.not.allowed = text not allowed in <{0}> element
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@ -664,7 +664,7 @@ public class TestSnippetTag extends SnippetTester {
|
||||
"-sourcepath", srcDir.toString(),
|
||||
"pkg");
|
||||
checkExit(Exit.ERROR);
|
||||
long actual = Pattern.compile("error: unknown tag: snippet:")
|
||||
long actual = Pattern.compile("error: unknown tag. Mistyped @snippet")
|
||||
.matcher(getOutput(Output.OUT)).results().count();
|
||||
checking("Number of errors");
|
||||
int expected = unknownTags.size();
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8314448
|
||||
* @bug 8314448 8288660
|
||||
* @library /tools/lib ../../lib
|
||||
* @modules jdk.compiler/com.sun.tools.javac.api
|
||||
* jdk.compiler/com.sun.tools.javac.main
|
||||
@ -33,6 +33,9 @@
|
||||
*/
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javadoc.tester.JavadocTester;
|
||||
import toolbox.ToolBox;
|
||||
@ -68,7 +71,7 @@ public class TestUnknownTags extends JavadocTester {
|
||||
"x");
|
||||
new OutputChecker(Output.OUT)
|
||||
.setExpectFound(true)
|
||||
.checkUnique("unknown tag");
|
||||
.checkUnique(Pattern.compile("unknown tag."));
|
||||
}
|
||||
// DocLint is default
|
||||
javadoc("-d", base.resolve("out").toString(),
|
||||
@ -76,7 +79,7 @@ public class TestUnknownTags extends JavadocTester {
|
||||
"x");
|
||||
new OutputChecker(Output.OUT)
|
||||
.setExpectFound(true)
|
||||
.checkUnique("unknown tag");
|
||||
.checkUnique(Pattern.compile("unknown tag."));
|
||||
}
|
||||
|
||||
// Disabled simple tags are treated as known tags, but aren't checked
|
||||
@ -103,4 +106,133 @@ public class TestUnknownTags extends JavadocTester {
|
||||
checkOutput(Output.OUT, false, "Tag @myDisabledTag cannot be used in class documentation");
|
||||
checkOutput(Output.OUT, true, "Tag @myEnabledTag cannot be used in class documentation");
|
||||
}
|
||||
|
||||
// This tests two assertions:
|
||||
//
|
||||
// - the "helpful note" is output exactly once,
|
||||
// - some typos have fix suggestions, and
|
||||
// - there's no difference between inline and block tags as
|
||||
// far as the diagnostic output is concerned
|
||||
@Test
|
||||
public void testSimilarTags(Path base) throws Exception {
|
||||
var src = base.resolve("src");
|
||||
// put some tags as inline in the main description, so that they are
|
||||
// not parsed as contents of the immediately preceding unknown
|
||||
// block tags
|
||||
tb.writeJavaFiles(src, """
|
||||
package x;
|
||||
|
||||
/**
|
||||
* {@cod}
|
||||
* {@codejdk.net.hosts.file}
|
||||
* {@coe}
|
||||
* {@cpde}
|
||||
* {@ocde}
|
||||
* {@ode}
|
||||
*
|
||||
* @auther
|
||||
*
|
||||
* @Depricated
|
||||
* @deprecation
|
||||
*
|
||||
* @DocRoot
|
||||
* @dccRoot
|
||||
* @docroot
|
||||
*
|
||||
* @ecception
|
||||
* @excception
|
||||
* @exceptbion
|
||||
* @exceptino
|
||||
* @exceptions
|
||||
* @exceptoin
|
||||
* @execption
|
||||
*
|
||||
* @implnote
|
||||
*
|
||||
* @inheritdoc
|
||||
* @inherotDoc
|
||||
* @inheretdoc
|
||||
* @inhertitDoc
|
||||
*
|
||||
* @jvm
|
||||
* @jmvs
|
||||
*
|
||||
* @Link
|
||||
* @linK
|
||||
* @linbk
|
||||
* @lini
|
||||
* @linke
|
||||
* @linked
|
||||
*
|
||||
* @linkplan
|
||||
*
|
||||
* @params
|
||||
* @pararm
|
||||
* @parasm
|
||||
* @parem
|
||||
* @parm
|
||||
* @parma
|
||||
* @praam
|
||||
* @prarm
|
||||
*
|
||||
* @Return
|
||||
* @eturn
|
||||
* @result
|
||||
* @retrun
|
||||
* @retuen
|
||||
* @retun
|
||||
* @retunr
|
||||
* @retur
|
||||
* @returns
|
||||
* @returnss
|
||||
* @retursn
|
||||
* @rturn
|
||||
*
|
||||
* @See
|
||||
* @gsee
|
||||
*
|
||||
* @serialdata
|
||||
*
|
||||
* @sinc
|
||||
* @sine
|
||||
*
|
||||
* @systemproperty
|
||||
*
|
||||
* @thows
|
||||
* @thrown
|
||||
* @throwss
|
||||
*/
|
||||
public class MyClass { }
|
||||
""");
|
||||
// don't check exit status: we don't care if it's an error or warning
|
||||
|
||||
// DocLint is explicit
|
||||
int i = 0;
|
||||
for (var check : new String[]{":all", ":none", "", null}) {
|
||||
var outputDir = "out-DocLint-" + i++; // use separate output directories
|
||||
|
||||
var args = new ArrayList<String>();
|
||||
if (check != null) // check == null means DocLint is default
|
||||
args.add("-Xdoclint" + check);
|
||||
args.addAll(Arrays.asList(
|
||||
"-d", base.resolve(outputDir).toString(),
|
||||
"-tag", "apiNote:a:API Note:",
|
||||
"-tag", "implSpec:a:Implementation Requirements:",
|
||||
"-tag", "implNote:a:Implementation Note:",
|
||||
"-tag", "jls:a:JLS", // this tag isn't exactly that of JDK, for simplicity reasons
|
||||
"-tag", "jvms:a:JVMS", // this tag isn't exactly that of JDK, for simplicity reasons
|
||||
"--source-path", src.toString(),
|
||||
"x"));
|
||||
|
||||
javadoc(args.toArray(new String[]{}));
|
||||
|
||||
new OutputChecker(Output.OUT)
|
||||
.setExpectFound(true)
|
||||
.setExpectOrdered(false)
|
||||
.check("author", "code", "deprecated", "docRoot",
|
||||
"exception", "implNote", "inheritDoc", "jvms",
|
||||
"link", "linkplain", "param", "return", "see",
|
||||
"serialData", "since", "systemProperty", "throws");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* @test /nodynamiccopyright/
|
||||
* @bug 8006248 8028318
|
||||
* @bug 8006248 8028318 8288660
|
||||
* @summary DocLint should report unknown tags
|
||||
* @modules jdk.javadoc/jdk.javadoc.internal.doclint
|
||||
* @build DocLintTester
|
||||
|
@ -1,10 +1,10 @@
|
||||
CustomTagTest.java:15: error: unknown tag: customTag
|
||||
CustomTagTest.java:15: error: unknown tag. Unregistered custom tag?
|
||||
* @customTag Text for a custom tag.
|
||||
^
|
||||
CustomTagTest.java:16: error: unknown tag: custom.tag
|
||||
CustomTagTest.java:16: error: unknown tag. Unregistered custom tag?
|
||||
* @custom.tag Text for another custom tag.
|
||||
^
|
||||
CustomTagTest.java:17: error: unknown tag: unknownTag
|
||||
CustomTagTest.java:17: error: unknown tag. Unregistered custom tag?
|
||||
* @unknownTag Text for an unknown tag.
|
||||
^
|
||||
3 errors
|
||||
|
@ -1,4 +1,4 @@
|
||||
CustomTagTest.java:17: error: unknown tag: unknownTag
|
||||
CustomTagTest.java:17: error: unknown tag. Unregistered custom tag?
|
||||
* @unknownTag Text for an unknown tag.
|
||||
^
|
||||
1 error
|
||||
|
Loading…
x
Reference in New Issue
Block a user