Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a95b881
Reject bad command-line options with a non-zero exit status
mernst Oct 7, 2026
2a6f52f
runtimechecker: reject bad options; accept -h and --help
mernst Oct 7, 2026
6380a6a
Remove stale comment in DtraceDiff
mernst Oct 7, 2026
06ecd50
Merge branch 'bad-option-core' into runtimechecker-options
mernst Oct 7, 2026
23bf08d
Address code review: reject "- " with no short options, report instru…
mernst Oct 7, 2026
d20d9f8
Merge remote-tracking branch 'origin/bad-option-core' into bad-option…
mernst Oct 7, 2026
0242d85
Address code review: recognize -h in instrumenter, fix runcluster.pl …
mernst Oct 7, 2026
9fc85fa
Address code review: SplitterFactoryTest.mainHelper throws instead of…
mernst Oct 7, 2026
ada0caf
Provide DaikonGetopt helpers that the per-tool PRs use; leave instrum…
mernst Oct 7, 2026
96c8069
Merge remote-tracking branch 'origin/bad-option-core' into runtimeche…
mernst Oct 7, 2026
4f58740
Restore bad-option-core's changes to InstrumentHandler that the merge…
mernst Oct 7, 2026
e560e3e
Merge branch 'master' of https://github.com/codespecs/daikon into bad…
mernst Oct 7, 2026
878a410
Merge remote-tracking branch 'origin/bad-option-core' into runtimeche…
mernst Oct 7, 2026
e8abacc
Move the runtimechecker changes to the runtimechecker-options branch
mernst Oct 10, 2026
f1d37ef
Merge branch 'bad-option-core' of github.com:mernst/daikon into runti…
mernst Oct 10, 2026
2b46b30
Address code review: remove unused DaikonGetopt helpers, reject stray…
mernst Oct 11, 2026
50e3e2a
Simplify DaikonGetopt: one constructor, which always handles -h and -…
mernst Oct 11, 2026
7df7f23
Move test-driver fixes and doc spelling fixes to separate pull requests
mernst Oct 11, 2026
4f54694
Handle DaikonTerminationException in SampleTester and SplitterFactory…
mernst Oct 11, 2026
117da2e
Brevity
mernst Oct 11, 2026
3bee663
Merge branch 'master' into bad-option-core
mernst Oct 11, 2026
bb2a3f6
Merge branch 'bad-option-core' into runtimechecker-options
mernst Oct 12, 2026
49c38f8
Merge branch 'master' of github.com:codespecs/daikon into runtimechec…
mernst Oct 12, 2026
91ae719
Add the DaikonGetopt helpers that this pull request uses: argsAfterLe…
mernst Oct 12, 2026
7d16552
Handle DaikonTerminationException once in runtimechecker Main; simpli…
mernst Oct 12, 2026
77e0e9d
Report instrument argument errors and handler failures via UserError
mernst Oct 12, 2026
31bedbc
Inline argsAfterOptions, rename Main.usageMessageString, report Write…
mernst Oct 12, 2026
b9ae8d8
Report class-lookup and I/O errors via UserError; make CommandHandler…
mernst Oct 12, 2026
9429a2d
Preserve cause when main method is missing in WriteViolationFile
mernst Oct 12, 2026
05c5637
CommandHandler: avoid Java 9+ syntax and APIs
mernst Oct 12, 2026
42db92b
Merge remote-tracking branch 'origin/master' into runtimechecker-options
mernst Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 74 additions & 7 deletions java/daikon/DaikonGetopt.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import gnu.getopt.Getopt;
import gnu.getopt.LongOpt;
import java.util.Arrays;
import java.util.function.Supplier;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.plumelib.util.ArraysPlume;

/**
Expand All @@ -20,8 +22,11 @@ public class DaikonGetopt extends Getopt {
*/
private static final String PROGNAME = "DaikonGetopt";

/** The usage message printed for {@code -h} and {@code --help}. */
private final String usage;
/** Computes the usage message printed for {@code -h} and {@code --help}. */
private final Supplier<String> usage;

/** Text appended to the description of a bad command-line option, or null to append nothing. */
private @Nullable String usageHint = "run with -h for usage";

