package com.laytonsmith.core.functions; import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscovery; import com.laytonsmith.PureUtilities.Common.ReflectionUtils; import com.laytonsmith.PureUtilities.Common.StreamUtils; import com.laytonsmith.PureUtilities.Common.StringUtils; import com.laytonsmith.PureUtilities.Version; import com.laytonsmith.abstraction.Implementation; import com.laytonsmith.annotations.api; import com.laytonsmith.annotations.core; import com.laytonsmith.annotations.hide; import com.laytonsmith.annotations.seealso; import com.laytonsmith.annotations.typeof; import com.laytonsmith.core.CHLog; import com.laytonsmith.core.CHVersion; import com.laytonsmith.core.LogLevel; import com.laytonsmith.core.ObjectGenerator; import com.laytonsmith.core.Optimizable; import com.laytonsmith.core.ParseTree; import com.laytonsmith.core.Prefs; import com.laytonsmith.core.Script; import com.laytonsmith.core.Static; import com.laytonsmith.core.compiler.FileOptions; import com.laytonsmith.core.constructs.CArray; import com.laytonsmith.core.constructs.CClassType; import com.laytonsmith.core.constructs.CClosure; import com.laytonsmith.core.constructs.CFunction; import com.laytonsmith.core.constructs.CNull; import com.laytonsmith.core.constructs.CString; import com.laytonsmith.core.constructs.CVoid; import com.laytonsmith.core.constructs.Construct; import com.laytonsmith.core.constructs.IVariable; import com.laytonsmith.core.constructs.IVariableList; import com.laytonsmith.core.constructs.NativeTypeList; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.environments.CommandHelperEnvironment; import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.environments.GlobalEnv; import com.laytonsmith.core.exceptions.CRE.AbstractCREException; import com.laytonsmith.core.exceptions.CRE.CRECastException; import com.laytonsmith.core.exceptions.CRE.CRECausedByWrapper; import com.laytonsmith.core.exceptions.CRE.CREFormatException; import com.laytonsmith.core.exceptions.CRE.CREThrowable; import com.laytonsmith.core.exceptions.CancelCommandException; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; import com.laytonsmith.core.exceptions.FunctionReturnException; import com.laytonsmith.core.exceptions.StackTraceManager; import com.laytonsmith.core.natives.interfaces.Mixed; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * */ @core public class Exceptions { public static String docs() { return "This class contains functions related to Exception handling in MethodScript"; } @api(environments=CommandHelperEnvironment.class) @seealso({_throw.class, com.laytonsmith.tools.docgen.templates.Exceptions.class}) public static class _try extends AbstractFunction { @Override public String getName() { return "try"; } @Override public Integer[] numArgs() { return new Integer[]{1, 2, 3, 4}; } @Override public String docs() { return "void {tryCode, [varName, catchCode, [exceptionTypes]] | tryCode, catchCode} This function works similar to a try-catch block in most languages. If the code in" + " tryCode throws an exception, instead of killing the whole script, it stops running, and begins running the catchCode." + " var should be an ivariable, and it is set to an array containing information about the exception." + " Consider using try/catch blocks instead of the try function." + " ---- If exceptionTypes is provided, it should be an array of exception types, or a single string that this try function is interested in." + " If the exception type matches one of the values listed, the exception will be caught, otherwise, the exception will continue up the stack." + " If exceptionTypes is missing, it will catch all exceptions." + " PLEASE NOTE! This function will not catch exceptions thrown by CommandHelper, only built in exceptions. " + " Please see [[CommandHelper/Exceptions|the wiki page on exceptions]] for more information about what possible " + " exceptions can be thrown and where, and examples."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class, CREFormatException.class}; } @Override public boolean isRestricted() { return false; } @Override public boolean preResolveVariables() { return false; } @Override public CHVersion since() { return CHVersion.V3_1_2; } @Override public Boolean runAsync() { return null; } @Override public Construct execs(Target t, Environment env, Script that, ParseTree... nodes) { ParseTree tryCode = nodes[0]; ParseTree varName = null; ParseTree catchCode = null; ParseTree types = null; if (nodes.length == 2) { catchCode = nodes[1]; } else if (nodes.length == 3) { varName = nodes[1]; catchCode = nodes[2]; } else if (nodes.length == 4) { varName = nodes[1]; catchCode = nodes[2]; types = nodes[3]; } IVariable ivar = null; if (varName != null) { Construct pivar = that.eval(varName, env); if (pivar instanceof IVariable) { ivar = (IVariable) pivar; } else { throw new CRECastException("Expected argument 2 to be an IVariable", t); } } List<String> interest = new ArrayList<String>(); if (types != null) { Construct ptypes = that.seval(types, env); if (ptypes instanceof CString) { interest.add(ptypes.val()); } else if (ptypes instanceof CArray) { CArray ca = (CArray) ptypes; for (int i = 0; i < ca.size(); i++) { interest.add(ca.get(i, t).val()); } } else { throw new CRECastException("Expected argument 4 to be a string, or an array of strings.", t); } } for (String in : interest) { try { NativeTypeList.getNativeClass(in); } catch (ClassNotFoundException e) { throw new CREFormatException("Invalid exception type passed to try():" + in, t); } } try { that.eval(tryCode, env); } catch (ConfigRuntimeException e) { String name = AbstractCREException.getExceptionName(e); if (Prefs.DebugMode()) { StreamUtils.GetSystemOut().println("[" + Implementation.GetServerType().getBranding() + "]:" + " Exception thrown (debug mode on) -> " + e.getMessage() + " :: " + name + ":" + e.getTarget().file() + ":" + e.getTarget().line()); } if (name != null && (interest.isEmpty() || interest.contains(name))) { if (catchCode != null) { CArray ex = ObjectGenerator.GetGenerator().exception(e, env, t); if (ivar != null) { ivar.setIval(ex); env.getEnv(GlobalEnv.class).GetVarList().set(ivar); } that.eval(catchCode, env); } } else { throw e; } } return CVoid.VOID; } @Override public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { return CVoid.VOID; } @Override public boolean useSpecialExec() { return true; } } @api @seealso({_try.class, com.laytonsmith.tools.docgen.templates.Exceptions.class}) public static class _throw extends AbstractFunction { @Override public String getName() { return "throw"; } @Override public Integer[] numArgs() { return new Integer[]{1, 2, 3}; } @Override public String docs() { Set<Class<CREThrowable>> e = ClassDiscovery.getDefaultInstance().loadClassesWithAnnotationThatExtend(typeof.class, CREThrowable.class); String exceptions = "\nValid Exceptions: "; List<String> ee = new ArrayList<>(); for (Class<CREThrowable> c : e) { String exceptionType = c.getAnnotation(typeof.class).value(); ee.add(exceptionType); } Collections.sort(ee); exceptions += StringUtils.Join(ee, ", ", ", and "); return "nothing {exceptionType, msg, [causedBy] | exception} This function causes an exception to be thrown. exceptionType may be any valid exception type." + "\n\nThe core exception types are: " + exceptions + "\n\nThere may be other exception types as well, refer to the documentation of any extensions you have installed."; } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREFormatException.class}; } @Override public boolean isRestricted() { return false; } @Override public CHVersion since() { return CHVersion.V3_1_2; } @Override public Boolean runAsync() { return null; } //The code: try(throw(...), @ex, ...) doesn't work, //because it sees throw, then kills the other children to try. //Blah. // @Override // public boolean isTerminal() { // return true; // } @Override public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException { if(args.length == 1){ try { // Exception type // We need to reverse the excpetion into an object throw ObjectGenerator.GetGenerator().exception(Static.getArray(args[0], t), t); } catch (ClassNotFoundException ex) { throw new CRECastException(ex.getMessage(), t); } } else { if (args[0] instanceof CNull) { CHLog.GetLogger().Log(CHLog.Tags.DEPRECATION, LogLevel.ERROR, "Uncatchable exceptions are no longer supported.", t); throw new CRECastException("An exception type must be specified", t); } Class<Mixed> c; try { c = NativeTypeList.getNativeClass(args[0].val()); } catch (ClassNotFoundException ex) { throw new CREFormatException("Expected a valid exception type, but found \"" + args[0].val() + "\"", t); } List<Class> classes = new ArrayList<>(); List<Object> arguments = new ArrayList<>(); classes.add(String.class); classes.add(Target.class); arguments.add(args[1].val()); arguments.add(t); if(args.length == 3){ classes.add(Throwable.class); arguments.add(new CRECausedByWrapper(Static.getArray(args[2], t))); } CREThrowable throwable = (CREThrowable)ReflectionUtils.newInstance(c, classes.toArray(new Class[classes.size()]), arguments.toArray()); throw throwable; } } } @api @seealso({_throw.class, _try.class, com.laytonsmith.tools.docgen.templates.Exceptions.class}) public static class set_uncaught_exception_handler extends AbstractFunction{ @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CRECastException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { if(args[0] instanceof CClosure){ CClosure old = environment.getEnv(GlobalEnv.class).GetExceptionHandler(); environment.getEnv(GlobalEnv.class).SetExceptionHandler((CClosure)args[0]); if(old == null){ return CNull.NULL; } else { return old; } } else { throw new CRECastException("Expecting arg 1 of " + getName() + " to be a Closure, but it was " + args[0].val(), t); } } @Override public String getName() { return "set_uncaught_exception_handler"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "closure {closure(@ex)} Sets the uncaught exception handler, returning the currently set one, or null if none has been" + " set yet. If code throws an exception, instead of doing" + " the default (displaying the error to the user/console) it will run your code instead. The exception" + " that was thrown will be passed to the closure, and it is expected that the closure returns either null," + " true, or false. ---- If null is returned, the default handling will occur. If false is returned, it will" + " be \"escalated\" which in the current implementation is the same as returning null (this will be used" + " in the future). If true is returned, then default action will not occur, as it is assumed you have handled" + " it. Only one exception handler can be registered at this time. If code inside the closure generates it's own" + " exception, this will be handled by displaying both exceptions. To prevent this, you could put a try() block" + " around the whole code block, but it is highly recommended you do not supress this. It is possible to completely" + " supress all runtime exceptions using this method, but it is highly recommended that you still have a generic" + " logging mechanism, perhaps to console, so you don't \"lose\" your exceptions, and fail to realize anything is wrong."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "set_uncaught_exception_handler(closure(@ex){\n" + "\tmsg('Exception caught!');\n" + "\tmsg(@ex);\n" + "\treturn(true);\n" + "});\n\n" + "@zero = 0;\n" + "@exception = 1 / @zero; // This should throw an exception\n", // Can't automatically run this, since the examples don't have // the exception handling fully working. "Exception caught!\n" + "{RangeException, Division by 0!, /path/to/script.ms, 8}"), }; } } @api @hide("In general, this should never be used in the functional syntax, and should only be" + " automatically generated by the try keyword.") public static class complex_try extends AbstractFunction implements Optimizable { /** Please do not change this name or make it final, it is used reflectively for testing */ @SuppressWarnings("FieldMayBeFinal") private static boolean doScreamError = false; @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return false; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { return CVoid.VOID; } @Override public Construct execs(Target t, Environment env, Script parent, ParseTree... nodes) { boolean exceptionCaught = false; ConfigRuntimeException caughtException = null; try { parent.eval(nodes[0], env); } catch (ConfigRuntimeException ex){ if(!(ex instanceof AbstractCREException)){ // This should never actually happen, but we want to protect // against errors, and continue to throw this one up the chain throw ex; } AbstractCREException e = AbstractCREException.getAbstractCREException(ex); CClassType exceptionType = new CClassType(e.getExceptionType(), t); for(int i = 1; i < nodes.length - 1; i+=2){ ParseTree assign = nodes[i]; CClassType clauseType = ((CClassType)assign.getChildAt(0).getData()); if(exceptionType.unsafeDoesExtend(clauseType)){ try { // We need to define the exception in the variable table IVariableList varList = env.getEnv(GlobalEnv.class).GetVarList(); IVariable var = (IVariable)assign.getChildAt(1).getData(); // This should eventually be changed to be of the appropriate type. Unfortunately, that will // require reworking basically everything. We need all functions to accept Mixed, instead of Construct. // This will have to do in the meantime. varList.set(new IVariable(new CClassType("array", t), var.getVariableName(), e.getExceptionObject(), t)); parent.eval(nodes[i + 1], env); varList.remove(var.getVariableName()); } catch (ConfigRuntimeException | FunctionReturnException newEx){ if(newEx instanceof ConfigRuntimeException){ caughtException = (ConfigRuntimeException)newEx; } exceptionCaught = true; throw newEx; } return CVoid.VOID; } } // No clause caught it. Continue to throw the exception up the chain caughtException = ex; exceptionCaught = true; throw ex; } finally { if(nodes.length % 2 == 0){ // There is a finally clause. Run that here. try { parent.eval(nodes[nodes.length - 1], env); } catch(ConfigRuntimeException | FunctionReturnException ex){ if(exceptionCaught && (doScreamError || Prefs.ScreamErrors() || Prefs.DebugMode())){ CHLog.GetLogger().Log(CHLog.Tags.RUNTIME, LogLevel.WARNING, "Exception was thrown and" + " unhandled in any catch clause," + " but is being hidden by a new exception being thrown in the finally clause.", t); ConfigRuntimeException.HandleUncaughtException(caughtException, env); } throw ex; } } } return CVoid.VOID; } @Override public String getName() { return "complex_try"; } @Override public Integer[] numArgs() { return new Integer[]{Integer.MAX_VALUE}; } @Override public String docs() { return "void {tryBlock, [catchVariable, catchBlock]+, [catchBlock]}"; } @Override public Version since() { return CHVersion.V3_3_1; } @Override public ParseTree optimizeDynamic(Target t, List<ParseTree> children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { List<CClassType> types = new ArrayList<>(); for(int i = 1; i < children.size() - 1; i+=2){ // TODO: Eh.. should probably move this check into the keyword, since techincally // catch(Exception @e = null) { } would work. ParseTree assign = children.get(i); if(assign.getChildAt(0).getData() instanceof CString){ // This is an unknown exception type, because otherwise it would have been cast to a CClassType throw new ConfigCompileException("Unknown class type: " + assign.getChildAt(0).getData().val(), t); } types.add((CClassType)assign.getChildAt(0).getData()); if(CFunction.IsFunction(assign, DataHandling.assign.class)) { // assign() will validate params 0 and 1 CClassType type = ((CClassType)assign.getChildAt(0).getData()); if(!type.unsafeDoesExtend(new CClassType("Throwable", t))){ throw new ConfigCompileException("The type defined in a catch clause must extend the" + " Throwable class.", t); } if(!(assign.getChildAt(2).getData() instanceof CNull)){ throw new ConfigCompileException("Assignments are not allowed in catch clauses", t); } continue; } throw new ConfigCompileException("Expecting a variable declaration, but instead " + assign.getData().val() + " was found", t); } for(int i = 0; i < types.size(); i++){ CClassType t1 = types.get(i); for(int j = i + 1; j < types.size(); j++){ CClassType t2 = types.get(j); if(t1.equals(t2)){ throw new ConfigCompileException("Duplicate catch clauses found. Only one clause may" + " catch exceptions of a particular type, but we found that " + t1.val() + " has" + " a duplicate signature", t); } } } return null; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of(OptimizationOption.OPTIMIZE_DYNAMIC); } @Override public boolean preResolveVariables() { return false; } @Override public boolean useSpecialExec() { return true; } } @api @seealso({com.laytonsmith.tools.docgen.templates.Exceptions.class}) public static class get_stack_trace extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return null; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { StackTraceManager stManager = environment.getEnv(GlobalEnv.class).GetStackTraceManager(); List<ConfigRuntimeException.StackTraceElement> elements = stManager.getCurrentStackTrace(); CArray ret = new CArray(t); for(ConfigRuntimeException.StackTraceElement e : elements){ ret.push(e.getObjectFor(), Target.UNKNOWN); } return ret; } @Override public String getName() { return "get_stack_trace"; } @Override public Integer[] numArgs() { return new Integer[]{0}; } @Override public String docs() { return "array {} Returns an array of stack trace elements. This is the same stack trace that would be generated" + " if one were to throw an exception, then catch it."; } @Override public Version since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Basic usage", "proc _a(){\n" + "\t_b();\n" + "}\n" + "\n" + "proc _b(){\n" + "\tmsg(get_stack_trace());\n" + "}\n" + "\n" + "_a();") }; } } }