package org.sugarj.driver.cli; import static org.spoofax.jsglr.client.imploder.ImploderAttachment.getLeftToken; import static org.spoofax.jsglr.client.imploder.ImploderAttachment.getRightToken; import static org.spoofax.jsglr.client.imploder.ImploderAttachment.getTokenizer; import static org.spoofax.terms.Term.tryGetConstructor; import static org.sugarj.common.Log.log; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.GnuParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Options; import org.spoofax.interpreter.core.Tools; import org.spoofax.interpreter.terms.IStrategoList; import org.spoofax.interpreter.terms.IStrategoString; import org.spoofax.interpreter.terms.IStrategoTerm; import org.spoofax.jsglr.client.MultiBadTokenException; import org.spoofax.jsglr.client.ParseTimeoutException; import org.spoofax.jsglr.client.imploder.IToken; import org.spoofax.jsglr.client.imploder.ITokenizer; import org.spoofax.jsglr.client.imploder.ImploderAttachment; import org.spoofax.jsglr.client.imploder.Token; import org.spoofax.jsglr.shared.BadTokenException; import org.spoofax.jsglr.shared.TokenExpectedException; import org.spoofax.terms.TermVisitor; import org.strategoxt.HybridInterpreter; import org.strategoxt.imp.runtime.Environment; import org.sugarj.common.ATermCommands; import org.sugarj.common.CommandExecution; import org.sugarj.common.Log; import org.sugarj.common.path.AbsolutePath; import org.sugarj.common.path.Path; import org.sugarj.driver.Result; import org.sugarj.driver.STRCommands; /** * @author Sebastian Erdweg <seba at informatik uni-marburg de> * * large chunk copied and adapted from org.strategoxt.imp.runtime.parser.ParseErrorHandler */ public class DriverCLI { private static final String CONSOLE_CMD = "sugarj"; private static class Error { public String msg; public int lineStart; public int lineEnd; public int columnStart; public int columEnd; Error(String msg, IToken left, IToken right) { this.msg = msg; this.lineStart = left.getLine(); this.columnStart = left.getColumn(); this.lineEnd = right.getLine(); this.columEnd = right.getColumn(); } /** * start of file error * @param msg */ Error(String msg) { this.msg = msg; this.lineStart = 0; this.columnStart = 0; this.lineEnd = 0; this.columEnd = 0; } } public static boolean processResultCLI(Result res, Path file, String project) throws IOException { if (res == null) { log.log("compilation failed", Log.ALWAYS); return false; } boolean success = res.getCollectedErrors().isEmpty(); for (String s : res.getCollectedErrors()) log.log(s, Log.ALWAYS); for (BadTokenException e : res.getParseErrors()) log.log("syntax error: line " + e.getLineNumber() + " column " + e.getColumnNumber() + ": " + e.getMessage(), Log.ALWAYS); if (res.getSugaredSyntaxTree() == null) return success; IToken tok = ImploderAttachment.getRightToken(res.getSugaredSyntaxTree()); IStrategoTerm tuple = ATermCommands.makeTuple( tok, res.getSugaredSyntaxTree(), ATermCommands.makeString(file.getAbsolutePath(), tok), ATermCommands.makeString(project, tok)); List<Error> errors = gatherNonFatalErrors(res.getSugaredSyntaxTree()); success &= errors.isEmpty(); for (Error error : errors) log.log("error: line " + error.lineStart + " column " + error.columnStart + " to line " + error.lineEnd + " column " + error.columEnd + ":\n " + error.msg, Log.ALWAYS); IStrategoTerm errorTree = STRCommands.assimilate("sugarj-analyze", res.getDesugaringsFile(), tuple, new HybridInterpreter()); assert errorTree.getTermType() == IStrategoTerm.TUPLE && errorTree.getSubtermCount() == 4 : "error in sugarj-analyze, did not return tuple with 4 elements"; IStrategoList semErrors = Tools.termAt(errorTree, 1); IStrategoList warnings = Tools.termAt(errorTree, 2); IStrategoList notes = Tools.termAt(errorTree, 3); success &= semErrors.isEmpty() && warnings.isEmpty() && notes.isEmpty(); for (IStrategoTerm error : semErrors.getAllSubterms()) if (error.getTermType() == IStrategoTerm.LIST) for (IStrategoTerm deepError : error.getAllSubterms()) reportCLI(deepError, "error"); else reportCLI(error, "error"); for (IStrategoTerm warning : warnings.getAllSubterms()) if (warning.getTermType() == IStrategoTerm.LIST) for (IStrategoTerm deepWarning : warning.getAllSubterms()) reportCLI(deepWarning, "warning"); else reportCLI(warning, "warning"); for (IStrategoTerm note : notes.getAllSubterms()) if (note.getTermType() == IStrategoTerm.LIST) for (IStrategoTerm deepNote : note.getAllSubterms()) reportCLI(deepNote, "note"); else reportCLI(note, "note"); // System.out.println(ATermCommands.atermToFile(errorTree)); return success; } private static void reportCLI(IStrategoTerm pairOrList, String kind) throws IOException { assert pairOrList.getTermType() == IStrategoTerm.TUPLE && pairOrList.getSubtermCount() == 2; IStrategoTerm term = Tools.termAt(pairOrList, 0); IStrategoString msg = Tools.termAt(pairOrList, 1); IToken left = ImploderAttachment.getLeftToken(term); IToken right = ImploderAttachment.getRightToken(term); if (left == null && right != null) left = right; else if (left != null && right == null) right = left; if (left == null || right == null) log.log("error: " + msg + "\n in tree " + ATermCommands.atermToFile(term), Log.ALWAYS); else log.log("error: line " + left.getLine() + " column " + left.getColumn() + " to line " + right.getLine() + " column " + right.getColumn() + ":\n " + msg, Log.ALWAYS); } /** * Report WATER + INSERT errors from parse tree */ private static List<Error> gatherNonFatalErrors(IStrategoTerm top) { List<Error> errors = new ArrayList<Error>(); ITokenizer tokenizer = getTokenizer(top); if (tokenizer == null) return errors; for (int i = 0, max = tokenizer.getTokenCount(); i < max; i++) { IToken token = tokenizer.getTokenAt(i); String error = token.getError(); if (error != null) { if (error == ITokenizer.ERROR_SKIPPED_REGION) { i = findRightMostWithSameError(token, null); reportSkippedRegion(token, tokenizer.getTokenAt(i), errors); } else if (error.startsWith(ITokenizer.ERROR_WARNING_PREFIX)) { i = findRightMostWithSameError(token, null); reportWarningAtTokens(token, tokenizer.getTokenAt(i), error, errors); } else if (error.startsWith(ITokenizer.ERROR_WATER_PREFIX)) { i = findRightMostWithSameError(token, ITokenizer.ERROR_WATER_PREFIX); reportErrorAtTokens(token, tokenizer.getTokenAt(i), error, errors); } else { i = findRightMostWithSameError(token, null); // UNDONE: won't work for multi-token errors (as seen in SugarJ) reportErrorAtTokens(token, tokenizer.getTokenAt(i), error, errors); } } } gatherAmbiguities(top, errors); return errors; } private static int findRightMostWithSameError(IToken token, String prefix) { String expectedError = token.getError(); ITokenizer tokenizer = token.getTokenizer(); int i = token.getIndex(); for (int max = tokenizer.getTokenCount(); i + 1 < max; i++) { String error = tokenizer.getTokenAt(i + 1).getError(); if (error != expectedError && (error == null || prefix == null || !error.startsWith(prefix))) break; } return i; } /** * Report recoverable errors (e.g., inserted brackets). * * @param outerBeginOffset The begin offset of the enclosing construct. */ private static void gatherAmbiguities(IStrategoTerm term, final List<Error> errors) { new TermVisitor() { IStrategoTerm ambStart; public void preVisit(IStrategoTerm term) { if (ambStart == null && Environment.getTermFactory().makeConstructor("amb", 1) == tryGetConstructor(term)) { reportAmbiguity(term, errors); ambStart = term; } } @Override public void postVisit(IStrategoTerm term) { if (term == ambStart) ambStart = null; } }.visit(term); } private static void reportAmbiguity(IStrategoTerm amb, List<Error> errors) { reportWarningAtTokens(getLeftToken(amb), getRightToken(amb), "Fragment is ambiguous", errors); } private static void reportSkippedRegion(IToken left, IToken right, List<Error> errors) { // Report entire region reportErrorAtTokens(left, right, ITokenizer.ERROR_SKIPPED_REGION, errors); } private static void reportTokenExpected(ITokenizer tokenizer, TokenExpectedException exception, List<Error> errors) { String message = exception.getShortMessage(); reportErrorNearOffset(tokenizer, exception.getOffset(), message, errors); } private static void reportBadToken(ITokenizer tokenizer, BadTokenException exception, List<Error> errors) { String message; if (exception.isEOFToken() || tokenizer.getTokenCount() <= 1) { message = exception.getShortMessage(); } else { IToken token = tokenizer.getTokenAtOffset(exception.getOffset()); token = findNextNonEmptyToken(token); message = ITokenizer.ERROR_WATER_PREFIX + ": " + token.toString().trim(); } reportErrorNearOffset(tokenizer, exception.getOffset(), message, errors); } private static void reportMultiBadToken(ITokenizer tokenizer, MultiBadTokenException exception, List<Error> errors) { for (BadTokenException e : exception.getCauses()) { reportException(tokenizer, e, errors); // use double dispatch } } private static void reportTimeOut(ITokenizer tokenizer, ParseTimeoutException exception, List<Error> errors) { String message = "Internal parsing error: " + exception.getMessage(); reportErrorAtFirstLine(message, errors); reportMultiBadToken(tokenizer, exception, errors); } private static void reportException(ITokenizer tokenizer, Exception exception, List<Error> errors) { try { throw exception; } catch (ParseTimeoutException e) { reportTimeOut(tokenizer, (ParseTimeoutException) exception, errors); } catch (TokenExpectedException e) { reportTokenExpected(tokenizer, (TokenExpectedException) exception, errors); } catch (MultiBadTokenException e) { reportMultiBadToken(tokenizer, (MultiBadTokenException) exception, errors); } catch (BadTokenException e) { reportBadToken(tokenizer, (BadTokenException) exception, errors); } catch (Exception e) { String message = "Internal parsing error: " + exception; reportErrorAtFirstLine(message, errors); } } private static void reportErrorNearOffset(ITokenizer tokenizer, int offset, String message, List<Error> errors) { IToken errorToken = tokenizer.getErrorTokenOrAdjunct(offset); reportErrorAtTokens(errorToken, errorToken, message, errors); } private static IToken findNextNonEmptyToken(IToken token) { ITokenizer tokenizer = token.getTokenizer(); IToken result = null; for (int i = token.getIndex(), max = tokenizer.getTokenCount(); i < max; i++) { result = tokenizer.getTokenAt(i); if (result.getLength() != 0 && !Token.isWhiteSpace(result)) break; } return result; } private static void reportErrorAtTokens(final IToken left, final IToken right, String message, List<Error> errors) { if (left.getStartOffset() > right.getEndOffset()) { reportErrorNearOffset(left.getTokenizer(), left.getStartOffset(), message, errors); } else { errors.add(new Error(message, left, right)); } } private static void reportWarningAtTokens(final IToken left, final IToken right, final String message, List<Error> errors) { errors.add(new Error(message, left, right)); } private static void reportErrorAtFirstLine(String message, List<Error> errors) { errors.add(new Error(message)); } /** * Parses and processes command line options. This method may * set paths and flags in {@link CommandExecution} and * {@link Environment} in the process. * * @param args * the command line arguments to be parsed * @return the source file to be processed * @throws CLIError * when the command line is not correct */ public static String[] handleOptions(String[] args, org.sugarj.common.Environment environment) { Options options = specifyOptions(); try { CommandLine line = parseOptions(options, args); return processOptions(options, line, environment); } catch (org.apache.commons.cli.ParseException e) { throw new CLIError(e.getMessage(), options); } } static void showUsageMessage(Options options) { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp( CONSOLE_CMD + " [options] source-files", options, false); } // cai 24.09.12 // constructs an AbsolutePath object from command-line argument // paths that are not acceptable verbatim are prepended with a dot // cf. org.sugarj.common.path.AbsolutePath.acceptable() private static AbsolutePath pathArgument(String path){ if (!AbsolutePath.acceptable(path)) { if (path.startsWith(File.separator) || path.startsWith("/")) path = "." + path; else path = "./" + path; } return new AbsolutePath(path); } private static String[] processOptions(Options options, CommandLine line, org.sugarj.common.Environment environment) throws org.apache.commons.cli.ParseException { if (line.hasOption("help")) throw new CLIError("help requested", options); if (line.hasOption("verbose")) { int level = 0; for (String option : line.getOptionValues("verbose")) if ("SILENT".equals(option)) ; else if ("CORE".equals(option)) level |= Log.CORE; else if ("PARSE".equals(option)) level |= Log.PARSE; else if ("TRANSFORM".equals(option)) level |= Log.TRANSFORM; else if ("IMPORT".equals(option)) level |= Log.IMPORT; else if ("BASELANG".equals(option)) level |= Log.BASELANG; else if ("CACHING".equals(option)) level |= Log.CACHING; else if ("DETAIL".equals(option)) level |= Log.DETAIL; else if ("DEBUG".equals(option)) level |= Log.ALWAYS; else throw new CLIError("Unknown verbosity level " + option, options); Log.log.setLoggingLevel(level); } if (line.hasOption("silent-execution")) CommandExecution.SILENT_EXECUTION = true; if (line.hasOption("sub-silent-execution")) CommandExecution.SUB_SILENT_EXECUTION = true; if (line.hasOption("full-command-line")) CommandExecution.FULL_COMMAND_LINE = true; if (line.hasOption("cache-info")) CommandExecution.CACHE_INFO = true; if (line.hasOption("buildpath")) for (String path : line.getOptionValue("buildpath").split(org.sugarj.common.Environment.classpathsep)) environment.addToIncludePath(pathArgument(path)); if (line.hasOption("sourcepath")) { List<Path> sourcePath = new LinkedList<Path>(); for (String path : line.getOptionValue("sourcepath").split(org.sugarj.common.Environment.classpathsep)) sourcePath.add(pathArgument(path)); environment.setSourcePath(sourcePath); } if (line.hasOption("d")) environment.setBin(pathArgument(line.getOptionValue("d"))); if (line.hasOption("cache")) environment.setCacheDir(pathArgument(line.getOptionValue("cache"))); if (line.hasOption("gen-files")) environment.setGenerateFiles(true); if (line.hasOption("atomic-imports")) environment.setAtomicImportParsing(true); if (line.hasOption("no-checking")) environment.setNoChecking(true); if (line.hasOption("language")) { String[] langNames = line.getOptionValues("language"); activateBaseLanguage(langNames); } String[] sources = line.getArgs(); if (sources.length < 1) throw new CLIError("No source files specified.", options); return sources; } private static void activateBaseLanguage(String[] langNames) { for (String langName : langNames) { String clName = "org.sugarj." + langName.toLowerCase() + ".Activator"; try { Class<?> cl = DriverCLI.class.getClassLoader().loadClass(clName); cl.newInstance(); } catch (Exception e) { Log.log.logErr("Could not load base language " + langName + ": " + e.getMessage(), Log.ALWAYS); } } } private static CommandLine parseOptions(Options options, String[] args) throws org.apache.commons.cli.ParseException { CommandLineParser parser = new GnuParser(); return parser.parse(options, args); } private static Options specifyOptions() { Options options = new Options(); options.addOption( "v", "verbose", true, "Verbosity. Values are SILENT, CORE, PARSE, TRANSFORM, IMPORT, BASELANG, CACHING, DETAIL, and DEBUG. Use multiple times to activate verbosity for multiple features."); options.addOption( null, "silent-execution", false, "Try to be silent"); options.addOption( null, "sub-silent-execution", false, "Do not display output of subprocesses"); options.addOption( null, "full-command-line", false, "Show all arguments to subprocesses"); options.addOption( null, "cache-info", false, "Show where files are cached"); options.addOption( "cp", "buildpath", true, "Specify where to find compiled files. Multiple paths can be given separated by \'" + org.sugarj.common.Environment.classpathsep + "\'."); options.addOption( null, "sourcepath", true, "Specify where to find source files. Multiple paths can be given separated by \'" + org.sugarj.common.Environment.classpathsep + "\'."); options.addOption( "d", null, true, "Specify where to place compiled files"); options.addOption( null, "help", false, "Print this synopsis of options"); options.addOption( null, "cache", true, "Specifiy a directory for caching."); options.addOption( null, "read-only-cache", false, "Specify the cache to be read-only."); options.addOption( null, "write-only-cache", false, "Specify the cache to be write-only."); options.addOption( null, "gen-files", false, "Generate files?"); options.addOption( null, "atomic-imports", false, "Parse all import statements simultaneously."); options.addOption( null, "no-checking", false, "Do not check resulting SDF and Stratego files."); options.addOption( "l", "language", true, "Specify a base language to activate."); return options; } }