package org.cx4a.rsense;
import java.util.Set;
import java.util.HashSet;
import java.util.Collection;
import java.util.Properties;
import java.io.File;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.HashMap;
import java.util.Map;
import org.cx4a.rsense.ruby.IRubyObject;
import org.cx4a.rsense.util.Logger;
import org.cx4a.rsense.util.StringUtil;
import org.cx4a.rsense.util.SourceLocation;
public class Main {
private static class TestStats {
public int count = 0;
public int success = 0;
public int failure = 0;
public int error = 0;
}
private static class ProgressMonitor extends Thread implements Project.EventListener {
private boolean stop;
private PrintStream out;
private int interval;
private Project.EventListener.Event event;
public ProgressMonitor(PrintStream out, int interval) {
this.out = out;
this.interval = interval;
}
public void run() {
if (interval > 0) {
while (isAlive()) {
print();
try {
Thread.sleep(interval);
} catch (InterruptedException e) {}
}
}
}
public void attach(Project project) {
event = null;
if (interval >= 0) {
project.addEventListener(this);
if (!isAlive())
start();
}
}
public void detach(Project project) {
event = null;
if (interval >= 0)
project.removeEventListener(this);
}
public void update(Project.EventListener.Event event) {
this.event = event;
if (interval == 0)
print();
}
private void print() {
if (event != null) {
switch (event.type) {
case DEFINE:
out.printf("progress: defining method %s...\n", event.name);
break;
case CLASS:
out.printf("progress: defining class %s...\n", event.name);
break;
case MODULE:
out.printf("progress: defining module %s...\n", event.name);
break;
}
}
}
}
private Properties properties;
private File currentDir;
private InputStream in;
private PrintStream out;
private Reader inReader;
private CodeAssist codeAssist;
private TestStats testStats;
private ProgressMonitor progressMonitor;
public static void main(String[] args) throws Exception {
new Main().run(args);
}
public void run(String[] args) throws Exception {
in = System.in;
out = System.out;
inReader = new InputStreamReader(in);
properties = new Properties();
InputStream stream = this.getClass().getResourceAsStream("rsense.properties");
if (stream != null) properties.load(stream);
if (args.length == 0 || args[0].equals("help")) {
usage();
return;
} else if (args[0].equals("version")) {
version();
return;
}
String command = args[0];
Options options = parseOptions(args, 1);
System.out.println("Command: " + command);
System.out.println("\nOptions: " + options);
Logger.getInstance().setLevel(options.getLogLevel());
init(options);
if (options.getLog() != null) {
PrintStream log = new PrintStream(new FileOutputStream(options.getLog(), true));
try {
Logger.getInstance().setOut(log);
start(command, options);
} finally {
log.close();
}
} else {
start(command, options);
}
testResult(options);
}
private void init(Options options) {
codeAssist = new CodeAssist(options);
Integer interval = options.getProgress();
progressMonitor = new ProgressMonitor(out, interval != null ? interval * 1000 : -1);
progressMonitor.setDaemon(true);
}
private void start(String command, Options options) {
command(command, options);
}
private void usage() {
out.print("RSense: Ruby development tools\n"
+ "\n"
+ "Usage: java -jar rsense.jar org.cx4a.rsense.Main command option...\n"
+ "\n"
+ "command:\n"
+ " code-completion - Code completion at specified position.\n"
+ " --file= - File to analyze\n"
+ " --location= - Location where you want to complete (pos, line:col, str)\n"
+ " --prefix= - Specify prefix string to complete\n"
+ "\n"
+ " type-inference - Infer type at specified position.\n"
+ " --file= - File to analyze\n"
+ " --location= - Location where you want to infer (pos, line:col, str)\n"
+ "\n"
+ " find-definition - Infer type at specified position.\n"
+ " --file= - File to analyze\n"
+ " --location= - Location where you want to find (pos, line:col, str)\n"
+ "\n"
+ " where - Print which class/module/method cursor at.\n"
+ " --file= - File to analyze\n"
+ " --line= - Line number to find\n"
+ "\n"
+ " load - Load file without any outputs.\n"
+ " --file= - File to analyze\n"
+ "\n"
+ " script - Run rsense script from file or stdin.\n"
+ " --prompt= - Prompt string in interactive shell mode\n"
+ " --no-prompt - Do not show prompt\n"
+ "\n"
+ " clear - Clear current environment.\n"
+ "\n"
+ " gc - Execute garbage collection.\n"
+ "\n"
+ " list-project - List loaded projects.\n"
+ "\n"
+ " open-project
- Open project in .\n"
+ "\n"
+ " close-project - Close project named .\n"
+ "\n"
+ " environment - Print environment.\n"
+ "\n"
+ " help - Print this help.\n"
+ "\n"
+ " version - Print version information.\n"
+ "\n"
+ "script-command:\n"
+ " exit\n"
+ " quit - Exit script.\n"
+ "\n"
+ "common-options:\n"
+ " --home= - Specify RSense home directory\n"
+ " --debug - Print debug messages (shorthand of --log-evel=debug)\n"
+ " --log= - Log file to output (default stderr)\n"
+ " --log-level= - Log level (fixme, error, warn, message, info, debug)\n"
+ " --progress - Report progress immediately\n"
+ " --progress= - Report progress per seconds\n\n"
+ " --format= - Output format (plain, emacs)\n"
+ " --verbose - Verbose output\n"
+ " --time - Print timing of each command\n"
+ " --encoding= - Input encoding\n"
+ " --load-path= - Load path string (: or ; separated)\n"
+ " --gem-path= - Gem path string (: or ; separated)\n"
+ " --config= - Config file\n"
+ " --project= - Specify project name\n"
+ " --detect-project - Detect project from --file option\n"
+ " --detect-project= - Detect project from specified location\n"
+ "\n"
+ "test-options:\n"
+ " --test= - Specify fixture name\n"
+ " --test-color - Print test result with colors\n"
+ " --should-contain= - Success if data contains expected data\n"
+ " --should-not-contain= - Success if data doesn't contains expected data\n"
+ " --should-be= - Success if data equals to expected data\n"
+ " --should-be-empty - Success if data is empty\n"
+ "\n"
+ "debug-options:\n"
+ " --print-ast - Print parsed AST\n"
);
}
private void version() {
out.println(versionString());
}
private String versionString() {
return "RSense " + properties.getProperty("rsense.version");
}
private Options parseOptions(String[] args, int offset) {
Options options = new Options();
for (int i = offset; i < args.length; i++) {
String arg = args[i];
if (arg.startsWith("--")) {
String[] lr = arg.substring(2).split("=");
if (lr.length >= 1) {
options.addOption(lr[0], lr.length >= 2 ? lr[1] : null);
}
} else {
options.addRestArg(arg);
}
}
String config = options.getConfig();
if (config != null) {
File configFile = new File(config);
if (configFile.isFile()) {
options.loadConfig(configFile);
}
}
return options;
}
static HashMap map = new HashMap();
private void script(Options options) {
if (options.getRestArgs().isEmpty()) {
runScript(in, options, false);
} else {
try {
for (String filename : options.getRestArgs()) {
if (filename.contains("*")) {
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + filename);
for (String file: currentDir.list()) {
if (matcher.matches(new File(file).toPath()) && map.get(file) == null) {
map.put(file, true);
runFileScript(file, options);
}
}
} else {
runFileScript(filename, options);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private void runFileScript(String filename, Options options) throws FileNotFoundException, IOException {
File file;
if (currentDir == null || !(file = new File(currentDir, filename)).exists()) {
// Load from current directory if possible
file = new File(filename);
}
File oldCurrentDir = currentDir;
currentDir = file.getParentFile();
InputStream in = new FileInputStream(file);
try {
runScript(in, options, true);
} finally {
in.close();
currentDir = oldCurrentDir;
}
}
private void runScript(InputStream in, Options options, boolean noprompt) {
String prompt = options.getPrompt();
if (prompt == null) {
prompt = options.defaultPrompt();
}
Reader oldInReader = inReader;
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(in, options.getEncoding()));
inReader = reader;
String line;
String endMark = options.getEndMark();
if (endMark != null && endMark.length() == 0) {
endMark = Options.defaultEndMark();
}
while (true) {
if (!noprompt) {
out.print(prompt);
}
line = reader.readLine();
if (line == null) {
break;
} else if (line.matches("^\\s*#.*")) {
// comment
continue;
}
String[] argv = StringUtil.shellwords(line);
if (argv.length > 0) {
String command = argv[0];
if (command.equals("exit") || command.equals("quit")) {
return;
}
Options opts = parseOptions(argv, 1);
opts.inherit(options);
command(command, opts);
if (endMark != null) {
out.println(endMark);
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
this.inReader = oldInReader;
}
}
private void command(String command, Options options) {
long start = System.currentTimeMillis();
Logger.info("command: %s", command);
if (options.isTest() && !options.isKeepEnv()) {
codeAssist.clear();
}
if (command.equals("code-completion")) {
commandCodeCompletion(options);
} else if (command.equals("type-inference")) {
commandTypeInference(options);
} else if (command.equals("find-definition")) {
commandFindDefinition(options);
} else if (command.equals("where")) {
commandWhere(options);
} else if (command.equals("load")) {
commandLoad(options);
} else if (command.equals("script")) {
script(options);
} else if (command.equals("clear")) {
commandClear(options);
} else if (command.equals("gc")) {
commandGC(options);
} else if (command.equals("list-project")) {
commandListProject(options);
} else if (command.equals("open-project")) {
commandOpenProject(options);
} else if (command.equals("close-project")) {
commandCloseProject(options);
} else if (command.equals("environment")) {
commandEnvironment(options);
} else if (command.equals("help")) {
commandHelp(options);
} else if (command.equals("version")) {
commandVersion(options);
} else if (command.length() == 0) {
} else {
commandUnknown(command, options);
}
if (options.isTime()) {
Logger.message("%s: %dms", command, (System.currentTimeMillis() - start));
}
}
private void commandCodeCompletion(Options options) {
CodeCompletionResult result;
Project project = codeAssist.getProject(options);
try {
progressMonitor.attach(project);
if (options.isFileStdin()) {
result = codeAssist.codeCompletion(project,
new File("(stdin)"),
options.getHereDocReader(inReader),
options.getLocation());
} else {
result = codeAssist.codeCompletion(project,
options.getFile(),
options.getEncoding(),
options.getLocation());
}
if (options.isPrintAST()) {
Logger.debug("AST:\n%s", result.getAST());
}
String prefix = options.getPrefix();
if (options.isTest()) {
Set data = new HashSet();
for (CodeCompletionResult.CompletionCandidate completion : result.getCandidates()) {
data.add(completion.getCompletion());
}
test(options, data);
} else {
if (options.isEmacsFormat()) {
out.print("(");
out.print("(completion");
for (CodeCompletionResult.CompletionCandidate completion : result.getCandidates()) {
if (prefix == null || completion.getCompletion().startsWith(prefix)) {
out.printf(" (\"%s\" \"%s\" \"%s\" \"%s\")",
completion.getCompletion(),
completion.getQualifiedName(),
completion.getBaseName(),
completion.getKind());
}
}
out.println(")");
codeAssistError(result, options);
out.println(")");
} else {
for (CodeCompletionResult.CompletionCandidate completion : result.getCandidates()) {
if (prefix == null || completion.getCompletion().startsWith(prefix)) {
out.printf("completion: %s %s %s %s\n",
completion.getCompletion(),
completion.getQualifiedName(),
completion.getBaseName(),
completion.getKind());
}
}
codeAssistError(result, options);
}
}
} catch (Exception e) {
if (options.isTest()) {
testError(options);
}
commandException(e, options);
} finally {
progressMonitor.detach(project);
}
}
private void commandTypeInference(Options options) {
TypeInferenceResult result;
Project project = codeAssist.getProject(options);
try {
progressMonitor.attach(project);
if (options.isFileStdin()) {
result = codeAssist.typeInference(project,
new File("(stdin)"),
options.getHereDocReader(inReader),
options.getLocation());
} else {
result = codeAssist.typeInference(project,
options.getFile(),
options.getEncoding(),
options.getLocation());
}
if (options.isPrintAST()) {
Logger.debug("AST:\n%s", result.getAST());
}
if (options.isTest()) {
Set data = new HashSet();
for (IRubyObject klass : result.getTypeSet()) {
data.add(klass.toString());
}
test(options, data);
} else {
if (options.isEmacsFormat()) {
out.print("(");
out.print("(type");
for (IRubyObject klass : result.getTypeSet()) {
out.print(" \"");
out.print(klass);
out.print("\"");
}
out.println(")");
codeAssistError(result, options);
out.println(")");
} else {
for (IRubyObject klass : result.getTypeSet()) {
out.print("type: ");
out.println(klass);
}
codeAssistError(result, options);
}
}
} catch (Exception e) {
if (options.isTest()) {
testError(options);
}
commandException(e, options);
} finally {
progressMonitor.detach(project);
}
}
private void commandFindDefinition(Options options) {
FindDefinitionResult result;
Project project = codeAssist.getProject(options);
try {
progressMonitor.attach(project);
if (options.isFileStdin()) {
result = codeAssist.findDefinition(project,
new File("(stdin)"),
options.getHereDocReader(inReader),
options.getLocation());
} else {
result = codeAssist.findDefinition(project,
options.getFile(),
options.getEncoding(),
options.getLocation());
}
if (options.isPrintAST()) {
Logger.debug("AST:\n%s", result.getAST());
}
if (options.isTest()) {
Set data = new HashSet();
for (SourceLocation location : result.getLocations()) {
data.add(location.toString());
}
test(options, data);
} else {
if (options.isEmacsFormat()) {
out.print("(");
out.print("(location");
for (SourceLocation location : result.getLocations()) {
if (location.getFile() != null)
out.printf(" (%s . %d)", emacsStringLiteral(location.getFile()), location.getLine());
}
out.println(")");
codeAssistError(result, options);
out.println(")");
} else {
for (SourceLocation location : result.getLocations()) {
out.printf("location: %d %s\n", location.getLine(), location.getFile());
}
codeAssistError(result, options);
}
}
} catch (Exception e) {
if (options.isTest())
testError(options);
commandException(e, options);
} finally {
progressMonitor.detach(project);
}
}
private void commandWhere(Options options) {
WhereResult result;
Project project = codeAssist.getProject(options);
try {
progressMonitor.attach(project);
if (options.isFileStdin()) {
result = codeAssist.where(project,
new File("(stdin)"),
options.getHereDocReader(inReader),
options.getLine());
} else {
result = codeAssist.where(project,
options.getFile(),
options.getEncoding(),
options.getLine());
}
if (options.isPrintAST()) {
Logger.debug("AST:\n%s", result.getAST());
}
if (options.isTest()) {
Set data = new HashSet();
if (result.getName() != null)
data.add(result.getName());
test(options, data);
} else {
if (options.isEmacsFormat()) {
out.print("(");
if (result.getName() != null)
out.printf("(name . \"%s\")", result.getName());
codeAssistError(result, options);
out.println(")");
} else {
if (result.getName() != null) {
out.print("name: ");
out.println(result.getName());
}
codeAssistError(result, options);
}
}
} catch (Exception e) {
if (options.isTest())
testError(options);
commandException(e, options);
} finally {
progressMonitor.detach(project);
}
}
private void commandLoad(Options options) {
LoadResult result;
Project project = codeAssist.getProject(options);
try {
progressMonitor.attach(project);
if (options.isFileStdin()) {
result = codeAssist.load(project,
new File("(stdin)"),
options.getHereDocReader(inReader));
} else {
result = codeAssist.load(project,
options.getFile(),
options.getEncoding());
}
if (options.isPrintAST()) {
Logger.debug("AST:\n%s", result.getAST());
}
if (options.isEmacsFormat()) {
out.print("(");
codeAssistError(result, options);
out.println(")");
} else {
codeAssistError(result, options);
}
} catch (Exception e) {
commandException(e, options);
} finally {
progressMonitor.detach(project);
}
}
private void commandClear(Options options) {
codeAssist.clear();
}
private void commandGC(Options options) {
System.gc();
}
private void commandListProject(Options options) {
boolean first = true;
boolean verbose = options.isVerbose();
if (options.isEmacsFormat()) {
out.print("(");
}
for (Map.Entry entry : codeAssist.getProjects().entrySet()) {
String name = entry.getKey();
Project project = entry.getValue();
if (verbose) {
if (!first) {
out.println();
}
first = false;
out.printf("%s:\n", name);
out.printf(" path: %s\n", project.getPath());
out.println(" load-path:");
for (File dir : project.getLoadPath()) {
out.println(" - " + dir.toString());
}
out.println(" gem-path:");
for (File dir : project.getGemPath()) {
out.println(" - " + dir.toString());
}
} else {
if (options.isEmacsFormat()) {
out.print("\"" + name + "\" ");
} else {
out.println(name);
}
}
}
if (options.isEmacsFormat()) {
out.println(")");
}
}
private void commandOpenProject(Options options) {
for (String name : options.getRestArgs()) {
codeAssist.openProject(name, options);
}
}
private void commandCloseProject(Options options) {
for (String name : options.getRestArgs()) {
codeAssist.closeProject(name);
}
}
private void commandEnvironment(Options options) {
out.println("version: " + versionString());
out.println("home: " + options.getRsenseHome());
out.println("debug: " + (options.isDebug() ? "yes" : "no"));
out.println("log: " + (options.getLog() != null ? options.getLog() : ""));
out.println("load-path:");
for (String path : options.getLoadPath()) {
out.println(" - " + path);
}
out.println("gem-path:");
for (String path : options.getLoadPath()) {
out.println(" - " + path);
}
}
private void commandHelp(Options options) {
if (options.isEmacsFormat()) {
out.print("\"");
}
usage();
if (options.isEmacsFormat()) {
out.println("\"");
}
}
private void commandVersion(Options options) {
if (options.isEmacsFormat()) {
out.println("\"" + versionString() + "\"");
} else {
version();
}
}
private void commandUnknown(String command, Options options) {
if (options.isEmacsFormat()) {
out.printf("((error . \"unknown command: %s\"))\n", command);
} else {
out.printf("unknown command: %s\n", command);
}
}
private void commandException(Exception e, Options options) {
if (options.isEmacsFormat()) {
out.println("((error . \"unexpected error\"))");
} else if (e instanceof Options.InvalidOptionException) {
out.println(e.getMessage());
} else {
out.println("unexpected error:");
e.printStackTrace(out);
}
}
private void test(Options options, Collection data) {
if (options.isShouldBeGiven()) {
Set shouldBe = options.getShouldBe();
if (shouldBe.equals(data)) {
testSuccess(options);
} else {
String expected = shouldBe.isEmpty() ? "empty" : shouldBe.toString();
testFailure(options, "%s should be %s", data, expected);
}
} else {
Set shouldContain = options.getShouldContain();
Set shouldNotContain = options.getShouldNotContain();
for (String str : shouldContain) {
if (!data.contains(str)) {
testFailure(options, "%s should be in %s", str, shouldContain);
return;
}
}
for (String str : shouldNotContain) {
if (data.contains(str)) {
testFailure(options, "%s should not be in %s", str, shouldNotContain);
return;
}
}
testSuccess(options);
}
}
private void testSuccess(Options options) {
testSuccess(options, null);
}
private void testSuccess(Options options, String format, Object... args) {
if (testStats == null) {
testStats = new TestStats();
}
testStats.count++;
testStats.success++;
if (options.isTestColor()) {
out.printf("\33[34m%s\33[0m... [\33[1;32mOK\33[0m]", options.getTest());
} else {
out.printf("%s... [OK]", options.getTest());
}
if (format != null) {
out.print("\n ");
out.printf(format, args);
}
out.println();
}
private void testFailure(Options options) {
testFailure(options, null);
}
private void testFailure(Options options, String format, Object... args) {
if (testStats == null) {
testStats = new TestStats();
}
testStats.count++;
testStats.failure++;
if (options.isTestColor()) {
out.printf("\33[34m%s\33[0m... [\33[1;31mBAD\33[0m]", options.getTest());
} else {
out.printf("%s... [BAD]", options.getTest());
}
if (format != null) {
out.print("\n ");
out.printf(format, args);
}
out.println();
}
private void testError(Options options) {
if (testStats == null) {
testStats = new TestStats();
}
testStats.count++;
testStats.error++;
if (options.isTestColor()) {
out.printf("\33[34m%s\33[0m... [\33[1;31mERROR\33[0m]", options.getTest());
} else {
out.printf("%s... [BAD]", options.getTest());
}
out.println();
}
private void testResult(Options options) {
if (testStats != null) {
String ok, bad, error;
if (options.isTestColor()) {
ok = String.format("\33[32;1m%s\33[0m", testStats.success);
if (testStats.failure > 0) {
bad = String.format("\33[31;1m%s\33[0m", testStats.failure);
} else {
bad = String.valueOf(testStats.failure);
}
if (testStats.error > 0) {
error = String.format("\33[31;1m%s\33[0m", testStats.error);
} else {
error = String.valueOf(testStats.error);
}
} else {
ok = String.valueOf(testStats.success);
bad = String.valueOf(testStats.failure);
error = String.valueOf(testStats.error);
}
out.printf("test: count=%d, success=%s, failure=%s, error=%s\n", testStats.count, ok, bad, error);
}
}
private void codeAssistError(CodeAssistResult result, Options options) {
boolean emacsFormat = options.isEmacsFormat();
if (emacsFormat) {
out.print(" (error");
}
for (CodeAssistError error : result.getErrors()) {
if (emacsFormat) {
out.print(" ");
out.print(error.getShortError());
} else {
out.printf("error: ", error.getShortError());
out.println(error.getCause());
}
}
if (emacsFormat) {
out.println(")");
}
}
private String emacsStringLiteral(String s) {
return "\"" + s.replace("\\", "\\\\").replaceAll("\"", "\\\"") + "\"";
}
}