Skip to content
Permalink
Browse files
8274211: Test man page that options are documented
Reviewed-by: prappo
  • Loading branch information
jonathan-gibbons committed Oct 6, 2021
1 parent 9561fea commit 734d1fbd33be0aa20b26e6e8c776709f478069de
Showing with 287 additions and 0 deletions.
  1. +287 −0 test/langtools/jdk/javadoc/tool/CheckManPageOptions.java
@@ -0,0 +1,287 @@
/*
* Copyright (c) 2021, 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
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

/*
* @test
* @bug 8274211
* @summary Test man page that options are documented
* @modules jdk.javadoc/jdk.javadoc.internal.tool:+open
* @run main CheckManPageOptions
*/

import jdk.javadoc.doclet.Doclet;
import jdk.javadoc.doclet.StandardDoclet;
import jdk.javadoc.internal.tool.ToolOptions;

import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
* Checks the set of options found by fuzzy-parsing the troff or Markdown versions
* of the javadoc man page against the set of options declared in the source code.
*/
public class CheckManPageOptions {
public static void main(String... args) throws Exception {
new CheckManPageOptions().run(args);
}

static final PrintStream out = System.err;

// FIXME: JDK-8274295, JDK-8266666
List<String> MISSING_IN_MAN_PAGE = List.of(
"--legal-notices",
"--link-platform-properties",
"--no-platform-links",
"--since",
"--since-label",
"--snippet-path");

void run(String... args) throws Exception {
var file = args.length == 0 ? findDefaultFile() : Path.of(args[0]);
out.println("File: " + file);
out.println();

var manPageOptions = getManPageOptions(file);
out.println("Man page options: " + manPageOptions);
out.println();

var toolOptions = getToolOptions();
out.println("ToolOptions: " + toolOptions);
out.println();

var docletOptions = getDocletOptions();
out.println("DocletOptions: " + docletOptions);
out.println();

var toolDocletOnly = new TreeSet<String>();
toolDocletOnly.addAll(toolOptions);
toolDocletOnly.addAll(docletOptions);
toolDocletOnly.removeAll(manPageOptions);
toolDocletOnly.removeAll(MISSING_IN_MAN_PAGE);
if (!toolDocletOnly.isEmpty()) {
error("The following options are defined by the tool or doclet, but not defined in the man page:\n"
+ toSimpleList(toolDocletOnly));
}

var manPageOnly = new TreeSet<String>();
manPageOnly.addAll(manPageOptions);
manPageOnly.removeAll(toolOptions);
manPageOnly.removeAll(docletOptions);
if (!manPageOnly.isEmpty()) {
error("The following options are defined in the man page, but not defined by the tool or doclet:\n"
+ toSimpleList(manPageOnly));
}

if (!MISSING_IN_MAN_PAGE.isEmpty()) {
var notMissing = new TreeSet<>(MISSING_IN_MAN_PAGE);
notMissing.retainAll(manPageOptions);
if (!notMissing.isEmpty()) {
error("The following options were declared as missing, but were found on the man page:\n"
+ toSimpleList(notMissing));
}

out.println("NOTE: the following options are currently excluded and need to be documented in the man page:");
out.println(toSimpleList(MISSING_IN_MAN_PAGE));
}

if (errors > 0) {
out.println(errors + " errors found");
throw new Exception(errors + " errors found");
}
}

int errors = 0;
void error(String message) {
("Error: " + message).lines().forEach(out::println);
errors++;
}

String toSimpleList(Collection<String> items) {
return items.stream().collect(Collectors.joining(", ", " ", ""));
}

Path findDefaultFile() {
return findRootDir().resolve("src/jdk.javadoc/share/man/javadoc.1");
}

Path findRootDir() {
Path dir = Path.of(System.getProperty("test.src", ".")).toAbsolutePath();
while (dir != null) {
if (Files.exists(dir.resolve("src"))) {
return dir;
} else {
Path openDir = dir.resolve("open");
if (Files.exists(openDir.resolve("src"))) {
return openDir;
}
}
dir = dir.getParent();
}
throw new IllegalStateException("cannot find root dir");
}

List<String> getToolOptions() throws Error {
try {
Class<ToolOptions> toolOptionsClass = ToolOptions.class;

Constructor<ToolOptions> constr = toolOptionsClass.getDeclaredConstructor();
constr.setAccessible(true);

Method getSupportedOptions = toolOptionsClass.getMethod("getSupportedOptions");
Class<?> toolOptionClass = List.of(toolOptionsClass.getDeclaredClasses()).stream()
.filter(c -> c.getSimpleName().equals("ToolOption"))
.findFirst()
.orElseThrow();

Field kindField = toolOptionClass.getDeclaredField("kind");
kindField.setAccessible(true);
Method getNames = toolOptionClass.getDeclaredMethod("getNames");
getNames.setAccessible(true);

ToolOptions t = constr.newInstance();
var list = new ArrayList<String>();
var options = (List<?>) getSupportedOptions.invoke(t);
for (var option : options) {
Object kind = kindField.get(option);
if (kind.toString().equals("HIDDEN")) {
continue;
}

@SuppressWarnings("unchecked")
var oNames = (List<String>) getNames.invoke(option);
oNames.stream()
.filter(o -> !o.equals("@"))
.forEach(list::add);
}
return list;
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}

List<String> getDocletOptions() {
StandardDoclet d = new StandardDoclet();
d.init(Locale.getDefault(), null);
return getDocletOptions(d);
}

List<String> getDocletOptions(Doclet d) {
return d.getSupportedOptions().stream()
.filter(o -> o.getKind() != Doclet.Option.Kind.OTHER)
.flatMap(o -> o.getNames().stream())
.map(n -> n.replaceAll(":$", ""))
.toList();
}

List<String> getManPageOptions(Path file) throws IOException {
String page = Files.readString(file);
String name = file.getFileName().toString();
String extn = name.substring(name.lastIndexOf('.'));
return switch (extn) {
case ".1" -> parseNRoff(page);
case ".md" -> parseMarkdown(page);
default -> throw new IllegalArgumentException(file.toString());
};
}

List<String> parseNRoff(String page) {
var list = new ArrayList<String>();

// In the troff man page, options are defined in one of two forms:
// 1. options delegated to javac appear in pairs of lines of the form
// .IP \[bu] 2
// \f[CB]\-....
// 2. options implemented by the tool or doclet appear in lines of the form
// .B \f[CB]\-...

Pattern p1 = Pattern.compile("\\R" + Pattern.quote(".IP \\[bu] 2") + "\\R" + Pattern.quote("\\f[CB]\\-") + ".*");
Pattern p2 = Pattern.compile("\\R" + Pattern.quote(".B \\f[CB]\\-") + ".*");
Pattern outer = Pattern.compile("(" + p1.pattern() + "|" + p2.pattern() + ")");
Matcher outerMatcher = outer.matcher(page);

// In the defining areas, option names are represented as follows:
// \f[CB]OPTION\f[R] or \f[CB]OPTION:
// where OPTION is the shortest string not containing whitespace or colon,
// and in which all '-' characters are escaped with a single backslash.

Pattern inner = Pattern.compile("\\s\\\\f\\[CB](\\\\-[^ :]+?)(:|\\\\f\\[R])");

while (outerMatcher.find()) {
String lines = outerMatcher.group();
out.println("found:" + lines + "\n");

Matcher innerMatcher = inner.matcher(lines);
while (innerMatcher.find()) {
String option = innerMatcher.group(1).replace("\\-", "-");
list.add(option);
}
}

return list;
}

List<String> parseMarkdown(String page) {
var list = new ArrayList<String>();
// In the Markdown man page, options are defined in one of two forms:
// 1. options delegated to javac appear in lines of the form
// - `-...
// 2. options implemented by the tool or doclet appear in lines of the form
// `-...`

Pattern p1 = Pattern.compile("\\R- `-.*");
Pattern p2 = Pattern.compile("\\R`-.*");
Pattern outer = Pattern.compile("(" + p1.pattern() + "|" + p2.pattern() + ")");
Matcher outerMatcher = outer.matcher(page);

// In the defining areas, option names are represented as follows:
// `OPTION`
// where OPTION is the shortest string not containing whitespace or colon
Pattern inner = Pattern.compile("\\s`([^:`]+)");

while (outerMatcher.find()) {
String lines = outerMatcher.group();
out.println("found:" + lines + "\n");

Matcher innerMatcher = inner.matcher(lines);
while (innerMatcher.find()) {
String option = innerMatcher.group(1);
list.add(option);
}
}

return list;
}
}

1 comment on commit 734d1fb

@openjdk-notifier

This comment has been minimized.

Copy link

@openjdk-notifier openjdk-notifier bot commented on 734d1fb Oct 6, 2021

Please sign in to comment.