/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * Portions Copyrighted 2007 Sun Microsystems, Inc. */ package indexhelper; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Program which builds the index helper used during indexing to augment * information we cannot glean directly from the code or comments, such as * the valid hashkeys that certain APIs accept. While it's possible to use * some heuristics to figure these out by just looking for the hashkey or splat * argument name and then looking for bulleted lists with hashkeys under a * sentence mentioning the parameter name, it doesn't work in general; many * APIs just say things like "the same options apply here as for the url_for * method", and so on. * * Maintaining this index is obviously painful. I'm -generating- the code * here in such a way that I can generate debug-indexers which tell me * whether there are errors in the data (e.g. certain class methods weren't * encountered during indexing) as well as an optimized indexer used in * production code. * * @todo This was written in Java because I was planning on hooking it up * to other functionality (JRuby AST checks etc) but I see I haven't needed * any of that so it should probably have been written in Ruby instead... * * @author Tor Norbye */ public class Generator { /** When true, the emitted code will be diagnostic, tracking which files were * actually hit during indexing etc. such that I can ensure that there are * no typos etc. */ private static final boolean GENERATE_DEBUG_VERSION = false; private static final String BEGINMARKER = "// BEGIN AUTOMATICALLY GENERATED CODE. SEE THE http://hg.netbeans.org/main/misc/ruby/indexhelper PROJECT FOR DETAILS."; private static final String ENDMARKER = "// END AUTOMATICALLY GENERATED CODE"; private List<MethodDef> methods; public Generator() { } public void run(File file) { initializeData(); checkData(); StringWriter sw = new StringWriter(); BufferedWriter writer = new BufferedWriter(sw); try { writer.write(BEGINMARKER); writer.write("\n"); // Constants we'll need writer.write("public static final String HASH_KEY_BOOL = \"bool\";\n"); writer.write("public static final String HASH_KEY_STRING = \"string\";\n"); writer.write("public static final String HASH_KEY_INTEGER = \"string\";\n\n"); if (GENERATE_DEBUG_VERSION) { generateDebugList(writer); } //generateNameMap(writer); generateAttributeIndexer(writer); writer.write(ENDMARKER); writer.flush(); String newSection = sw.toString(); // Indent //newSection = newSection.replace("\n", "\n "); String[] lines = newSection.split("\n"); StringBuilder sb = new StringBuilder(); for (String line : lines) { if (sb.length() > 0) { sb.append("\n"); } sb.append(" "); // indent sb.append(line); if (line.indexOf('\"') != -1) { sb.append(" // NOI18N"); } } newSection = sb.toString(); sb = new StringBuilder(); BufferedReader br = new BufferedReader(new FileReader(file)); while (true) { String s = br.readLine(); if (s == null) { break; } sb.append(s); sb.append("\n"); } br.close(); // Attempt to replace the contents String oldContents = sb.toString(); int start = oldContents.indexOf(BEGINMARKER); assert start != -1; int end = oldContents.indexOf(ENDMARKER); assert end != -1; String newContents = oldContents.substring(0, start) + newSection + oldContents.substring(end+ENDMARKER.length()); // Update the file BufferedWriter bw = new BufferedWriter(new FileWriter(file)); bw.write(newContents); bw.flush(); bw.close(); } catch (IOException ioe) { ioe.printStackTrace(); } // Attempt to replace file contents... } private void generateDebugList(Writer writer) throws IOException { assert GENERATE_DEBUG_VERSION; writer.write("\nprivate static List<String> unused = new ArrayList<String>();\n"); writer.write("static {\n"); for (MethodDef m : methods) { String s = m.classname + "#" + m.methodPrefix; writer.write(" unused.add(\"" + s + "\");\n"); } writer.write(" unused.add(\"Dummy item, should be only result in unused after indexing\");\n"); writer.write(" Runtime.getRuntime().addShutdownHook(new Thread() {\n"); writer.write(" public void run() {\n"); writer.write(" if (unused.size() > 0) {\n for (int i = 0; i < 5; i++) java.awt.Toolkit.getDefaultToolkit().beep();\n }\n"); writer.write(" System.err.println(\"***Unused index entries (\" + unused.size() + \")=\" + unused);\n"); writer.write(" }\n"); writer.write(" });\n"); writer.write("}\n"); writer.write("public static void showUnused() {\n"); writer.write(" System.err.println(\"***Unused index entries (\" + unused.size() + \")=\" + unused);\n"); writer.write("}\n"); } private void generateNameMap(Writer writer) throws IOException { Set<String> fileSet = new HashSet<String>(); for (MethodDef m : methods) { final String filename = m.filename; assert filename.length() > 2 : filename; // I will be keying by first two chars fileSet.add(filename); } List<String> filenames = new ArrayList<String>(fileSet); Collections.sort(filenames); // Ensure all filenames have at least two chars writer.write("private static Set<String> knownFiles = new HashSet<String>(" + 2*filenames.size() + ");\n"); writer.write("static {\n"); for (String s : filenames) { writer.write(" knownFiles.add(\""); writer.write(s); writer.write("\");\n"); } writer.write("}\n"); writer.write("private static String isSpecial(FileObject file) {\n"); writer.write(" return knownFiles.contains(file.getName());\n"); writer.write("}\n\n"); } private void generateAttributeIndexer(Writer writer) throws IOException { writer.write("private static String clz(Node root, Node n) {\n"); writer.write(" AstPath path = new AstPath(root, n);\n"); writer.write(" String clz = AstUtilities.getFqnName(path);\n"); writer.write(" return clz;\n"); writer.write("}\n\n"); writer.write("private static String sig(MethodDefNode method) {\n"); writer.write(" return AstUtilities.getDefSignature(method);\n"); writer.write("}\n\n"); writer.write("private static String getAttribute(" + (GENERATE_DEBUG_VERSION ? "RubyParseResult result, " : "") + "FileObject file, Node root, MethodDefNode method) {\n"); // Get files Set<String> fileSet = new HashSet<String>(); //Map<Integer,List<MethodDef>> methodMap = new HashMap<Integer, List<Generator.MethodDef>>(); Map<String,List<MethodDef>> methodMap = new HashMap<String, List<Generator.MethodDef>>(); for (MethodDef m : methods) { final String filename = m.filename; assert filename.length() > 2 : filename; // I will be keying by first two chars fileSet.add(filename); List<MethodDef> l = methodMap.get(filename); if (l == null) { l = new ArrayList<MethodDef>(); methodMap.put(filename, l); } l.add(m); // // // Generate key // int firstChar = filename.charAt(0); // int secondChar = filename.charAt(1); } List<String> filenames = new ArrayList<String>(fileSet); Collections.sort(filenames); // Ensure all filenames have at least two chars // Emit switch block writer.write(" String n = file.getName();\n if (n.length() < 2) {\n return null;\n }\n"); writer.write(" char c = n.charAt(0);\n"); writer.write(" switch (c) {\n"); char prevChar = 0; String prevFileName = ""; boolean firstCase = true; for (String s : filenames) { List<MethodDef> l = methodMap.get(s); assert l != null; char firstChar = s.charAt(0); if (firstChar != prevChar) { if (!firstCase) { writer.write(" break;\n"); } firstCase = false; writer.write(" case '"); writer.write(firstChar); writer.write("':\n"); prevFileName = ""; } if (!prevFileName.equals(s)) { writer.write(" if (\"" + s + "\".equals(n)) {\n"); } String prevClassName = ""; boolean first = true; for (MethodDef m : l) { if (!prevClassName.equals(m.classname)) { if (!first) { writer.write(" return null;\n"); writer.write(" }\n"); } else { writer.write(" String clz = clz(root,method);\n"); } first = false; writer.write(" if (\"" + m.classname + "\".equals(clz)) {\n"); if (GENERATE_DEBUG_VERSION) { writer.write(" Map<String,String> classMap = new HashMap<String,String>();\n"); // Iterate over the signatures String clz = m.classname; for (MethodDef ml : l) { String name = ml.methodPrefix; int paren = name.indexOf('('); if (paren != -1) { name = name.substring(0, paren); } String fqn = clz + "." + name; String args = ml.arguments.replace("\"", "\\\""); writer.write(" classMap.put(\"" + fqn + "\", \"" + args + "\");\n"); } writer.write(" verify(result, classMap);\n\n"); } writer.write(" String sig = sig(method);\n"); } writer.write(" if (sig.startsWith(\"" + m.methodPrefix + "\")) {\n"); if (GENERATE_DEBUG_VERSION) { String item = m.classname + "#" + m.methodPrefix; writer.write(" unused.remove(\"" + item + "\");\n"); } String args = m.arguments; if (m.previousVersion != null) { // This item is version dependent writer.write(" String path = file.getPath();\n"); writer.write(" if (path.indexOf(\"-2\") != -1 || path.indexOf(\"-1\") == -1) {\n"); if (args.indexOf('"') != -1) { args = args.replace("\"", "\\\""); } writer.write(" return \"" + args + "\"; // NOI18N\n"); writer.write(" } else {\n"); String argsOld = m.previousVersion.arguments; if (argsOld.indexOf('"') != -1) { argsOld = argsOld.replace("\"", "\\\""); } writer.write(" return \"" + argsOld + "\"; // NOI18N\n"); writer.write(" }\n"); } else { if (args.indexOf('"') != -1) { args = args.replace("\"", "\\\""); } writer.write(" return \"" + args + "\";\n"); } writer.write(" }\n"); prevClassName = m.classname; } if (prevClassName.length() > 0) { writer.write(" return null;\n"); writer.write(" }\n"); } if (!prevFileName.equals(s)) { writer.write(" return null;\n"); writer.write(" }\n"); } prevFileName = s; prevChar = firstChar; } if (!firstCase) { writer.write(" break;\n"); } writer.write(" }\n"); writer.write(" return null;\n"); writer.write("}\n"); String helperCode = " private static AstElement findElement(List<? extends AstElement> children, String signature) {\n" + " for (AstElement child : children) {\n" + " if (child.getKind() == ElementKind.METHOD) {\n" + " if (signature.endsWith(\".\"+child.getName())) {\n" + " if (signature.endsWith(child.getIn()+\".\"+child.getName())) {\n" + " return child;\n" + " } else {\n" + " System.err.println(\"WARNING - couldn't find element - but it sure looked similar - signature=\" + signature + \" and element=\" + child);\n" + " }\n" + " }\n" + " }\n" + " AstElement result = findElement(child.getChildren(), signature);\n" + " if (result != null) {\n" + " return result;\n" + " }\n" + " }\n" + " \n" + " return null;\n" + " }\n" + " \n" + " private static void verify(RubyParseResult result, Map<String,String> signatureToArgumentsMap) {\n" + " List<? extends AstElement> elements = result.getStructure().getElements();\n" + " for (String signature : signatureToArgumentsMap.keySet()) {\n" + " AstElement element = findElement(elements, signature);\n" + " if (element == null) {\n" + " System.err.println(\"WARNING: No element found for \" + signature);\n" + " continue;\n" + " }\n" + " // Check that the parameters are all there\n" + " List<String> parameters = ((AstMethodElement) element).getParameters();\n" + " Set<String> parametersSet = new HashSet<String>();\n" + " for (String s : parameters) {\n" + " if (s.startsWith(\"*\")) {\n" + " s = s.substring(1);\n" + " } else if (s.startsWith(\"&\")) {\n" + " s = s.substring(1);\n" + " }\n" + " parametersSet.add(s);\n" + " }\n" + " String attributeString = signatureToArgumentsMap.get(signature);\n" + " String[] args = attributeString.split(\",\");\n" + " for (String s : args) {\n" + " int paren = s.indexOf('(');\n" + " if (paren != -1) {\n" + " s = s.substring(0, paren);\n" + " }\n" + " if (!parametersSet.contains(s)) {\n" + " System.err.println(\"WARNING: \" + signature + \" does not contain documented parameter \" + s);\n" + " }\n" + " }\n" + " }\n" + " }\n"; if (GENERATE_DEBUG_VERSION) { writer.write(helperCode); } } /** * @param args the command line arguments */ public static void main(String[] args) { // TODO code application logic here args = new String[] { "/Users/tor/netbeans/hg/main/ruby/src/org/netbeans/modules/ruby/RubyIndexerHelper.java" }; if (args.length != 1) { System.err.println("Usage: " + Generator.class.getName() + " <path to RubyIndexerHelper.java>\n\n" + "The RubyIndexerHelper.java file should be in the ruby/editing module. Running this program\n" + "will overwrite portions of the existing file.\n"); System.exit(0); } File f = new File(args[0]); if (!f.exists()) { System.err.println("File " + f.getPath() + " does not exist."); System.exit(0); } new Generator().run(f); } private void checkData() { for (MethodDef m : methods) { String hashNames = m.arguments; // Parse to make sure we don't have problems later... int offset = 0; while (true) { int paren = hashNames.indexOf('(', offset); if (paren != -1) { paren = hashNames.indexOf(')',paren); assert paren != -1; offset = paren; } else { break; } } } } private void initializeData() { String TABLENAME = "-table"; String COLUMNNAME = "-column"; String MODELNAME = "-model"; String VALIDATIONACTIVE = "validationactive"; // hash type String SUBMITMETHOD = "submitmethod"; // hash type String TABLE_OPTIONS = "(=>id:bool|primary_key:string|options:hash|temporary:bool|force:bool)"; String TABLE_COLUMN_OPTIONS = "(=>limit|default:nil|null:bool|precision|scale)"; String TABLE_COLUMN_TYPE = "(:primary_key|:string|:text|:integer|:float|:decimal|:datetime|:timestamp|:time|:date|:binary|:boolean)"; String HTML_HASH_OPTIONS="class|id"; // XXX what else? methods = new ArrayList<MethodDef>(); // ActiveRecord String clzSchemaStatementsFile = "schema_statements"; String clzSchemaStatements = "ActiveRecord::ConnectionAdapters::SchemaStatements"; methods.add(new MethodDef(clzSchemaStatementsFile, clzSchemaStatements, "create_table(", "options"+TABLE_OPTIONS)); methods.add(new MethodDef(clzSchemaStatementsFile, clzSchemaStatements, "add_column(", "table_name(" + TABLENAME + ")," + "column_name(" + COLUMNNAME + ")," + "options"+TABLE_COLUMN_OPTIONS + "," + "type" + TABLE_COLUMN_TYPE)); methods.add(new MethodDef(clzSchemaStatementsFile, clzSchemaStatements, "change_column(", "table_name(" + TABLENAME + ")," + "column_name(" + COLUMNNAME + ")," + "options"+TABLE_COLUMN_OPTIONS + "," + "type" + TABLE_COLUMN_TYPE)); // Rails 1: MethodDef renameTableV1 = new MethodDef(clzSchemaStatementsFile, clzSchemaStatements, "rename_table(", "name(" + TABLENAME + ")", RailsVersion.V1); // Rails 2: Renamed parameter name to table_name MethodDef renameTable = new MethodDef(clzSchemaStatementsFile, clzSchemaStatements, "rename_table(", "table_name(" + TABLENAME + ")", RailsVersion.V2); renameTable.setPreviousVersion(renameTableV1); methods.add(renameTable); methods.add(new MethodDef(clzSchemaStatementsFile, clzSchemaStatements, "rename_column(", "table_name(" + TABLENAME + ")," + "column_name(" + COLUMNNAME + ")")); methods.add(new MethodDef(clzSchemaStatementsFile, clzSchemaStatements, "change_column_default(", "table_name(" + TABLENAME + ")," + "column_name(" + COLUMNNAME + ")")); // Rails 1: MethodDef dropTableV1 = new MethodDef(clzSchemaStatementsFile, clzSchemaStatements, "drop_table(", "name(" + TABLENAME + ")", RailsVersion.V1); // Rails 2: Renamed parameter name to table_name MethodDef dropTable = new MethodDef(clzSchemaStatementsFile, clzSchemaStatements, "drop_table(", "table_name(" + TABLENAME + ")", RailsVersion.V2); dropTable.setPreviousVersion(dropTableV1); methods.add(dropTable); methods.add(new MethodDef(clzSchemaStatementsFile, clzSchemaStatements, "add_index(", "table_name(" + TABLENAME + ")," + "column_name(" + COLUMNNAME + ")")); methods.add(new MethodDef(clzSchemaStatementsFile, clzSchemaStatements, "remove_index(", "table_name(" + TABLENAME + ")")); methods.add(new MethodDef(clzSchemaStatementsFile, clzSchemaStatements, "remove_column(", "table_name(" + TABLENAME + ")," + "column_name(" + COLUMNNAME + ")")); String tableDefClz = "ActiveRecord::ConnectionAdapters::TableDefinition"; String tableDefClzFile = "schema_definitions"; methods.add(new MethodDef(tableDefClzFile, tableDefClz, "column(", "type"+TABLE_COLUMN_TYPE + "," + "options" + TABLE_COLUMN_OPTIONS)); String activeRecordBaseClz = "ActiveRecord::Base"; String activeRecordBaseClzFile = "base"; methods.add(new MethodDef(activeRecordBaseClzFile, activeRecordBaseClz, "find(", "args(:first|:all),args(=>conditions|order|group|limit|offset|joins|readonly:bool|include|select|from|readonly:bool|lock:bool)")); String associationsClassMethodsFile = "associations"; String associationsClassMethodsClz = "ActiveRecord::Associations::ClassMethods"; String hasManyOptions = "(=>class_name|conditions|order|group|foreign_key|dependent|exclusively_dependent|" + "finder_sql|counter_sql|extend|include|limit|offset|select|as|through|source|source_type|uniq)"; methods.add(new MethodDef(associationsClassMethodsFile, associationsClassMethodsClz, "has_many(", // NOTE - when you add this to a class, a number of new methods are added to it; // I should teach code completion about that // TODO - I can help with the class_name and foreign_key attributes here! "options" + hasManyOptions + ",association_id(" + TABLENAME + ")")); // NOTE - when you add this to a class, a number of new methods are added to it; // I should teach code completion about that // TODO - I can help with the class_name and foreign_key attributes here! String hasOneOptions = "(=>class_name|conditions|order|dependent|foreign_key|include|as)"; methods.add(new MethodDef(associationsClassMethodsFile, associationsClassMethodsClz, "has_one(", "options" + hasOneOptions + "),association_id(" + MODELNAME + ")")); // NOTE - when you add this to a class, a number of new methods are added to it; // I should teach code completion about that // TODO - I can help with the class_name and foreign_key attributes here! String belongsToOptions = "(=>class_name|conditions|foreign_key|counter_cache|include|polymorphic)"; methods.add(new MethodDef(associationsClassMethodsFile, associationsClassMethodsClz, "belongs_to(", "options" + belongsToOptions + "),association_id(" + MODELNAME + ")")); // NOTE - when you add this to a class, a number of new methods are added to it; // I should teach code completion about that // TODO - I can help with the class_name and foreign_key attributes here! String belongsToOptions2 = "(=>class_name|join_table|foreign_key|association_foreign_key|conditions|order|uniq:bool|finder_sql|delete_sql|insert_sql|extend|include|group|limit|offset|select)"; methods.add(new MethodDef(associationsClassMethodsFile, associationsClassMethodsClz, "has_and_belongs_to_many(", "options" + belongsToOptions2 + "),association_id(" + TABLENAME + ")")); String aggregationsClz = "ActiveRecord::Aggregations::ClassMethods"; String aggregationsFile = "aggregations"; methods.add(new MethodDef(aggregationsFile, aggregationsClz, "composed_of(", "options(=>class_name|mapping|allow_nil:bool)")); methods.add(new MethodDef("list", "ActiveRecord::Acts::List::ClassMethods", "acts_as_list(", "options(=>column|scope)")); methods.add(new MethodDef("tree", "ActiveRecord::Acts::Tree::ClassMethods", "acts_as_tree(", "options(=>foreign_key|order|counter_cache)")); methods.add(new MethodDef("nested_set", "ActiveRecord::Acts::NestedSet::ClassMethods", "acts_as_nested_set(", "options(=>parent_column|left_column|right_column|scope)")); //methods.add(new MethodDef("transactions", "ActiveRecord::Transactions::ClassMethods", // No methods with options or docs here String COUNT_OPTIONS = "(=>conditions|joins|include|order|group|select|distinct:bool)"; String CALCULATE_OPTIONS = "(=>conditions|joins|order|group|select|distinct:bool)"; String calculationsFile = "calculations"; String calculationsClz = "ActiveRecord::Calculations::ClassMethods"; methods.add(new MethodDef(calculationsFile, calculationsClz, "calculate(", "options" + CALCULATE_OPTIONS + ",operation(:count|:avg|:min|:max|:sum),column_name(" + COLUMNNAME + ")")); methods.add(new MethodDef(calculationsFile, calculationsClz, "count(", // XXX will the "*" match work? "args" + COUNT_OPTIONS)); methods.add(new MethodDef(calculationsFile, calculationsClz, "minimum(", "options" + CALCULATE_OPTIONS + ",column_name(" + COLUMNNAME + ")")); methods.add(new MethodDef(calculationsFile, calculationsClz, "average(", "options" + CALCULATE_OPTIONS + ",column_name(" + COLUMNNAME + ")")); methods.add(new MethodDef(calculationsFile, calculationsClz, "sum(", "options" + CALCULATE_OPTIONS + ",column_name(" + COLUMNNAME + ")")); methods.add(new MethodDef(calculationsFile, calculationsClz, "maximum(", "options" + CALCULATE_OPTIONS + ",column_name(" + COLUMNNAME + ")")); String validationsFile = "validations"; String validationsClz = "ActiveRecord::Validations::ClassMethods"; methods.add(new MethodDef(validationsFile, validationsClz, "validates_each(", "attrs(=>on:" + VALIDATIONACTIVE + "|allow_nil:bool|if)")); methods.add(new MethodDef(validationsFile, validationsClz, "validates_confirmation_of(", "attr_names(=>on:" + VALIDATIONACTIVE + "|message|if)")); methods.add(new MethodDef(validationsFile, validationsClz, "validates_acceptance_of(", "attr_names(=>on:" + VALIDATIONACTIVE + "|message|if|accept)")); methods.add(new MethodDef(validationsFile, validationsClz, "validates_presence_of(", "attr_names(=>on:" + VALIDATIONACTIVE + "|message|if)")); methods.add(new MethodDef(validationsFile, validationsClz, "validates_length_of(", "attrs(=>minimum|maximum|is|within|in|allow_nil:bool|too_long|too_short|wrong_length|on:" + VALIDATIONACTIVE + "|message|if)")); methods.add(new MethodDef(validationsFile, validationsClz, "validates_uniqueness_of(", "attr_names(=>message|scope|case_sensitive:bool|allow_nil:bool|if)")); methods.add(new MethodDef(validationsFile, validationsClz, "validates_format_of(", "attr_names(=>on:" + VALIDATIONACTIVE + "|message|if|with)")); methods.add(new MethodDef(validationsFile, validationsClz, "validates_inclusion_of(", "attr_names(=>in|message|allow_nil:bool|if)")); methods.add(new MethodDef(validationsFile, validationsClz, "validates_exclusion_of(", "attr_names(=>in|message|allow_nil:bool|if)")); methods.add(new MethodDef(validationsFile, validationsClz, "validates_associated(", "attr_names(=>on:" + VALIDATIONACTIVE + "|if)")); methods.add(new MethodDef(validationsFile, validationsClz, "validates_numericality_of(", "attr_names(=>on:" + VALIDATIONACTIVE + "|message|if|only_integer:bool|allow_nil:bool)")); // TimeStamp, AttributeMethods, XmlSerialization, Locking, Reflection, Observing, Callbacks - nothing // ActionController String actionControllerClz = "ActionController::Base"; String actionControllerFile = "base"; methods.add(new MethodDef(actionControllerFile, actionControllerClz, "url_for(", "options(=>anchor|only_path:bool|controller:controller|action:action|trailing_slash:bool|host|protocol)")); methods.add(new MethodDef(actionControllerFile, actionControllerClz, "redirect_to(", "options(=>anchor|only_path:bool|controller:controller|action:action|trailing_slash:bool|host|protocol),options(:back|\"http://)")); methods.add(new MethodDef(actionControllerFile, actionControllerClz, "render(", // layout:bool applies if action is used // locals: applies if action is used // use_full_path: applies if file is used // collection:collection: applies if partial is used // spacer_template applies if partial is used "options(=>action:action|partial:partial|status|template|file:file|text:string|json|inline|nothing)")); methods.add(new MethodDef(actionControllerFile, actionControllerClz, "render_to_string(", // Same as render( "options(=>action:action|partial:partial|status|template|file:file|text:string|json|inline|nothing)")); methods.add(new MethodDef("cgi_process", actionControllerClz, "process_cgi(", "session_options(=>database_manager|session_key|session_id|new_session|session_expires|session_domain|session_secure|session_path)")); //methods.add(new MethodDef("layout", ActionController::Layout::ClassMethods", // Nothing with smart args here methods.add(new MethodDef("streaming", "ActionController::Streaming", "send_file(", "options(=>filename|type|disposition|stream|buffer_size|status)")); methods.add(new MethodDef("streaming", "ActionController::Streaming", "send_data(", "options(=>filename|type|disposition|status)")); methods.add(new MethodDef("pagination", "ActionController::Pagination", "paginate(", "options(=>singular_name|class_name|per_page|conditions|order|order_by|joins|join|include|selected|count)")); // I'm not sure what the options in count_collection_for_pagination are referring to methods.add(new MethodDef("verification", "ActionController::Verification::ClassMethods", "verify(", "options(=>params|session|flash|method|post:" + SUBMITMETHOD + "|xhr:bool|add_flash:hash|add_headers:hash|redirect_to|render|only:bool|except:bool)")); methods.add(new MethodDef("session_management", "ActionController::SessionManagement::ClassMethods", "session_store=(", "store(:active_record_store|:drb_store|:mem_cache_store|:memory_store)")); methods.add(new MethodDef("session_management", "ActionController::SessionManagement::ClassMethods", "session(", "args(=>on:bool|off:bool|only|except|database_manager|session_key|session_id|new_session|session_expires|session_domain|session_secure|session_path)")); //"ActionController::MimeResponds::InstanceMethods")) { // Nothing for me to do here //if (signature.startsWith("respond_to(")) { //} methods.add(new MethodDef("scaffolding","ActionController::Scaffolding::ClassMethods", "scaffold(", "model_id(" + MODELNAME + "),options(=>suffix:bool)")); //methods.add(new MethodDef("filters", "ActionController::Filters::ClassMethods", // no relevant methods //if (signature.startsWith("scaffold(")) { //} // Nothing for Layout, Dependencies, Benchmarking, Flash, Macros, AutoComplete, Caching, Cookies // ActionView // Rails 1 MethodDef formForV1 = new MethodDef("form_helper", "ActionView::Helpers::FormHelper", "form_for(", "object_name(" + MODELNAME + "),args(=>url:hash|html:hash|builder)", RailsVersion.V1); // Rails 2: Renamed parameter name to record_or_name_or_array MethodDef formFor = new MethodDef("form_helper", "ActionView::Helpers::FormHelper", "form_for(", "record_or_name_or_array(" + MODELNAME + "),(rgs=>url:hash|html:hash|builder)", RailsVersion.V2); formFor.setPreviousVersion(formForV1); methods.add(formFor); // Rails 1 MethodDef fieldsForV1 = new MethodDef("form_helper", "ActionView::Helpers::FormHelper", "fields_for(", "object_name(" + MODELNAME + "),args(=>url:hash)", RailsVersion.V1); // Rails 2 MethodDef fieldsFor = new MethodDef("form_helper", "ActionView::Helpers::FormHelper", "fields_for(", "record_or_name_or_array(" + MODELNAME + "),args(=>url:hash)", RailsVersion.V2); fieldsFor.setPreviousVersion(fieldsForV1); methods.add(fieldsFor); String[] mtds = {"text_field(","password_field(","hidden_field(","file_field(","text_area(","check_box(","radio_button(" }; for (String mtd : mtds) { // Rails 1 MethodDef defV1 = new MethodDef("form_helper", "ActionView::Helpers::FormHelper", mtd, "object_name(" + MODELNAME + ")", RailsVersion.V1); // Rails 2 MethodDef def = new MethodDef("form_helper", "ActionView::Helpers::FormHelper", mtd, "record_or_name_or_array(" + MODELNAME + ")", RailsVersion.V2); def.setPreviousVersion(defV1); methods.add(def); } methods.add(new MethodDef("prototype_helper", "ActionView::Helpers::PrototypeHelper", "observe_field", "options(=>url:hash|function|frequency|update|with|on)")); methods.add(new MethodDef("prototype_helper", "ActionView::Helpers::PrototypeHelper", "observe_form", "options(=>url:hash|function|frequency|update|with|on)")); mtds = new String[] { "link_to_remote(", "remote_function(", "submit_to_remote(", "form_remote_tag(" }; for (String mtd : mtds) { methods.add(new MethodDef("prototype_helper", "ActionView::Helpers::PrototypeHelper", mtd, "options(=>url:hash|update)")); } methods.add(new MethodDef("form_tag_helper", "ActionView::Helpers::FormTagHelper", "form_tag(","options(=>anchor|only_path:bool|controller:controller|action:action|trailing_slash:bool|host|protocol)")); methods.add(new MethodDef("form_tag_helper", "ActionView::Helpers::FormTagHelper", "select_tag(", "options(=>multiple:bool)")); methods.add(new MethodDef("form_tag_helper", "ActionView::Helpers::FormTagHelper", "text_area_tag(", "options(=>size)")); mtds = new String[] { "text_field_tag(", "password_field_tag(", "hidden_field_tag(" }; for (String mtd : mtds) { methods.add(new MethodDef("form_tag_helper", "ActionView::Helpers::FormTagHelper", mtd, "options(=>disabled:bool|size|maxlength)")); } methods.add(new MethodDef("number_helper", "ActionView::Helpers::NumberHelper", "number_to_phone(", "options(=>area_code:bool|delimiter|extension|country_code)")); methods.add(new MethodDef("number_helper", "ActionView::Helpers::NumberHelper", "number_to_currency(", "options(=>precision|unit|separator|delimiter)")); methods.add(new MethodDef("number_helper", "ActionView::Helpers::NumberHelper", "number_to_percentage(","options(=>precision|separator)")); // This was wrong in Rails 1 as well - these aren't option //methods.add(new MethodDef("number_helper", "ActionView::Helpers::NumberHelper", // "number_with_delimiter(", // "options(=>delimiter|separator)")); methods.add(new MethodDef("date_helper", "ActionView::Helpers::DateHelper", "date_select(","options(=>discard_year:bool|discard_month:bool|discard_day:bool|order|disabled:bool)")); methods.add(new MethodDef("date_helper", "ActionView::Helpers::DateHelper", "time_select(", "options(=>include_seconds:bool)")); // XXX Not sure about the rest here methods.add(new MethodDef("asset_tag_helper", "ActionView::Helpers::AssetTagHelper", "auto_discovery_link_tag(", "type(:rss|:atom),tag_options(=>rel|type|title),url_options(=>anchor|only_path:bool|controller:controller|action:action|trailing_slash:bool|host|protocol)")); methods.add(new MethodDef("asset_tag_helper", "ActionView::Helpers::AssetTagHelper", "image_tag(", "options(=>alt|size)")); methods.add(new MethodDef("url_helper", "ActionView::Helpers::UrlHelper", "url_for(", "options(=>escape:bool|anchor|only_path:bool|controller:controller|action:action|trailing_slash:bool|host|protocol)")); methods.add(new MethodDef("url_helper", "ActionView::Helpers::UrlHelper", "link_to(", "options(=>anchor|only_path:bool|controller:controller|action:action|trailing_slash:bool|host|protocol),html_options(=>confirm:string|popup:bool|method" + HTML_HASH_OPTIONS +")")); methods.add(new MethodDef("url_helper", "ActionView::Helpers::UrlHelper", "button_to(", "options(=>anchor|only_path:bool|controller:controller|action:action|trailing_slash:bool|host|protocol),html_options(=>confirm:string|popup:bool|method|disabled:bool" + HTML_HASH_OPTIONS +")")); methods.add(new MethodDef("url_helper", "ActionView::Helpers::UrlHelper", "mail_to(", "html_options(=>encode|replace_at|replace_dot|subject|body|cc|bcc" + HTML_HASH_OPTIONS +")")); // XXX missing some methods here - link_to_if etc. methods.add(new MethodDef("benchmark_helper", "ActionView::Helpers::BenchmarkHelper", "benchmark(", "level(:debug|:info|:warn|:error)")); methods.add(new MethodDef("pagination_helper", "ActionView::Helpers::PaginationHelper", "pagination_links(", "options(name|window_size|always_show_anchors:bool|link_to_current_page:bool|params),html_options(=>confirm:string|popup:bool|method" + HTML_HASH_OPTIONS +")")); methods.add(new MethodDef("active_record_helper", "ActionView::Helpers::ActiveRecordHelper", "form(", "options(action:action)")); methods.add(new MethodDef("active_record_helper", "ActionView::Helpers::ActiveRecordHelper", "error_messages_for(", // XXX note sig had "*params" instead of a hash - make // sure my name comparison is okay with that "params(=>header_tag|id|class|object_name)")); // TODO: TagHelper -- if you generate a tag I can do the // attributes conditionally based on the tag you're building // TODO FormOptionsHelper -- not sure what to do there // RSpec methods.add(new MethodDef("kernel", "Kernel", "describe(", // Behavior types seem to be controller,model,helper,view "args(=>behaviour_type|shared:bool)")); } enum RailsVersion { V1, V2 }; /** Method definition */ private class MethodDef { private String filename; private String classname; private String methodPrefix; private String arguments; private RailsVersion version; private MethodDef previousVersion; MethodDef(String filename, String classname, String methodPrefix, String arguments) { this.filename = filename; this.classname = classname; this.methodPrefix = methodPrefix; this.arguments = arguments; } MethodDef(String filename, String classname, String methodPrefix, String arguments, RailsVersion version) { this(filename, classname, methodPrefix, arguments); this.version = version; } public void setPreviousVersion(MethodDef previousVersion) { this.previousVersion = previousVersion; } public String toString() { return "MethodDef(" + filename + ","+classname+","+methodPrefix+","+arguments+")"; } } }