/**
* Creates a command-line option processor that recognizes short and long options, plus {@code -h}
Expand All @@ -33,6 +38,20 @@ public class DaikonGetopt extends Getopt {
* @param usage the usage message to print for {@code -h} and {@code --help}
*/
public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String usage) {
this(argv, optstring, longopts, () -> usage);
}

/**
* Creates a command-line option processor that recognizes short and long options, plus {@code -h}
* and {@code --help}, which print the usage message and throw {@link Daikon.NormalTermination}.
* The usage message is computed only if it is needed.
*
* @param argv the command-line arguments
* @param optstring the short options, in the format of {@link Getopt}; must not contain {@code h}
* @param longopts the long options; must not contain {@code help}
* @param usage computes the usage message to print for {@code -h} and {@code --help}
*/
public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, Supplier<String> usage) {
super(
PROGNAME,
argv,
Expand All @@ -43,6 +62,49 @@ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String
opterr = false;
}

/**
* Processes the leading command-line arguments of a program that takes no options other than
* {@code -h} and {@code --help}. Processing stops at the first argument that is not an option, so
* later arguments may be options for some other program.
*
* @param argv the command-line arguments
* @param usage the usage message to print for {@code -h} and {@code --help}
* @return the arguments from the first one that is not an option onward
* @throws Daikon.NormalTermination after printing the usage message, if a leading argument
* requests it
* @throws Daikon.UserError if a leading argument is any other option
*/
public static String[] argsAfterLeadingOptions(String[] argv, String usage) {
return argsAfterLeadingOptions(argv, () -> usage);
}

/**
* Like {@link #argsAfterLeadingOptions(String[], String)}, but computes the usage message only if
* it is needed.
*
* @param argv the command-line arguments
* @param usage computes the usage message to print for {@code -h} and {@code --help}
* @return the arguments from the first one that is not an option onward
* @throws Daikon.NormalTermination after printing the usage message, if a leading argument
* requests it
* @throws Daikon.UserError if a leading argument is any other option
*/
public static String[] argsAfterLeadingOptions(String[] argv, Supplier<String> usage) {
// A leading "+" tells Getopt to stop at the first argument that is not an option.
return argsAfterOptions(argv, "+", usage);
}

/**
* Sets the text that is appended to the description of a bad command-line option. The default
* tells the user to run the program with -h.
*
* @param usageHint text appended to the description of a bad command-line option, or null to
* append nothing
*/
public void setUsageHint(@Nullable String usageHint) {
this.usageHint = usageHint;
}

/**
* Processes the command-line arguments of a program that takes no options other than {@code -h}
* and {@code --help}.
Expand All @@ -54,7 +116,7 @@ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String
* @throws Daikon.UserError if an argument is any other option
*/
public static String[] nonOptionArgs(String[] argv, String usage) {
return argsAfterOptions(argv, "", usage);
return argsAfterOptions(argv, "", () -> usage);
}

/**
Expand All @@ -63,12 +125,13 @@ public static String[] nonOptionArgs(String[] argv, String usage) {
*
* @param argv the command-line arguments
* @param optstring "" to process all the arguments, or "+" to stop at the first non-option
* @param usage the usage message to print for {@code -h} and {@code --help}
* @param usage computes the usage message to print for {@code -h} and {@code --help}
* @return the arguments that Getopt did not process as options
* @throws Daikon.NormalTermination after printing the usage message, if an argument requests it
* @throws Daikon.UserError if an argument is any other option
*/
private static String[] argsAfterOptions(String[] argv, String optstring, String usage) {
private static String[] argsAfterOptions(
String[] argv, String optstring, Supplier<String> usage) {
// Getopt permutes its argument array, so give it a copy to avoid modifying the caller's array.
String[] argvCopy = argv.clone();
DaikonGetopt g = new DaikonGetopt(argvCopy, optstring, new LongOpt[0], usage);
Expand All @@ -93,10 +156,14 @@ private static String[] argsAfterOptions(String[] argv, String optstring, String
public int getopt() {
int c = super.getopt();
if (c == '?' || c == ':') {
throw new Daikon.UserError(badOptionMessage() + "; run with -h for usage");
String message = badOptionMessage();
if (usageHint != null) {
message += "; " + usageHint;
}
throw new Daikon.UserError(message);
}
if (c == 'h') {
System.out.println(usage);
System.out.println(usage.get());
throw new Daikon.NormalTermination();
}
return c;
Expand Down
34 changes: 26 additions & 8 deletions java/daikon/tools/runtimechecker/CommandHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import static java.nio.charset.StandardCharsets.UTF_8;

import daikon.Daikon;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.util.stream.Collectors;

/**
* A command handler handles a set of commands. A command is the first argument given to the
Expand All @@ -18,25 +20,41 @@ public boolean handles(String command) {
throw new UnsupportedOperationException();
}

public boolean handle(String[] args) {
/**
* Performs the command.
*
* @param args the command, followed by its arguments
* @throws Daikon.DaikonTerminationException if the program should exit
*/
public void handle(String[] args) {
throw new UnsupportedOperationException();
}

public void usageMessage() {
/**
* Returns the usage message, which is the contents of this class's documentation file.
*
* @return the usage message
* @throws Daikon.BugInDaikon if the documentation file is missing
*/
public String usageMessageString() {
String[] classnameArray = getClass().getName().split("\\.");
String simpleClassname = classnameArray[classnameArray.length - 1];

String docFile = simpleClassname + ".doc";
InputStream in = getClass().getResourceAsStream(docFile);
if (in == null) {
System.err.println("Didn't find documentation for " + getClass());
return;
throw new Daikon.BugInDaikon("Didn't find documentation " + docFile + " for " + getClass());
}
// When Daikon requires Java 11 or later, simplify this to:
// try (in) {
// return new String(in.readAllBytes(), UTF_8).stripTrailing();
// }
// and remove the imports of BufferedReader, InputStreamReader, and Collectors.
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println(line);
}
return reader
.lines()
.collect(Collectors.joining(System.lineSeparator()))
.replaceAll("\\s+$", "");
} catch (IOException e) {
throw new UncheckedIOException("problem reading " + docFile, e);
}
Expand Down
1 change: 1 addition & 0 deletions java/daikon/tools/runtimechecker/InstrumentHandler.doc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ instrument [OPTIONS] INV_FILE JAVA_FILES
INV_FILE is an invariant file (suffix .inv or .inv.gz) created by Daikon.
JAVA_FILES is one or more Java files that will be instrumented.
OPTIONS:
-h, --help Display this usage message.
--make_all_fields_public Make all declared fields public.
--directory=DIRNAME Output directory; default: instrumented-classes
--output_only_high_conf_invariants
Expand Down
67 changes: 26 additions & 41 deletions java/daikon/tools/runtimechecker/InstrumentHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@
import static java.util.logging.Level.INFO;

import daikon.Daikon;
import daikon.DaikonGetopt;
import daikon.FileIO;
import daikon.Global;
import daikon.PptMap;
import daikon.tools.jtb.ParseResults;
import gnu.getopt.Getopt;
import gnu.getopt.LongOpt;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import jtb.syntaxtree.*;
import jtb.visitor.TreeDumper;
import jtb.visitor.TreeFormatter;
import org.checkerframework.checker.interning.qual.UsesObjectEquals;

/**
* Instruments a file to check invariant violations at run time. Violated invariants are stored in a
Expand Down Expand Up @@ -71,24 +71,13 @@ public boolean handles(String command) {
// oneFile.roots) {"
)
@Override
public boolean handle(String[] args) {
public void handle(String[] args) {

Daikon.output_format = daikon.inv.OutputFormat.JAVA;
daikon.PrintInvariants.wrap_xml = true;

if (!args[0].equals("instrument")) {
System.err.println("Command (first argument) to instrumenter was not recognized.");
return false;
}

String[] realArgs = new String[args.length - 1];
for (int i = 0; i < realArgs.length; i++) {
realArgs[i] = args[i + 1];
}
String[] realArgs = Arrays.copyOfRange(args, 1, args.length);
Arguments arguments = readArguments(realArgs);
if (arguments == errorWhileReadingArguments) {
return false;
}

// Set up debug traces; note this comes after reading command line options.
daikon.LogHelper.setupLogs(Global.debugAll ? FINE : INFO);
Expand All @@ -102,10 +91,7 @@ public boolean handle(String[] args) {
ppts =
FileIO.read_serialized_pptmap(new File(arguments.invFile), true /* use saved config */);
} catch (IOException e) {
System.err.println("Exception while reading invariant file " + arguments.invFile);
System.err.println(e.getMessage());
e.printStackTrace();
return false;
throw new Daikon.UserError(e, "Problem while reading invariant file " + arguments.invFile);
}

// Compile original sources (because daikon.tools.jtb.Ast accesses
Expand Down Expand Up @@ -177,17 +163,11 @@ public boolean handle(String[] args) {
}

} catch (IOException e) {
System.err.println("Exception while instrumenting " + oneFile.fileName);
System.err.println(e.getMessage());
e.printStackTrace();
return false;
throw new Daikon.UserError(e, "Problem while writing output for " + oneFile.fileName);
}
}

return true;
}

