package com.laytonsmith.core.functions; import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscovery; import com.laytonsmith.PureUtilities.ClassLoading.DynamicEnum; import com.laytonsmith.PureUtilities.Common.ReflectionUtils; import com.laytonsmith.PureUtilities.Version; import com.laytonsmith.annotations.MDynamicEnum; import com.laytonsmith.annotations.MEnum; import com.laytonsmith.annotations.api; import com.laytonsmith.annotations.core; import com.laytonsmith.core.CHVersion; import com.laytonsmith.core.Optimizable; import com.laytonsmith.core.ParseTree; import com.laytonsmith.core.Procedure; 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.CFunction; import com.laytonsmith.core.constructs.CInt; import com.laytonsmith.core.constructs.CNull; import com.laytonsmith.core.constructs.CString; import com.laytonsmith.core.constructs.Construct; import com.laytonsmith.core.constructs.IVariable; 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.events.Event; import com.laytonsmith.core.events.EventList; import com.laytonsmith.core.exceptions.CRE.CREFormatException; import com.laytonsmith.core.exceptions.CRE.CREIOException; import com.laytonsmith.core.exceptions.CRE.CREInsufficientArgumentsException; import com.laytonsmith.core.exceptions.CRE.CREThrowable; import com.laytonsmith.core.exceptions.ConfigCompileException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; import com.laytonsmith.persistence.DataSourceFactory; import com.laytonsmith.persistence.PersistenceNetwork; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * */ @core public class Reflection { public static String docs() { return "This class of functions allows scripts to hook deep into the interpreter itself," + " and get meta information about the operations of a script. This is useful for" + " debugging, testing, and ultra dynamic scripting. See the" + " [[CommandHelper/Reflection|guide to reflection]] on the wiki for more" + " details. In order to make the most of these functions, you should familiarize" + " yourself with the general workings of the language. These functions explore" + " extremely advanced concepts, and should normally not be used; especially" + " if you are not familiar with the language."; } @api(environments={CommandHelperEnvironment.class}) public static class reflect_pull extends AbstractFunction { private static Set<Construct> protocols; @Override public String getName() { return "reflect_pull"; } @Override public Integer[] numArgs() { return new Integer[]{Integer.MAX_VALUE}; } @Override public String docs() { return "mixed {param, [args, ...]} Returns information about the runtime in a usable" + " format. Depending on the information returned, it may be useable directly," + " or it may be more of a referential format. ---- The following items can be retrieved:" + "<table><tr><th>param</th><th>args</th><th>returns/description</th></tr>" + "<tr><td>label</td><td></td><td>Return the label that the script is currently running under</td></tr>" + "<tr><td>command</td><td></td><td>Returns the command that was used to fire off this script (if applicable)</td></tr>" + "<tr><td>varlist</td><td>[name]</td><td>Returns a list of currently in scope variables. If name" + " is provided, the currently set value is instead returned.</td></tr>" + "<tr><td>line_num</td><td></td><td>The current line number</td></tr>" + "<tr><td>file</td><td></td><td>The absolute path to the current file</td></tr>" + "<tr><td>col</td><td></td><td>The current column number</td></tr>" + "<tr><td>datasources</td><td></td><td>An array of data source protocols available</td></tr>" + "<tr><td>enum</td><td>[enum name]</td><td>An array of enum names, or if one if provided, a list of all" + " the values in that enum</td></tr>" + "</table>"; //+ "<tr><td></td><td></td><td></td></tr>" } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREFormatException.class, CREIOException.class}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return null; } @Override public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException { if (args.length < 1) { throw new CREInsufficientArgumentsException("Not enough parameters was sent to " + getName(), t); } String param = args[0].val(); if ("label".equalsIgnoreCase(param)) { return new CString(env.getEnv(GlobalEnv.class).GetLabel(), t); } else if ("command".equalsIgnoreCase(param)) { return new CString(env.getEnv(CommandHelperEnvironment.class).GetCommand(), t); } else if ("varlist".equalsIgnoreCase(param)) { if (args.length == 1) { //No name provided CArray ca = new CArray(t); for (String name : env.getEnv(GlobalEnv.class).GetVarList().keySet()) { ca.push(new CString(name, t), t); } return ca; } else if (args.length == 2) { //The name was provided String name = args[1].val(); return env.getEnv(GlobalEnv.class).GetVarList().get(name, t).ival(); } } else if ("line_num".equalsIgnoreCase(param)) { return new CInt(t.line(), t); } else if ("file".equalsIgnoreCase(param)) { if (t.file() == null) { return new CString("Unknown (maybe the interpreter?)", t); } else { try { return new CString(t.file().getCanonicalPath().replace('\\', '/'), t); } catch (IOException ex) { throw new CREIOException(ex.getMessage(), t); } } } else if ("col".equalsIgnoreCase(param)) { return new CInt(t.col(), t); } else if("datasources".equalsIgnoreCase(param)){ if(protocols == null){ protocols = new HashSet<Construct>(); for(String s : DataSourceFactory.GetSupportedProtocols()){ protocols.add(new CString(s, Target.UNKNOWN)); } } return new CArray(t, protocols); } else if("enum".equalsIgnoreCase(param)){ CArray a = new CArray(t); Set<Class<Enum>> enums = ClassDiscovery.getDefaultInstance().loadClassesWithAnnotationThatExtend(MEnum.class, Enum.class); Set<Class<DynamicEnum>> dEnums = ClassDiscovery.getDefaultInstance().loadClassesWithAnnotationThatExtend(MDynamicEnum.class, DynamicEnum.class); if(args.length == 1){ //No name provided for(Class<Enum> e : enums){ a.push(new CString(e.getAnnotation(MEnum.class).value(), t), t); } for (Class<DynamicEnum> d : dEnums) { a.push(new CString(d.getAnnotation(MDynamicEnum.class).value(), t), t); } } else if(args.length == 2){ String enumName = args[1].val(); for(Class<Enum> e : enums){ if(e.getAnnotation(MEnum.class).value().equals(enumName)){ for(Enum ee : e.getEnumConstants()){ a.push(new CString(ee.name(), t), t); } break; } } for (Class<DynamicEnum> d : dEnums) { if (d.getAnnotation(MDynamicEnum.class).value().equals(enumName)) { for (DynamicEnum ee : (Collection<DynamicEnum>) ReflectionUtils.invokeMethod(d, null, "values")) { a.push(new CString(ee.name(), t), t); } break; } } } return a; } throw new CREFormatException("The arguments passed to " + getName() + " are incorrect. Please check them and try again.", t); } @Override public CHVersion since() { return CHVersion.V3_3_1; } } @api public static class reflect_docs extends AbstractFunction implements Optimizable { public static enum DocField { TYPE, RETURN, ARGS, DESCRIPTION; public String getName() { return name().toLowerCase(); } public static DocField getValue(String value) { return DocField.valueOf(value.toUpperCase()); } } @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{CREFormatException.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 { String element = args[0].val(); DocField docField; try { docField = DocField.getValue(args[1].val()); } catch (IllegalArgumentException e) { throw new CREFormatException("Invalid docField provided: " + args[1].val(), t); } //For now, we have special handling, since functions are actually the only thing that will work, //but eventually this will be a generic interface. if (element.startsWith("@")) { IVariable var = environment.getEnv(GlobalEnv.class).GetVarList().get(element, t); if (var == null) { throw new CREFormatException("Invalid variable provided: " + element + " does not exist in the current scope", t); } } else if (element.startsWith("_")) { if (!environment.getEnv(GlobalEnv.class).GetProcs().containsKey(element)) { throw new CREFormatException("Invalid procedure name provided: " + element + " does not exist in the current scope", t); } } else { try { Function f = (Function) FunctionList.getFunction(new CFunction(element, t)); return new CString(formatFunctionDoc(f.docs(), docField), t); } catch (ConfigCompileException ex) { throw new CREFormatException("Unknown function: " + element, t); } } return CNull.NULL; } public String formatFunctionDoc(String docs, DocField field) { Pattern p = Pattern.compile("(?s)\\s*(.*?)\\s*\\{(.*?)\\}\\s*(.*)\\s*"); Matcher m = p.matcher(docs); if (!m.find()) { throw new Error("An error has occured in " + getName() + ". While trying to get the documentation" + ", it was unable to parse this: " + docs); } if (field == DocField.RETURN || field == DocField.TYPE) { return m.group(1); } else if (field == DocField.ARGS) { return m.group(2); } else if (field == DocField.DESCRIPTION) { return m.group(3); } throw new Error("Unhandled case in formatFunctionDoc!"); } @Override public ParseTree optimizeDynamic(Target t, List<ParseTree> children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException { if(children.isEmpty()){ //They are requesting this function's documentation. We can just return a string, //and then it will never actually get called, so we handle it entirely in here. return new ParseTree(new CString(docs(), t), null); } if (children.get(0).isConst()) { //If it's a function, we can check to see if it actually exists, //and make it a compile error if it doesn't, even if parameter 2 is dynamic String value = children.get(0).getData().val(); if (!value.startsWith("_") && !value.startsWith("@")) { //It's a function FunctionList.getFunction(new CFunction(value, t)); } } if (children.get(1).isConst()) { try { DocField.getValue(children.get(1).getData().val()); } catch (IllegalArgumentException e) { throw new ConfigCompileException("Invalid docField provided: " + children.get(1).getData().val(), t); } } return null; } @Override public Set<OptimizationOption> optimizationOptions() { return EnumSet.of( OptimizationOption.CONSTANT_OFFLINE, OptimizationOption.OPTIMIZE_DYNAMIC ); } @Override public String getName() { return "reflect_docs"; } @Override public Integer[] numArgs() { return new Integer[]{0, 2}; } @Override public String docs() { return "string { | element, docField} Returns the documentation for an element. There are 4 things that an element might have," + " and one of these should be passed as the docField argument: type, return, args, description. A valid element is either" + " the name of an ivariable, or a function/proc. For instance, reflect_docs('reflect_docs', 'description') would return" + " what you are reading right now. User defined variables and procs may not have any documentation, in which case null" + " is returned. If the specified argument cannot be found, a FormatException is thrown. If no arguments are passed in," + " it returns the documentation for " + getName() + ", that is, what you're reading right now."; } @Override public CHVersion since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Return type", "reflect_docs('array_contains', 'return'); // Using 'type' would also work"), new ExampleScript("Args", "reflect_docs('array_contains', 'args');"), new ExampleScript("Description", "reflect_docs('array_contains', 'description');") }; } } @api public static class get_functions extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return false; } private static Map<String,List<String>> funcs = new HashMap<String,List<String>>(); private void initf() { for (FunctionBase f : FunctionList.getFunctionList(api.Platforms.INTERPRETER_JAVA)) { String[] pack = f.getClass().getEnclosingClass().getName().split("\\."); String clazz = pack[pack.length - 1]; if (!funcs.containsKey(clazz)) { funcs.put(clazz, new ArrayList<String>()); } funcs.get(clazz).add(f.getName()); } } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { CArray ret = CArray.GetAssociativeArray(t); if (funcs.keySet().size() < 10) { initf(); } for (String cname : funcs.keySet()) { CArray fnames = new CArray(t); for (String fname : funcs.get(cname)) { fnames.push(new CString(fname, t), t); } ret.set(new CString(cname, t), fnames, t); } return ret; } @Override public String getName() { return "get_functions"; } @Override public Integer[] numArgs() { return new Integer[]{0}; } @Override public String docs() { return "array {} Returns an associative array of all loaded functions. The keys of this array are the" + " names of the classes containing the functions (which you know as the sections of the API page)," + " and the values are arrays of the names of the functions within those classes."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api public static class get_events extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return false; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { CArray ret = new CArray(t); for (Event event : EventList.GetEvents()) { ret.push(new CString(event.getName(), t), t); } ret.sort(CArray.SortType.STRING_IC); return ret; } @Override public String getName() { return "get_events"; } @Override public Integer[] numArgs() { return new Integer[]{0}; } @Override public String docs() { return "array {} Returns an array of all registered event names."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api public static class get_aliases extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return false; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { CArray ret = new CArray(t); for (Script s : Static.getAliasCore().getScripts()) { ret.push(new CString(s.getSignature(), t), t); } ret.sort(CArray.SortType.STRING_IC); return ret; } @Override public String getName() { return "get_aliases"; } @Override public Integer[] numArgs() { return new Integer[]{0}; } @Override public String docs() { return "array {} Returns an array of the defined alias signatures (The part left of the = sign)."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api public static class reflect_value_source extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new 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 { PersistenceNetwork pn = environment.getEnv(GlobalEnv.class).GetPersistenceNetwork(); return new CString(pn.getKeySource(args[0].val().split("\\.")).toString(), t); } @Override public String getName() { return "reflect_value_source"; } @Override public Integer[] numArgs() { return new Integer[]{1}; } @Override public String docs() { return "string {persistenceKey} Returns the source file that this key will store a value to in the Persistence Network." + " For instance, in your persistence.ini file, if you have the entry \"storage.test.**=json:///path/to/file.json\"," + " then reflect_value_source('storage.test.testing') would return 'json:///path/to/file.json'. This is useful for" + " debugging, as it will definitively trace back the source/destination of a value."; } @Override public Version since() { return CHVersion.V3_3_1; } } @api public static class get_procedures extends AbstractFunction { @Override public Class<? extends CREThrowable>[] thrown() { return new Class[]{}; } @Override public boolean isRestricted() { return true; } @Override public Boolean runAsync() { return false; } @Override public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException { CArray ret = new CArray(t); for (Map.Entry<String, Procedure> p : environment.getEnv(GlobalEnv.class).GetProcs().entrySet()) { ret.push(new CString(p.getKey(), t), t); } ret.sort(CArray.SortType.STRING_IC); return ret; } @Override public String getName() { return "get_procedures"; } @Override public Integer[] numArgs() { return new Integer[]{0}; } @Override public String docs() { return "array {} Returns an array of procedures callable in the current scope."; } @Override public Version since() { return CHVersion.V3_3_1; } @Override public ExampleScript[] examples() throws ConfigCompileException { return new ExampleScript[]{ new ExampleScript("Simple example", "msg(get_procedures());\n" + "proc _testProc() {}\n" + "msg(get_procedures());"), new ExampleScript("Example with procedures within procedures", "msg(get_procedures());\n" + "proc _testProc() {\n" + "\tproc _innerProc() {}\n" + "\tmsg(get_procedures());\n" + "}\n" + "_testProc();\n" + "msg(get_procedures());") }; } } }