@UsesObjectEquals
private static class Arguments {
String invFile;
List<String> javaFileNames;
Expand All @@ -198,9 +178,17 @@ private static class Arguments {
}
}

private static Arguments errorWhileReadingArguments =
new Arguments("error while reading arguments", new ArrayList<String>());

/** Text appended to the description of a bad command-line argument. */
private static final String usageHint = "run with \"instrument -h\" for usage";

/**
* Parses the command-line arguments of the {@code instrument} command.
*
* @param args the arguments that follow the command name
* @return the parsed arguments
* @throws Daikon.NormalTermination after printing the usage message, if an argument requests it
* @throws Daikon.UserError if an argument is malformed or missing
*/
private Arguments readArguments(String[] args) {

LongOpt[] longopts =
Expand All @@ -213,7 +201,8 @@ private Arguments readArguments(String[] args) {
new LongOpt(directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0),
new LongOpt(checkers_directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0)
};
Getopt g = new Getopt("daikon.tools.runtimechecker.InstrumentHandler", args, "hs", longopts);
DaikonGetopt g = new DaikonGetopt(args, "", longopts, this::usageMessageString);
g.setUsageHint(usageHint);
int c;
while ((c = g.getopt()) != -1) {
switch (c) {
Expand All @@ -236,37 +225,33 @@ private Arguments readArguments(String[] args) {
} else if (Daikon.debug_SWITCH.equals(option_name)) {
daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE);
} else {
System.err.println("Unknown long option received: " + option_name);
throw new Daikon.BugInDaikon("Unhandled long option " + option_name);
}
break;
default:
System.out.println("unrecognized option" + c);
return errorWhileReadingArguments;
throw new Daikon.BugInDaikon("getopt() returned " + c);
}
}
// The index of the first non-option argument -- the name of the
// invariant file.
int argindex = g.getOptind();
if (argindex >= args.length) {
System.out.println("Error: No .inv file or .java file arguments supplied.");
return errorWhileReadingArguments;
throw new Daikon.UserError("No .inv file or .java file arguments supplied; " + usageHint);
}
String invfile = args[argindex];
argindex++;
if (!(invfile.endsWith(".inv") || invfile.endsWith(".inv.gz"))) {
System.out.println("Error: first argument must be a file ending in .inv or .inv.gz.");
return errorWhileReadingArguments;
throw new Daikon.UserError(
"First argument must be a file ending in .inv or .inv.gz; " + usageHint);
}
if (argindex >= args.length) {
System.out.println("Error: No .java file arguments supplied.");
return errorWhileReadingArguments;
throw new Daikon.UserError("No .java file arguments supplied; " + usageHint);
}
List<String> javaFileNames = new ArrayList<>();
for (; argindex < args.length; argindex++) {
String javafile = args[argindex];
if (!javafile.endsWith(".java")) {
System.out.println("File does not end in .java: " + javafile);
return errorWhileReadingArguments;
throw new Daikon.UserError("File does not end in .java: " + javafile + "; " + usageHint);
}
javaFileNames.add(javafile);
}
Expand Down
2 changes: 1 addition & 1 deletion java/daikon/tools/runtimechecker/Main.doc
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ invariant properties are checked at run time.
The instrumenter can operate in the following modes, indicated by the first
argument to the daikon.tools.runtimecheker.Main class:

help
help, -h, --help
Print a usage message
Loading
Loading