├── README.md ├── pom.xml ├── src └── main │ ├── java │ └── org │ │ └── yinwang │ │ └── rubysonar │ │ ├── Analyzer.java │ │ ├── AstCache.java │ │ ├── Binder.java │ │ ├── Binding.java │ │ ├── CallStack.java │ │ ├── Constants.java │ │ ├── Diagnostic.java │ │ ├── JSONDump.java │ │ ├── Options.java │ │ ├── Parser.java │ │ ├── Progress.java │ │ ├── State.java │ │ ├── Stats.java │ │ ├── Test.java │ │ ├── TypeStack.java │ │ ├── _.java │ │ ├── ast │ │ ├── Array.java │ │ ├── Assign.java │ │ ├── Attribute.java │ │ ├── BinOp.java │ │ ├── Block.java │ │ ├── Call.java │ │ ├── Class.java │ │ ├── Control.java │ │ ├── Dict.java │ │ ├── Dummy.java │ │ ├── For.java │ │ ├── Function.java │ │ ├── Handler.java │ │ ├── If.java │ │ ├── Index.java │ │ ├── Keyword.java │ │ ├── Name.java │ │ ├── NameType.java │ │ ├── Node.java │ │ ├── Op.java │ │ ├── Raise.java │ │ ├── RbFloat.java │ │ ├── RbInt.java │ │ ├── RbModule.java │ │ ├── Regexp.java │ │ ├── Return.java │ │ ├── Slice.java │ │ ├── Starred.java │ │ ├── Str.java │ │ ├── StrEmbed.java │ │ ├── Subscript.java │ │ ├── Symbol.java │ │ ├── Try.java │ │ ├── UnaryOp.java │ │ ├── Undef.java │ │ ├── Url.java │ │ ├── Void.java │ │ ├── While.java │ │ └── Yield.java │ │ ├── demos │ │ ├── Demo.java │ │ ├── Linker.java │ │ ├── Style.java │ │ └── StyleApplier.java │ │ └── types │ │ ├── BoolType.java │ │ ├── ClassType.java │ │ ├── DictType.java │ │ ├── FloatType.java │ │ ├── FunType.java │ │ ├── InstanceType.java │ │ ├── IntType.java │ │ ├── ListType.java │ │ ├── ModuleType.java │ │ ├── StrType.java │ │ ├── SymbolType.java │ │ ├── TupleType.java │ │ ├── Type.java │ │ └── UnionType.java │ └── resources │ └── org │ └── yinwang │ └── rubysonar │ ├── css │ └── demo.css │ ├── javascript │ ├── highlight-debug.js │ └── highlight.js │ ├── models │ └── test.rb │ └── ruby │ └── dump_ruby.rb └── tests ├── block.test ├── refs.json └── test1.rb ├── call.test ├── blockarg.rb ├── refs.json └── star.rb ├── class-instance ├── both-class-instance.test │ ├── both-class-instance.rb │ └── refs.json ├── class-methods.test │ ├── classmethods.rb │ └── refs.json ├── class-only.test │ ├── class-only.rb │ └── refs.json └── instance-only.test │ ├── instance-only.rb │ └── refs.json ├── constants.test ├── refs.json └── test1.rb ├── do.test ├── refs.json └── test1.rb ├── exceptions.test ├── begin.rb ├── refs.json └── single-rescue.rb ├── fields.test ├── refs.json ├── test1.rb └── test2.rb ├── include.test ├── include.rb └── refs.json ├── inheritance.test ├── refs.json └── test1.rb ├── names.test ├── names.rb └── refs.json ├── operators.test ├── operator.rb └── refs.json ├── rails-module-hack.test ├── refs.json ├── test1.rb └── test2.rb ├── reference-basic.test ├── basic.rb └── refs.json ├── reopen.test ├── refs.json └── test1.rb ├── require-diff-level.test ├── chipmunks │ └── tommy.rb ├── italy.rb └── refs.json ├── require-multi.test ├── italy.rb ├── pepperoni.rb ├── refs.json ├── sausage.rb └── tommy.rb ├── require-same-level.test ├── italy.rb ├── refs.json └── tommy.rb ├── return.test ├── refs.json └── test1.rb ├── string.test ├── embed.rb └── refs.json └── unicode.test ├── refs.json ├── test1.rb └── test2.rb /README.md: -------------------------------------------------------------------------------- 1 | ### RubySonar - an advanced semantic indexer for Ruby 2 | 3 | RubySonar is a semantic indexer for Ruby, which does 4 | interprocedural analysis to infer types. It is one of the underlying 5 | technologies that powers the code search site Sourcegraph. 7 | 8 | RubySonar is modeled after PySonar2, which does a similar 10 | analysis for Python and has been in use by Sourcegraph and Google. To understand 11 | its technical properties, please refer to my blog posts: 12 | 13 | - http://yinwang0.wordpress.com/2010/09/12/pysonar 14 | 15 | 16 | #### Demo 17 | 18 | 19 | 20 | 21 | 22 | #### How to build 23 | 24 | mvn package 25 | 26 | 27 | 28 | #### System Requirements 29 | 30 | * irb 31 | 32 | RubySonar uses the `irb` interpreter to parse Ruby code, so please make sure you 33 | have it installed and pointed to by the `PATH` environment variable. 34 | 35 | 36 | 37 | #### How to use 38 | 39 | RubySonar is mainly designed as a library for IDEs and other developer tools, so 40 | its interface may not be as appealing as an end-user tool, but for your 41 | understanding of the library's capabilities, a reasonably nice demo program has 42 | been built. 43 | 44 | You can build a simple "code-browser" of your ruby code with the following 45 | command line: 46 | 47 | java -jar target/rubysonar-0.1-SNAPSHOT.jar /path/to/project ./html 48 | 49 | This will take a few minutes. You should find some interactive HTML files inside 50 | the _html_ directory after this process. 51 | 52 | 53 | 54 | #### License (BSD Style) 55 | 56 | RubySonar - an advanced semantic indexer for Ruby 57 | 58 | Copyright (c) 2013-2019 Yin Wang 59 | 60 | Redistribution and use in source and binary forms, with or without modification, 61 | are permitted provided that the following conditions are met: 62 | 63 | 1. Redistributions of source code must retain the above copyright notice, this 64 | list of conditions and the following disclaimer. 65 | 66 | 1. Redistributions in binary form must reproduce the above copyright notice, 67 | this list of conditions and the following disclaimer in the documentation 68 | and/or other materials provided with the distribution. 69 | 70 | 1. The name of the author may not be used to endorse or promote products derived 71 | from this software without specific prior written permission. 72 | 73 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED 74 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 75 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 76 | SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 77 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 78 | OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 79 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 80 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 81 | IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 82 | OF SUCH DAMAGE. 83 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.yinwang 8 | rubysonar 9 | 0.1-SNAPSHOT 10 | jar 11 | rubysonar 12 | 13 | 14 | UTF-8 15 | UTF-8 16 | 3.1 17 | 2.1 18 | 2.2.4 19 | 2.10.0 20 | 21 | 22 | 23 | 24 | 25 | com.google.code.gson 26 | gson 27 | 2.2.4 28 | 29 | 30 | 31 | com.intellij 32 | annotations 33 | 5.1 34 | 35 | 36 | 37 | 38 | com.fasterxml.jackson.core 39 | jackson-core 40 | ${jackson-version} 41 | 42 | 43 | 45 | 46 | com.fasterxml.jackson.core 47 | jackson-annotations 48 | ${jackson-version} 49 | 50 | 51 | 52 | 53 | com.fasterxml.jackson.core 54 | jackson-databind 55 | ${jackson-version} 56 | 57 | 58 | 59 | commons-io 60 | commons-io 61 | 2.4 62 | 63 | 64 | 65 | org.apache.commons 66 | commons-lang3 67 | 3.0 68 | 69 | 70 | 71 | 72 | 73 | ${project.artifactId}-${project.version} 74 | 75 | 76 | maven-compiler-plugin 77 | ${maven-compiler-plugin.version} 78 | 79 | 1.7 80 | 1.7 81 | 82 | 83 | 84 | org.apache.maven.plugins 85 | maven-shade-plugin 86 | ${maven-shade-plugin.version} 87 | 88 | 89 | 90 | 91 | *:* 92 | 93 | META-INF/*.SF 94 | META-INF/*.DFA 95 | META-INF/*.RSA 96 | 97 | 98 | 99 | 100 | 101 | 102 | package 103 | 104 | shade 105 | 106 | 107 | 108 | 110 | 112 | org.yinwang.rubysonar.demos.Demo 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/AstCache.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.yinwang.rubysonar.ast.RbModule; 6 | import org.yinwang.rubysonar.ast.Node; 7 | 8 | import java.io.*; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.logging.Level; 12 | import java.util.logging.Logger; 13 | 14 | 15 | /** 16 | * Provides a factory for ruby source ASTs. Maintains configurable on-disk and 17 | * in-memory caches to avoid re-parsing files during analysis. 18 | */ 19 | public class AstCache { 20 | 21 | private static final Logger LOG = Logger.getLogger(AstCache.class.getCanonicalName()); 22 | 23 | private static AstCache INSTANCE; 24 | 25 | @NotNull 26 | private Map cache = new HashMap<>(); 27 | @NotNull 28 | private static Parser parser; 29 | 30 | 31 | private AstCache() { 32 | } 33 | 34 | 35 | public static AstCache get() { 36 | if (INSTANCE == null) { 37 | INSTANCE = new AstCache(); 38 | } 39 | parser = new Parser(); 40 | return INSTANCE; 41 | } 42 | 43 | 44 | /** 45 | * Clears the memory cache. 46 | */ 47 | public void clear() { 48 | cache.clear(); 49 | } 50 | 51 | 52 | /** 53 | * Removes all serialized ASTs from the on-disk cache. 54 | * 55 | * @return {@code true} if all cached AST files were removed 56 | */ 57 | public boolean clearDiskCache() { 58 | try { 59 | _.deleteDirectory(new File(Analyzer.self.cacheDir)); 60 | return true; 61 | } catch (Exception x) { 62 | LOG.log(Level.SEVERE, "Failed to clear disk cache: " + x); 63 | return false; 64 | } 65 | } 66 | 67 | 68 | public void close() { 69 | parser.close(); 70 | // clearDiskCache(); 71 | } 72 | 73 | 74 | /** 75 | * Returns the syntax tree for {@code path}. May find and/or create a 76 | * cached copy in the mem cache or the disk cache. 77 | * 78 | * @param path absolute path to a source file 79 | * @return the AST, or {@code null} if the parse failed for any reason 80 | */ 81 | @Nullable 82 | public Node getAST(@NotNull String path) { 83 | // Cache stores null value if the parse failed. 84 | if (cache.containsKey(path)) { 85 | return cache.get(path); 86 | } 87 | 88 | // Might be cached on disk but not in memory. 89 | Node node = getSerializedModule(path); 90 | if (node != null) { 91 | LOG.log(Level.FINE, "reusing " + path); 92 | cache.put(path, node); 93 | return node; 94 | } 95 | 96 | node = null; 97 | try { 98 | LOG.log(Level.FINE, "parsing " + path); 99 | node = parser.parseFile(path); 100 | } finally { 101 | cache.put(path, node); // may be null 102 | } 103 | 104 | if (node != null) { 105 | serialize(node); 106 | } 107 | 108 | return node; 109 | } 110 | 111 | 112 | /** 113 | * Each source file's AST is saved in an object file named for the MD5 114 | * checksum of the source file. All that is needed is the MD5, but the 115 | * file's base name is included for ease of debugging. 116 | */ 117 | @NotNull 118 | public String getCachePath(@NotNull String sourcePath) { 119 | return getCachePath(_.getSHA(sourcePath), sourcePath); 120 | } 121 | 122 | 123 | @NotNull 124 | public String getCachePath(String md5, String name) { 125 | return _.makePathString(Analyzer.self.cacheDir, name + md5 + ".ast"); 126 | } 127 | 128 | 129 | // package-private for testing 130 | void serialize(@NotNull Node ast) { 131 | String path = getCachePath(_.getSHA(ast.file), new File(ast.file).getName()); 132 | ObjectOutputStream oos = null; 133 | FileOutputStream fos = null; 134 | try { 135 | fos = new FileOutputStream(path); 136 | oos = new ObjectOutputStream(fos); 137 | oos.writeObject(ast); 138 | } catch (Exception e) { 139 | _.msg("Failed to serialize: " + path); 140 | } finally { 141 | try { 142 | if (oos != null) { 143 | oos.close(); 144 | } else if (fos != null) { 145 | fos.close(); 146 | } 147 | } catch (Exception e) { 148 | } 149 | } 150 | } 151 | 152 | 153 | // package-private for testing 154 | @Nullable 155 | RbModule getSerializedModule(String sourcePath) { 156 | if (!new File(sourcePath).canRead()) { 157 | return null; 158 | } 159 | File cached = new File(getCachePath(sourcePath)); 160 | if (!cached.canRead()) { 161 | return null; 162 | } 163 | return deserialize(sourcePath); 164 | } 165 | 166 | 167 | // package-private for testing 168 | @Nullable 169 | RbModule deserialize(@NotNull String sourcePath) { 170 | String cachePath = getCachePath(sourcePath); 171 | FileInputStream fis = null; 172 | ObjectInputStream ois = null; 173 | try { 174 | fis = new FileInputStream(cachePath); 175 | ois = new ObjectInputStream(fis); 176 | return (RbModule) ois.readObject(); 177 | } catch (Exception e) { 178 | return null; 179 | } finally { 180 | try { 181 | if (ois != null) { 182 | ois.close(); 183 | } else if (fis != null) { 184 | fis.close(); 185 | } 186 | } catch (Exception e) { 187 | 188 | } 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/Binder.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.ast.*; 5 | import org.yinwang.rubysonar.types.*; 6 | 7 | import java.util.List; 8 | 9 | 10 | /** 11 | * Handles binding names to scopes, including destructuring assignment. 12 | */ 13 | public class Binder { 14 | 15 | public static void bind(@NotNull State s, Node target, @NotNull Type rvalue, Binding.Kind kind) { 16 | if (target instanceof Name) { 17 | bind(s, (Name) target, rvalue, kind); 18 | } else if (target instanceof Array) { 19 | bind(s, ((Array) target).elts, rvalue, kind); 20 | } else if (target instanceof Attribute) { 21 | ((Attribute) target).setAttr(s, rvalue); 22 | } else if (target instanceof Subscript) { 23 | Subscript sub = (Subscript) target; 24 | Type valueType = Node.transformExpr(sub.value, s); 25 | if (sub.slice != null) { 26 | Node.transformExpr(sub.slice, s); 27 | } 28 | if (valueType instanceof ListType) { 29 | ListType t = (ListType) valueType; 30 | t.setElementType(UnionType.union(t.eltType, rvalue)); 31 | } 32 | } else if (target != null) { 33 | Analyzer.self.putProblem(target, "invalid location for assignment"); 34 | } 35 | } 36 | 37 | 38 | /** 39 | * Without specifying a kind, bind determines the kind according to the type 40 | * of the scope. 41 | */ 42 | public static void bind(@NotNull State s, Node target, @NotNull Type rvalue) { 43 | Binding.Kind kind; 44 | if (s.getStateType() == State.StateType.FUNCTION) { 45 | kind = Binding.Kind.VARIABLE; 46 | } else if (s.stateType == State.StateType.CLASS || 47 | s.stateType == State.StateType.INSTANCE) { 48 | kind = Binding.Kind.ATTRIBUTE; 49 | } else { 50 | kind = Binding.Kind.SCOPE; 51 | } 52 | bind(s, target, rvalue, kind); 53 | } 54 | 55 | 56 | public static void bind(@NotNull State s, @NotNull List xs, @NotNull Type rvalue, Binding.Kind kind) { 57 | if (rvalue instanceof TupleType) { 58 | List vs = ((TupleType) rvalue).eltTypes; 59 | if (xs.size() != vs.size()) { 60 | reportUnpackMismatch(xs, vs.size()); 61 | } else { 62 | for (int i = 0; i < xs.size(); i++) { 63 | bind(s, xs.get(i), vs.get(i), kind); 64 | } 65 | } 66 | } else { 67 | if (rvalue instanceof ListType) { 68 | bind(s, xs, ((ListType) rvalue).toTupleType(xs.size()), kind); 69 | } else if (rvalue instanceof DictType) { 70 | bind(s, xs, ((DictType) rvalue).toTupleType(xs.size()), kind); 71 | } else if (rvalue.isUnknownType()) { 72 | for (Node x : xs) { 73 | bind(s, x, Type.UNKNOWN, kind); 74 | } 75 | } else if (xs.size() > 0) { 76 | Analyzer.self.putProblem(xs.get(0).file, 77 | xs.get(0).start, 78 | xs.get(xs.size() - 1).end, 79 | "unpacking non-iterable: " + rvalue); 80 | } 81 | } 82 | } 83 | 84 | 85 | public static void bind(@NotNull State s, @NotNull Name name, @NotNull Type rvalue, Binding.Kind kind) { 86 | if (s.isGlobalName(name.id) || name.isGlobalVar()) { 87 | Binding b = new Binding(name, rvalue, kind); 88 | s.getGlobalTable().update(name.id, b); 89 | Analyzer.self.putRef(name, b); 90 | } else { 91 | s.insert(name.id, name, rvalue, kind); 92 | } 93 | } 94 | 95 | 96 | // iterator 97 | public static void bindIter(@NotNull State s, Node target, @NotNull Node iter, Binding.Kind kind) { 98 | Type iterType = Node.transformExpr(iter, s); 99 | 100 | if (iterType instanceof ListType) { 101 | bind(s, target, ((ListType) iterType).eltType, kind); 102 | } else if (iterType instanceof TupleType) { 103 | bind(s, target, ((TupleType) iterType).toListType().eltType, kind); 104 | } else { 105 | List ents = iterType.table.lookupAttr("__iter__"); 106 | if (ents != null) { 107 | for (Binding ent : ents) { 108 | if (ent == null || !(ent.type instanceof FunType)) { 109 | if (!iterType.isUnknownType()) { 110 | Analyzer.self.putProblem(iter, "not an iterable type: " + iterType); 111 | } 112 | bind(s, target, Type.UNKNOWN, kind); 113 | } else { 114 | bind(s, target, ((FunType) ent.type).getReturnType(), kind); 115 | } 116 | } 117 | } else { 118 | bind(s, target, Type.UNKNOWN, kind); 119 | } 120 | } 121 | } 122 | 123 | 124 | private static void reportUnpackMismatch(@NotNull List xs, int vsize) { 125 | int xsize = xs.size(); 126 | int beg = xs.get(0).start; 127 | int end = xs.get(xs.size() - 1).end; 128 | int diff = xsize - vsize; 129 | String msg; 130 | if (diff > 0) { 131 | msg = "ValueError: need more than " + vsize + " values to unpack"; 132 | } else { 133 | msg = "ValueError: too many values to unpack"; 134 | } 135 | Analyzer.self.putProblem(xs.get(0).file, beg, end, msg); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/Binding.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.yinwang.rubysonar.ast.Class; 6 | import org.yinwang.rubysonar.ast.RbModule; 7 | import org.yinwang.rubysonar.ast.*; 8 | import org.yinwang.rubysonar.types.ModuleType; 9 | import org.yinwang.rubysonar.types.Type; 10 | 11 | import java.util.LinkedHashSet; 12 | import java.util.Set; 13 | 14 | 15 | public class Binding implements Comparable { 16 | 17 | public enum Kind { 18 | MODULE, // file 19 | CLASS, // class definition 20 | METHOD, // instance method 21 | CLASS_METHOD, // class method 22 | ATTRIBUTE, // attr accessed with "." on some other object 23 | PARAMETER, // function param 24 | SCOPE, // top-level variable ("scope" means we assume it can have attrs) 25 | VARIABLE, // local variable 26 | CONSTANT, 27 | } 28 | 29 | 30 | @NotNull 31 | public Node node; 32 | @NotNull 33 | public String qname; // qualified name 34 | public Type type; // inferred type 35 | public Kind kind; // name usage context 36 | 37 | public Set refs; 38 | 39 | public int start = -1; 40 | public int end = -1; 41 | public int bodyStart = -1; 42 | public int bodyEnd = -1; 43 | 44 | @Nullable 45 | public String file; 46 | 47 | 48 | public Binding(@NotNull Node node, @NotNull Type type, @NotNull Kind kind) { 49 | this.qname = type.table.path; 50 | this.type = type; 51 | this.kind = kind; 52 | this.node = node; 53 | refs = new LinkedHashSet<>(1); 54 | 55 | if (node instanceof Url) { 56 | String url = ((Url) node).getURL(); 57 | if (url.startsWith("file://")) { 58 | file = url.substring("file://".length()); 59 | } else { 60 | file = url; 61 | } 62 | } else { 63 | file = node.file; 64 | } 65 | 66 | initLocationInfo(node); 67 | Analyzer.self.registerBinding(this); 68 | } 69 | 70 | 71 | private void initLocationInfo(Node node) { 72 | start = node.start; 73 | end = node.end; 74 | 75 | // find the node which node is the name of 76 | Node bodyNode = node.parent; 77 | while (!(bodyNode == null || 78 | bodyNode instanceof Function || 79 | bodyNode instanceof Class || 80 | bodyNode instanceof RbModule)) 81 | { 82 | bodyNode = bodyNode.parent; 83 | } 84 | 85 | if ((bodyNode instanceof Function && ((Function) bodyNode).name == node) || 86 | (bodyNode instanceof Class && ((Class) bodyNode).name == node) || 87 | (bodyNode instanceof RbModule && ((RbModule) bodyNode).name == node)) 88 | { 89 | bodyStart = bodyNode.start; 90 | bodyEnd = bodyNode.end; 91 | } else { 92 | bodyStart = node.start; 93 | bodyEnd = node.end; 94 | } 95 | } 96 | 97 | 98 | public Str findDocString() { 99 | Node fullNode = node; 100 | if (kind == Kind.CLASS) { 101 | while (fullNode != null && !(fullNode instanceof org.yinwang.rubysonar.ast.Class)) { 102 | fullNode = fullNode.parent; 103 | } 104 | } else if (kind == Kind.METHOD || kind == Kind.CLASS_METHOD) { 105 | while (fullNode != null && !(fullNode instanceof Function)) { 106 | fullNode = fullNode.parent; 107 | } 108 | } else if (kind == Kind.MODULE) { 109 | while (fullNode != null && !(fullNode instanceof RbModule)) { 110 | fullNode = fullNode.parent; 111 | } 112 | } 113 | 114 | 115 | if (fullNode instanceof org.yinwang.rubysonar.ast.Class) { 116 | return ((org.yinwang.rubysonar.ast.Class) fullNode).docstring; 117 | } else if (fullNode instanceof Function) { 118 | return ((Function) fullNode).docstring; 119 | } else if (fullNode instanceof RbModule) { 120 | return ((RbModule) fullNode).docstring; 121 | } else { 122 | return null; 123 | } 124 | } 125 | 126 | 127 | public void setQname(@NotNull String qname) { 128 | this.qname = qname; 129 | } 130 | 131 | 132 | public void addRef(Node ref) { 133 | refs.add(ref); 134 | } 135 | 136 | 137 | @NotNull 138 | public String getFirstFile() { 139 | Type bt = type; 140 | if (bt instanceof ModuleType) { 141 | 142 | String file = ((ModuleType) bt).file; 143 | return file != null ? file : ""; 144 | } 145 | 146 | String file = this.file; 147 | if (file != null) { 148 | return file; 149 | } 150 | 151 | return ""; 152 | } 153 | 154 | 155 | /** 156 | * Bindings can be sorted by their location for outlining purposes. 157 | */ 158 | public int compareTo(@NotNull Object o) { 159 | return start - ((Binding) o).start; 160 | } 161 | 162 | 163 | @NotNull 164 | @Override 165 | public String toString() { 166 | StringBuilder sb = new StringBuilder(); 167 | sb.append("(binding:"); 168 | sb.append(":kind=").append(kind); 169 | sb.append(":node=").append(node); 170 | sb.append(":type=").append(type); 171 | sb.append(":qname=").append(qname); 172 | sb.append(":refs="); 173 | if (refs.size() > 10) { 174 | sb.append("["); 175 | sb.append(refs.iterator().next()); 176 | sb.append(", ...("); 177 | sb.append(refs.size() - 1); 178 | sb.append(" more)]"); 179 | } else { 180 | sb.append(refs); 181 | } 182 | sb.append(">"); 183 | return sb.toString(); 184 | } 185 | 186 | 187 | @Override 188 | public boolean equals(Object obj) { 189 | if (!(obj instanceof Binding)) { 190 | return false; 191 | } else { 192 | Binding b = (Binding) obj; 193 | return (start == b.start && 194 | end == b.end && 195 | _.same(file, b.file)); 196 | } 197 | } 198 | 199 | 200 | @Override 201 | public int hashCode() { 202 | return ("" + file + start).hashCode(); 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/CallStack.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.ast.Node; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | 10 | 11 | public class CallStack { 12 | 13 | @NotNull 14 | private Set stack = new HashSet<>(); 15 | 16 | 17 | public void push(Node call, Type type) { 18 | stack.add(call); 19 | } 20 | 21 | 22 | public void pop(Node call, Type type) { 23 | stack.remove(call); 24 | } 25 | 26 | 27 | public boolean contains(Node call, Type type) { 28 | return stack.contains(call); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/Constants.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar; 2 | 3 | public class Constants { 4 | public static final String SELFNAME = "self"; 5 | public static final String INSTNAME = "#this"; 6 | public static final String IDSEP = "^"; 7 | public static final String IDSEP_REGEX = "\\^"; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/Diagnostic.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | 6 | public class Diagnostic { 7 | public enum Category { 8 | INFO, WARNING, ERROR 9 | } 10 | 11 | 12 | public String file; 13 | public Category category; 14 | public int start; 15 | public int end; 16 | public String msg; 17 | 18 | 19 | public Diagnostic(String file, Category category, int start, int end, String msg) { 20 | this.category = category; 21 | this.file = file; 22 | this.start = start; 23 | this.end = end; 24 | this.msg = msg; 25 | } 26 | 27 | 28 | @NotNull 29 | @Override 30 | public String toString() { 31 | return ""; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/Options.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar; 2 | 3 | import java.util.ArrayList; 4 | import java.util.LinkedHashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public class Options { 9 | 10 | private Map optionsMap = new LinkedHashMap<>(); 11 | 12 | 13 | private List args = new ArrayList<>(); 14 | 15 | 16 | public Options(String[] args) { 17 | for (int i = 0; i < args.length; i++) { 18 | String key = args[i]; 19 | if (key.startsWith("--")) { 20 | if (i + 1 >= args.length) { 21 | _.die("option needs a value: " + key); 22 | } else { 23 | key = key.substring(2); 24 | String value = args[i + 1]; 25 | if (!value.startsWith("-")) { 26 | optionsMap.put(key, value); 27 | i++; 28 | } 29 | } 30 | } else if (key.startsWith("-")) { 31 | key = key.substring(1); 32 | optionsMap.put(key, true); 33 | } else { 34 | this.args.add(key); 35 | } 36 | } 37 | } 38 | 39 | 40 | public Object get(String key) { 41 | return optionsMap.get(key); 42 | } 43 | 44 | 45 | public boolean hasOption(String key) { 46 | Object v = optionsMap.get(key); 47 | if (v instanceof Boolean) { 48 | return (boolean) v; 49 | } else { 50 | return false; 51 | } 52 | } 53 | 54 | 55 | public void put(String key, Object value) { 56 | optionsMap.put(key, value); 57 | } 58 | 59 | 60 | public List getArgs() { 61 | return args; 62 | } 63 | 64 | 65 | public Map getOptionsMap() { 66 | return optionsMap; 67 | } 68 | 69 | 70 | public static void main(String[] args) { 71 | Options options = new Options(args); 72 | for (String key : options.optionsMap.keySet()) { 73 | System.out.println(key + " = " + options.get(key)); 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/Progress.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar; 2 | 3 | public class Progress { 4 | 5 | private static final int MAX_SPEED_DIGITS = 5; 6 | 7 | long startTime; 8 | long lastTickTime; 9 | long lastCount; 10 | int lastRate; 11 | int lastAvgRate; 12 | long total; 13 | long count; 14 | long width; 15 | long segSize; 16 | 17 | 18 | public Progress(long total, long width) { 19 | this.startTime = System.currentTimeMillis(); 20 | this.lastTickTime = System.currentTimeMillis(); 21 | this.lastCount = 0; 22 | this.lastRate = 0; 23 | this.lastAvgRate = 0; 24 | this.total = total; 25 | this.width = width; 26 | this.segSize = total / width; 27 | if (segSize == 0) { 28 | segSize = 1; 29 | } 30 | } 31 | 32 | 33 | public void tick(int n) { 34 | count += n; 35 | if (count > total) { 36 | total = count; 37 | } 38 | 39 | long elapsed = System.currentTimeMillis() - lastTickTime; 40 | 41 | if (elapsed > 500 || count == total) { 42 | _.msg_("\r"); 43 | int dlen = (int) Math.ceil(Math.log10((double) total)); 44 | _.msg_(_.percent(count, total) + " (" + 45 | _.formatNumber(count, dlen) + 46 | " of " + _.formatNumber(total, dlen) + ")"); 47 | 48 | int rate; 49 | if (elapsed > 1) { 50 | rate = (int) ((count - lastCount) / (elapsed / 1000.0)); 51 | } else { 52 | rate = lastRate; 53 | } 54 | 55 | lastRate = rate; 56 | _.msg_(" SPEED: " + _.formatNumber(rate, MAX_SPEED_DIGITS) + "/s"); 57 | 58 | long totalElapsed = System.currentTimeMillis() - startTime; 59 | int avgRate; 60 | 61 | if (totalElapsed > 1) { 62 | avgRate = (int) (count / (totalElapsed / 1000.0)); 63 | } else { 64 | avgRate = lastAvgRate; 65 | } 66 | avgRate = avgRate == 0 ? 1 : avgRate; 67 | 68 | _.msg_(" AVG SPEED: " + _.formatNumber(avgRate, MAX_SPEED_DIGITS) + "/s"); 69 | 70 | long remain = total - count; 71 | long remainTime = remain / avgRate * 1000; 72 | _.msg_(" ETA: " + _.formatTime(remainTime)); 73 | 74 | 75 | _.msg_(" "); // overflow area 76 | 77 | lastTickTime = System.currentTimeMillis(); 78 | lastAvgRate = avgRate; 79 | lastCount = count; 80 | } 81 | } 82 | 83 | 84 | public void tick() { 85 | tick(1); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/Stats.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | 7 | public class Stats { 8 | Map contents = new HashMap<>(); 9 | 10 | 11 | public void putInt(String key, long value) { 12 | contents.put(key, value); 13 | } 14 | 15 | 16 | public void inc(String key, long x) { 17 | Long old = getInt(key); 18 | 19 | if (old == null) { 20 | contents.put(key, 1); 21 | } else { 22 | contents.put(key, old + x); 23 | } 24 | } 25 | 26 | 27 | public void inc(String key) { 28 | inc(key, 1); 29 | } 30 | 31 | 32 | public Long getInt(String key) { 33 | Long ret = (Long) contents.get(key); 34 | if (ret == null) { 35 | return 0L; 36 | } else { 37 | return ret; 38 | } 39 | } 40 | 41 | 42 | public String print() { 43 | StringBuilder sb = new StringBuilder(); 44 | 45 | for (Map.Entry e : contents.entrySet()) { 46 | sb.append("\n- " + e.getKey() + ": " + e.getValue()); 47 | } 48 | 49 | return sb.toString(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/TypeStack.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | 9 | public class TypeStack { 10 | 11 | class Pair { 12 | public Object first; 13 | public Object second; 14 | 15 | 16 | public Pair(Object first, Object second) { 17 | this.first = first; 18 | this.second = second; 19 | } 20 | } 21 | 22 | 23 | @NotNull 24 | private List stack = new ArrayList<>(); 25 | 26 | 27 | public void push(Object first, Object second) { 28 | stack.add(new Pair(first, second)); 29 | } 30 | 31 | 32 | public void pop(Object first, Object second) { 33 | stack.remove(stack.size() - 1); 34 | } 35 | 36 | 37 | public boolean contains(Object first, Object second) { 38 | for (Pair p : stack) { 39 | if (p.first == first && p.second == second || 40 | p.first == second && p.second == first) 41 | { 42 | return true; 43 | } 44 | } 45 | return false; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Array.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.ListType; 6 | import org.yinwang.rubysonar.types.Type; 7 | 8 | import java.util.List; 9 | 10 | 11 | public class Array extends Node { 12 | 13 | @NotNull 14 | public List elts; 15 | 16 | 17 | public Array(@NotNull List elts, String file, int start, int end) { 18 | super(file, start, end); 19 | this.elts = elts; 20 | addChildren(elts); 21 | } 22 | 23 | 24 | @NotNull 25 | @Override 26 | public Type transform(State s) { 27 | if (elts.size() == 0) { 28 | return new ListType(); // list 29 | } 30 | 31 | ListType listType = new ListType(); 32 | for (Node elt : elts) { 33 | listType.add(transformExpr(elt, s)); 34 | if (elt instanceof Str) { 35 | listType.addValue(((Str) elt).value); 36 | } 37 | } 38 | 39 | return listType; 40 | } 41 | 42 | 43 | @NotNull 44 | @Override 45 | public String toString() { 46 | return "(array:" + elts + ")"; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Assign.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.*; 5 | import org.yinwang.rubysonar.types.ModuleType; 6 | import org.yinwang.rubysonar.types.Type; 7 | 8 | 9 | public class Assign extends Node { 10 | 11 | @NotNull 12 | public Node target; 13 | @NotNull 14 | public Node value; 15 | 16 | 17 | public Assign(@NotNull Node target, @NotNull Node value, String file, int start, int end) { 18 | super(file, start, end); 19 | this.target = target; 20 | this.value = value; 21 | addChildren(target); 22 | addChildren(value); 23 | } 24 | 25 | 26 | @NotNull 27 | @Override 28 | public Type transform(@NotNull State s) { 29 | Type valueType = transformExpr(value, s); 30 | if (target instanceof Name && ((Name) target).isInstanceVar()) { 31 | Type thisType = s.lookupType(Constants.INSTNAME); 32 | thisType = thisType != null ? thisType : s.lookupType(Constants.SELFNAME); 33 | if (thisType == null) { 34 | Analyzer.self.putProblem(this, "Instance variable assignment not within class"); 35 | } else if (thisType instanceof ModuleType) { 36 | thisType.table.insertTagged(((Name) target).id, "class", target, valueType, Binding.Kind.ATTRIBUTE); 37 | } else { 38 | thisType.table.insert(((Name) target).id, target, valueType, Binding.Kind.ATTRIBUTE); 39 | } 40 | } else if (s.stateType == State.StateType.CLASS && 41 | target instanceof Name && ((Name) target).id.toUpperCase().equals(((Name) target).id)) 42 | { 43 | // constant 44 | s.insertTagged(((Name) target).id, "class", target, valueType, Binding.Kind.CONSTANT); 45 | } else { 46 | Binder.bind(s, target, valueType); 47 | } 48 | return valueType; 49 | } 50 | 51 | 52 | @NotNull 53 | @Override 54 | public String toString() { 55 | return "(" + target + " = " + value + ")"; 56 | } 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Attribute.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.yinwang.rubysonar.Analyzer; 6 | import org.yinwang.rubysonar.Binding; 7 | import org.yinwang.rubysonar.State; 8 | import org.yinwang.rubysonar.types.*; 9 | 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | import static org.yinwang.rubysonar.Binding.Kind.ATTRIBUTE; 14 | 15 | 16 | public class Attribute extends Node { 17 | 18 | @Nullable 19 | public Node target; 20 | @NotNull 21 | public Name attr; 22 | 23 | 24 | public Attribute(@Nullable Node target, @NotNull Name attr, String file, int start, int end) { 25 | super(file, start, end); 26 | this.target = target; 27 | this.attr = attr; 28 | addChildren(target, attr); 29 | } 30 | 31 | 32 | @Nullable 33 | public String getAttributeName() { 34 | return attr.id; 35 | } 36 | 37 | 38 | @NotNull 39 | public Name getAttr() { 40 | return attr; 41 | } 42 | 43 | 44 | public void setAttr(State s, @NotNull Type v) { 45 | Type targetType = transformExpr(target, s); 46 | if (targetType instanceof UnionType) { 47 | Set types = ((UnionType) targetType).types; 48 | for (Type tp : types) { 49 | setAttrType(tp, v); 50 | } 51 | } else { 52 | setAttrType(targetType, v); 53 | } 54 | } 55 | 56 | 57 | private void setAttrType(@NotNull Type targetType, @NotNull Type v) { 58 | if (targetType.isUnknownType()) { 59 | Analyzer.self.putProblem(this, "Can't set attribute for UnknownType"); 60 | return; 61 | } 62 | // new attr, mark the type as "mutated" 63 | if (targetType.table.lookupAttr(attr.id) == null || 64 | !targetType.table.lookupAttrType(attr.id).equals(v)) 65 | { 66 | targetType.setMutated(true); 67 | } 68 | targetType.table.insert(attr.id, attr, v, ATTRIBUTE); 69 | } 70 | 71 | 72 | @NotNull 73 | @Override 74 | public Type transform(State s) { 75 | // the form of ::A in ruby 76 | if (target == null) { 77 | return transformExpr(attr, s); 78 | } 79 | 80 | Type targetType = transformExpr(target, s); 81 | if (targetType instanceof UnionType) { 82 | Set types = ((UnionType) targetType).types; 83 | Type retType = Type.UNKNOWN; 84 | for (Type tt : types) { 85 | retType = UnionType.union(retType, getAttrType(tt)); 86 | } 87 | return retType; 88 | } else { 89 | return getAttrType(targetType); 90 | } 91 | } 92 | 93 | 94 | private Type getAttrType(@NotNull Type targetType) { 95 | List bs; 96 | if (targetType instanceof ClassType || targetType instanceof ModuleType) { 97 | // look for class methods only 98 | bs = targetType.table.lookupAttrTagged(attr.id, "class"); 99 | if (bs == null) { 100 | bs = targetType.table.lookupAttr(attr.id); 101 | } 102 | } else { 103 | bs = targetType.table.lookupAttr(attr.id); 104 | } 105 | 106 | if (bs == null) { 107 | Analyzer.self.putProblem(attr, "attribute not found in type: " + targetType); 108 | return Type.UNKNOWN; 109 | } else { 110 | for (Binding b : bs) { 111 | Analyzer.self.putRef(attr, b); 112 | if (parent != null && (parent instanceof Call) && 113 | b.type instanceof FunType && targetType instanceof InstanceType) 114 | { // method call 115 | ((FunType) b.type).setSelfType(targetType); 116 | } 117 | } 118 | 119 | return State.makeUnion(bs); 120 | } 121 | } 122 | 123 | 124 | @NotNull 125 | @Override 126 | public String toString() { 127 | return "(" + target + "." + getAttributeName() + ")"; 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/BinOp.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | public class BinOp extends Node { 9 | 10 | @NotNull 11 | public Node left; 12 | @NotNull 13 | public Node right; 14 | @NotNull 15 | public Op op; 16 | 17 | 18 | public BinOp(@NotNull Op op, @NotNull Node left, @NotNull Node right, String file, int start, int end) { 19 | super(file, start, end); 20 | this.left = left; 21 | this.right = right; 22 | this.op = op; 23 | addChildren(left, right); 24 | } 25 | 26 | 27 | @NotNull 28 | @Override 29 | public Type transform(State s) { 30 | Type ltype = transformExpr(left, s); 31 | Type rtype = transformExpr(right, s); 32 | 33 | if (ltype != Type.UNKNOWN) { 34 | return ltype; 35 | } else if (rtype != Type.UNKNOWN) { 36 | return rtype; 37 | } else { 38 | return Type.UNKNOWN; 39 | } 40 | } 41 | 42 | 43 | @NotNull 44 | @Override 45 | public String toString() { 46 | return "(" + left + " " + op + " " + right + ")"; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Block.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.Analyzer; 5 | import org.yinwang.rubysonar.State; 6 | import org.yinwang.rubysonar.types.Type; 7 | import org.yinwang.rubysonar.types.UnionType; 8 | 9 | import java.util.List; 10 | 11 | 12 | public class Block extends Node { 13 | 14 | @NotNull 15 | public List seq; 16 | 17 | 18 | public Block(@NotNull List seq, String file, int start, int end) { 19 | super(file, start, end); 20 | this.seq = seq; 21 | addChildren(seq); 22 | } 23 | 24 | 25 | @NotNull 26 | @Override 27 | public Type transform(@NotNull State state) { 28 | 29 | boolean returned = false; 30 | Type retType = Type.UNKNOWN; 31 | boolean wasStatic = Analyzer.self.staticContext; 32 | 33 | for (Node n : seq) { 34 | Type t = transformExpr(n, state); 35 | if (n == seq.get(seq.size() - 1)) { 36 | // return last value 37 | retType = UnionType.remove(t, Type.CONT); 38 | } else if (!returned) { 39 | retType = UnionType.union(retType, t); 40 | if (!t.isUnknownType() && !UnionType.contains(t, Type.CONT)) { 41 | returned = true; 42 | retType = UnionType.remove(retType, Type.CONT); 43 | } 44 | } else if (state.getStateType() != State.StateType.GLOBAL && 45 | state.getStateType() != State.StateType.MODULE) 46 | { 47 | Analyzer.self.putProblem(n, "unreachable code"); 48 | } 49 | } 50 | 51 | Analyzer.self.setStaticContext(wasStatic); 52 | return retType; 53 | } 54 | 55 | 56 | public boolean isEmpty() { 57 | return seq.isEmpty(); 58 | } 59 | 60 | 61 | @NotNull 62 | @Override 63 | public String toString() { 64 | return "(block:" + seq + ")"; 65 | } 66 | 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Class.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.yinwang.rubysonar.*; 6 | import org.yinwang.rubysonar.types.ClassType; 7 | import org.yinwang.rubysonar.types.Type; 8 | 9 | 10 | public class Class extends Node { 11 | private static int classCounter = 0; 12 | 13 | @Nullable 14 | public Node locator; 15 | public Name name; 16 | public Node base; 17 | public Node body; 18 | public Str docstring; 19 | public boolean isStatic; 20 | 21 | 22 | public Class(@Nullable Node locator, Node base, Node body, Str docstring, boolean isStatic, String file, int start, 23 | int end) 24 | { 25 | super(file, start, end); 26 | 27 | // set name 28 | if (locator instanceof Attribute) { 29 | this.name = ((Attribute) locator).attr; 30 | } else if (locator instanceof Name) { 31 | this.name = (Name) locator; 32 | } else { 33 | this.name = new Name(genClassName(), file, start, start + 1); 34 | addChildren(this.name); 35 | } 36 | 37 | this.locator = locator; 38 | this.base = base; 39 | this.body = body; 40 | this.docstring = docstring; 41 | this.isStatic = isStatic; 42 | addChildren(this.locator, this.body, this.base, this.docstring); 43 | } 44 | 45 | 46 | @NotNull 47 | public static String genClassName() { 48 | classCounter = classCounter + 1; 49 | return "class%" + classCounter; 50 | } 51 | 52 | 53 | @NotNull 54 | @Override 55 | public Type transform(@NotNull State s) { 56 | if (locator != null) { 57 | Type reopened = transformExpr(locator, s); 58 | if (isStatic) { 59 | if (body != null) { 60 | boolean wasStatic = Analyzer.self.staticContext; 61 | Analyzer.self.setStaticContext(true); 62 | transformExpr(body, reopened.table); 63 | Analyzer.self.setStaticContext(wasStatic); 64 | } 65 | return Type.CONT; 66 | } 67 | } 68 | 69 | ClassType classType = new ClassType(name.id, s); 70 | classType.table.setParent(s); 71 | 72 | if (base != null) { 73 | Type baseType = transformExpr(base, s); 74 | if (baseType instanceof ClassType) { 75 | classType.addSuper(baseType); 76 | } else { 77 | Analyzer.self.putProblem(base, base + " is not a class"); 78 | } 79 | } 80 | 81 | // Bind ClassType to name here before resolving the body because the 82 | // methods need this type as self. 83 | Binder.bind(s, name, classType, Binding.Kind.CLASS); 84 | classType.table.insert(Constants.SELFNAME, name, classType, Binding.Kind.SCOPE); 85 | 86 | if (body != null) { 87 | transformExpr(body, classType.table); 88 | } 89 | return Type.CONT; 90 | } 91 | 92 | 93 | @NotNull 94 | @Override 95 | public String toString() { 96 | return "(class:" + name.id + ")"; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Control.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | public class Control extends Node { 9 | 10 | public String command; 11 | 12 | 13 | public Control(String command, String file, int start, int end) { 14 | super(file, start, end); 15 | this.command = command; 16 | } 17 | 18 | 19 | @NotNull 20 | @Override 21 | public Type transform(State s) { 22 | return Type.NIL; 23 | } 24 | 25 | 26 | @NotNull 27 | @Override 28 | public String toString() { 29 | return "(" + command + ")"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Dict.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.DictType; 6 | import org.yinwang.rubysonar.types.Type; 7 | 8 | import java.util.List; 9 | 10 | 11 | public class Dict extends Node { 12 | 13 | public List keys; 14 | public List values; 15 | 16 | 17 | public Dict(List keys, List values, String file, int start, int end) { 18 | super(file, start, end); 19 | this.keys = keys; 20 | this.values = values; 21 | addChildren(keys); 22 | addChildren(values); 23 | } 24 | 25 | 26 | @NotNull 27 | @Override 28 | public Type transform(State s) { 29 | Type keyType = resolveUnion(keys, s); 30 | Type valType = resolveUnion(values, s); 31 | return new DictType(keyType, valType); 32 | } 33 | 34 | 35 | @NotNull 36 | @Override 37 | public String toString() { 38 | return "(dict)"; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Dummy.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | /** 9 | * dummy node for locating purposes only 10 | * rarely used 11 | */ 12 | public class Dummy extends Node { 13 | 14 | public Dummy(String file, int start, int end) { 15 | super(file, start, end); 16 | } 17 | 18 | 19 | @NotNull 20 | @Override 21 | protected Type transform(State s) { 22 | return Type.UNKNOWN; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/For.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.Binder; 5 | import org.yinwang.rubysonar.Binding; 6 | import org.yinwang.rubysonar.State; 7 | import org.yinwang.rubysonar.types.Type; 8 | import org.yinwang.rubysonar.types.UnionType; 9 | 10 | 11 | public class For extends Node { 12 | 13 | public Node target; 14 | public Node iter; 15 | public Block body; 16 | public Block orelse; 17 | 18 | 19 | public For(Node target, Node iter, Block body, Block orelse, 20 | String file, int start, int end) 21 | { 22 | super(file, start, end); 23 | this.target = target; 24 | this.iter = iter; 25 | this.body = body; 26 | this.orelse = orelse; 27 | addChildren(target, iter, body, orelse); 28 | } 29 | 30 | 31 | @NotNull 32 | @Override 33 | public Type transform(@NotNull State s) { 34 | Binder.bindIter(s, target, iter, Binding.Kind.SCOPE); 35 | 36 | Type ret; 37 | if (body == null) { 38 | ret = Type.UNKNOWN; 39 | } else { 40 | ret = transformExpr(body, s); 41 | } 42 | if (orelse != null) { 43 | ret = UnionType.union(ret, transformExpr(orelse, s)); 44 | } 45 | return ret; 46 | } 47 | 48 | 49 | @NotNull 50 | @Override 51 | public String toString() { 52 | return "(for:" + target + ":" + iter + ":" + body + ":" + orelse + ")"; 53 | } 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Function.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.Analyzer; 5 | import org.yinwang.rubysonar.Binding; 6 | import org.yinwang.rubysonar.State; 7 | import org.yinwang.rubysonar._; 8 | import org.yinwang.rubysonar.types.ClassType; 9 | import org.yinwang.rubysonar.types.FunType; 10 | import org.yinwang.rubysonar.types.ModuleType; 11 | import org.yinwang.rubysonar.types.Type; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | 17 | public class Function extends Node { 18 | 19 | public Node locator; 20 | public Name name; 21 | public List args; 22 | public List defaults; 23 | public Name vararg; // *args 24 | public Name kwarg; // **kwarg 25 | public Name blockarg = null; // block arg of Ruby 26 | public List afterRest = null; // after rest arg of Ruby 27 | public Node body; 28 | public boolean called = false; 29 | public boolean isLamba = false; 30 | public Str docstring; 31 | 32 | 33 | public Function(Node locator, List args, Node body, List defaults, 34 | Name vararg, Name kwarg, List afterRest, Name blockarg, 35 | Str docstring, String file, int start, int end) 36 | { 37 | super(file, start, end); 38 | if (locator != null) { 39 | this.locator = locator; 40 | } else { 41 | isLamba = true; 42 | String fn = genLambdaName(); 43 | this.locator = new Name(fn, file, -1, -1); 44 | addChildren(this.locator); 45 | } 46 | 47 | if (this.locator instanceof Attribute) { 48 | this.name = ((Attribute) this.locator).attr; 49 | } else if (this.locator instanceof Name) { 50 | this.name = (Name) this.locator; 51 | } 52 | 53 | this.args = args; 54 | this.body = body; 55 | this.defaults = defaults; 56 | this.vararg = vararg; 57 | this.kwarg = kwarg; 58 | this.afterRest = afterRest; 59 | this.blockarg = blockarg; 60 | this.docstring = docstring; 61 | addChildren(args); 62 | addChildren(defaults); 63 | addChildren(afterRest); 64 | addChildren(locator, body, vararg, kwarg, blockarg); 65 | } 66 | 67 | 68 | @NotNull 69 | @Override 70 | public Type transform(@NotNull State s) { 71 | Type locType = null; 72 | if (locator instanceof Attribute) { 73 | locType = transformExpr(((Attribute) locator).target, s); 74 | if (!locType.isUnknownType()) { 75 | s = locType.table; 76 | } 77 | } 78 | 79 | FunType fun = new FunType(this, s); 80 | fun.table.setParent(s); 81 | fun.setDefaultTypes(resolveList(defaults, s)); 82 | 83 | if (!isLamba) { 84 | Type outType = s.type; 85 | if (outType instanceof ClassType) { 86 | fun.setCls((ClassType) outType); 87 | } 88 | } 89 | 90 | if (locType instanceof ClassType || locType instanceof ModuleType || Analyzer.self.staticContext) { 91 | fun.setClassMethod(true); 92 | s.insertTagged(name.id, "class", name, fun, Binding.Kind.CLASS_METHOD); 93 | fun.table.setPath(s.extendPath(name.id, ".")); 94 | } else { 95 | s.insert(name.id, name, fun, Binding.Kind.METHOD); 96 | fun.table.setPath(s.extendPath(name.id, "#")); 97 | } 98 | Analyzer.self.addUncalled(fun); 99 | return Type.CONT; 100 | } 101 | 102 | 103 | private static int lambdaCounter = 0; 104 | 105 | 106 | @NotNull 107 | public static String genLambdaName() { 108 | lambdaCounter = lambdaCounter + 1; 109 | return "lambda%" + lambdaCounter; 110 | } 111 | 112 | 113 | public String getArgList() { 114 | 115 | List argList = new ArrayList<>(); 116 | if (args != null) { 117 | for (Node n : args) { 118 | argList.add(n.toDisplay()); 119 | } 120 | } 121 | 122 | if (vararg != null) { 123 | argList.add("*" + vararg.toDisplay()); 124 | } 125 | 126 | if (afterRest != null) { 127 | for (Node a : afterRest) { 128 | argList.add(a.toDisplay()); 129 | } 130 | } 131 | 132 | if (kwarg != null) { 133 | argList.add("**" + kwarg.toDisplay()); 134 | } 135 | 136 | if (blockarg != null) { 137 | argList.add("&" + blockarg.toDisplay()); 138 | } 139 | 140 | return _.joinWithSep(argList, ",", null, null); 141 | } 142 | 143 | 144 | @NotNull 145 | @Override 146 | public String toString() { 147 | return "(func:" + locator + ")"; 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Handler.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.Binder; 5 | import org.yinwang.rubysonar.State; 6 | import org.yinwang.rubysonar.types.Type; 7 | import org.yinwang.rubysonar.types.UnionType; 8 | 9 | import java.util.List; 10 | 11 | 12 | public class Handler extends Node { 13 | 14 | public List exceptions; 15 | public Node binder; 16 | public Node handler; 17 | public Node orelse; 18 | 19 | 20 | public Handler(List exceptions, Node binder, Node handler, Node orelse, String file, int start, int end) { 21 | super(file, start, end); 22 | this.exceptions = exceptions; 23 | this.binder = binder; 24 | this.handler = handler; 25 | this.orelse = orelse; 26 | addChildren(binder, handler, orelse); 27 | addChildren(exceptions); 28 | } 29 | 30 | 31 | @NotNull 32 | @Override 33 | public Type transform(@NotNull State s) { 34 | Type typeval = Type.UNKNOWN; 35 | if (exceptions != null) { 36 | typeval = resolveUnion(exceptions, s); 37 | } 38 | if (binder != null) { 39 | Binder.bind(s, binder, typeval); 40 | } 41 | 42 | Type ret = Type.UNKNOWN; 43 | 44 | if (handler != null) { 45 | ret = UnionType.union(ret, transformExpr(handler, s)); 46 | } 47 | 48 | if (orelse != null) { 49 | ret = UnionType.union(ret, transformExpr(orelse, s)); 50 | } 51 | 52 | return ret; 53 | } 54 | 55 | 56 | @NotNull 57 | @Override 58 | public String toString() { 59 | return "(handler:" + exceptions + ":" + binder + ")"; 60 | } 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/If.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | import org.yinwang.rubysonar.types.UnionType; 7 | 8 | 9 | public class If extends Node { 10 | 11 | @NotNull 12 | public Node test; 13 | public Node body; 14 | public Node orelse; 15 | 16 | 17 | public If(@NotNull Node test, Node body, Node orelse, String file, int start, int end) { 18 | super(file, start, end); 19 | this.test = test; 20 | this.body = body; 21 | this.orelse = orelse; 22 | addChildren(test, body, orelse); 23 | } 24 | 25 | 26 | @NotNull 27 | @Override 28 | public Type transform(@NotNull State s) { 29 | Type type1, type2; 30 | State s1 = s.copy(); 31 | State s2 = s.copy(); 32 | 33 | // ignore condition for now 34 | transformExpr(test, s); 35 | 36 | if (body != null) { 37 | type1 = transformExpr(body, s1); 38 | } else { 39 | type1 = Type.CONT; 40 | } 41 | 42 | if (orelse != null) { 43 | type2 = transformExpr(orelse, s2); 44 | } else { 45 | type2 = Type.CONT; 46 | } 47 | 48 | boolean cont1 = UnionType.contains(type1, Type.CONT); 49 | boolean cont2 = UnionType.contains(type2, Type.CONT); 50 | 51 | // decide which branch affects the downstream state 52 | if (cont1 && cont2) { 53 | s.overwrite(State.merge(s1, s2)); 54 | } else if (cont1) { 55 | s.overwrite(s1); 56 | } else if (cont2) { 57 | s.overwrite(s2); 58 | } 59 | 60 | return UnionType.union(type1, type2); 61 | } 62 | 63 | 64 | @NotNull 65 | @Override 66 | public String toString() { 67 | return "(if:" + test + ":" + body + ":" + orelse + ")"; 68 | } 69 | 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Index.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | public class Index extends Node { 9 | 10 | public Node value; 11 | 12 | 13 | public Index(Node n, String file, int start, int end) { 14 | super(file, start, end); 15 | this.value = n; 16 | addChildren(n); 17 | } 18 | 19 | 20 | @NotNull 21 | @Override 22 | public Type transform(State s) { 23 | return transformExpr(value, s); 24 | } 25 | 26 | 27 | @NotNull 28 | @Override 29 | public String toString() { 30 | return "(index:" + value + ")"; 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Keyword.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | /** 9 | * Represents a keyword argument (name=value) in a function call. 10 | */ 11 | public class Keyword extends Node { 12 | 13 | public String arg; 14 | @NotNull 15 | public Node value; 16 | 17 | 18 | public Keyword(String arg, @NotNull Node value, String file, int start, int end) { 19 | super(file, start, end); 20 | this.arg = arg; 21 | this.value = value; 22 | addChildren(value); 23 | } 24 | 25 | 26 | @NotNull 27 | @Override 28 | public Type transform(State s) { 29 | return transformExpr(value, s); 30 | } 31 | 32 | 33 | public String getArg() { 34 | return arg; 35 | } 36 | 37 | 38 | @NotNull 39 | public Node getValue() { 40 | return value; 41 | } 42 | 43 | 44 | @NotNull 45 | @Override 46 | public String toDisplay() { 47 | return arg; 48 | } 49 | 50 | 51 | @NotNull 52 | @Override 53 | public String toString() { 54 | return "(keyword:" + arg + ":" + value + ")"; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Name.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.Analyzer; 5 | import org.yinwang.rubysonar.Binding; 6 | import org.yinwang.rubysonar.Constants; 7 | import org.yinwang.rubysonar.State; 8 | import org.yinwang.rubysonar.types.Type; 9 | 10 | import java.util.List; 11 | 12 | 13 | public class Name extends Node { 14 | 15 | @NotNull 16 | public final String id; // identifier 17 | public NameType type; 18 | 19 | 20 | public Name(String id) { 21 | this(id, null, -1, -1); 22 | } 23 | 24 | 25 | public Name(@NotNull String id, String file, int start, int end) { 26 | super(file, start, end); 27 | this.id = id; 28 | this.name = id; 29 | this.type = NameType.LOCAL; 30 | } 31 | 32 | 33 | public Name(@NotNull String id, NameType type, String file, int start, int end) { 34 | super(file, start, end); 35 | this.id = id; 36 | this.name = id; 37 | this.type = type; 38 | } 39 | 40 | 41 | public static boolean isSyntheticName(String name) { 42 | return name.equals(Constants.SELFNAME) || name.equals(Constants.INSTNAME); 43 | } 44 | 45 | 46 | @NotNull 47 | @Override 48 | public Type transform(@NotNull State s) { 49 | List b; 50 | 51 | if (Analyzer.self.staticContext) { 52 | b = s.lookupTagged(id, "class"); 53 | if (b == null) { 54 | b = s.lookup(id); 55 | } 56 | } else { 57 | b = s.lookup(id); 58 | if (b == null) { 59 | b = s.lookupTagged(id, "class"); 60 | } 61 | } 62 | 63 | if (b == null) { 64 | Type thisType = s.lookupType(Constants.INSTNAME); 65 | thisType = thisType != null ? thisType : s.lookupType(Constants.SELFNAME); 66 | if (thisType != null) { 67 | b = thisType.table.lookup(id); 68 | if (b == null) { 69 | b = thisType.table.lookupTagged(id, "class"); 70 | } 71 | } 72 | } 73 | 74 | if (b != null) { 75 | Analyzer.self.putRef(this, b); 76 | Analyzer.self.resolved.add(this); 77 | Analyzer.self.unresolved.remove(this); 78 | return State.makeUnion(b); 79 | } else if (id.equals("true") || id.equals("false")) { 80 | return Type.BOOL; 81 | } else { 82 | Analyzer.self.putProblem(this, "unbound variable " + id); 83 | Analyzer.self.unresolved.add(this); 84 | return Type.UNKNOWN; 85 | } 86 | } 87 | 88 | 89 | /** 90 | * Returns {@code true} if this name node is the {@code attr} child 91 | * (i.e. the attribute being accessed) of an {@link Attribute} node. 92 | */ 93 | public boolean isAttribute() { 94 | return parent instanceof Attribute 95 | && ((Attribute) parent).getAttr() == this; 96 | } 97 | 98 | 99 | public boolean isInstanceVar() { 100 | return type == NameType.INSTANCE; 101 | } 102 | 103 | 104 | public boolean isGlobalVar() { 105 | return type == NameType.GLOBAL; 106 | } 107 | 108 | 109 | @NotNull 110 | @Override 111 | public String toDisplay() { 112 | return id; 113 | } 114 | 115 | 116 | @NotNull 117 | @Override 118 | public String toString() { 119 | return "(" + id + ":" + start + ")"; 120 | } 121 | 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/NameType.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | 4 | public enum NameType { 5 | LOCAL, 6 | INSTANCE, 7 | CLASS, 8 | GLOBAL 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Node.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.yinwang.rubysonar.Analyzer; 6 | import org.yinwang.rubysonar.State; 7 | import org.yinwang.rubysonar._; 8 | import org.yinwang.rubysonar.types.Type; 9 | import org.yinwang.rubysonar.types.UnionType; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | import java.util.List; 14 | 15 | 16 | public abstract class Node implements java.io.Serializable { 17 | 18 | public String file; 19 | public int start = -1; 20 | public int end = -1; 21 | public String name; 22 | public String path; 23 | public Node parent = null; 24 | 25 | 26 | public Node() { 27 | } 28 | 29 | 30 | public Node(String file, int start, int end) { 31 | this.file = file; 32 | this.start = start; 33 | this.end = end; 34 | } 35 | 36 | 37 | public void setParent(Node parent) { 38 | 39 | this.parent = parent; 40 | } 41 | 42 | 43 | @NotNull 44 | public Node getAstRoot() { 45 | if (parent == null) { 46 | return this; 47 | } 48 | return parent.getAstRoot(); 49 | } 50 | 51 | 52 | public void addChildren(@Nullable Node... nodes) { 53 | if (nodes != null) { 54 | for (Node n : nodes) { 55 | if (n != null) { 56 | n.setParent(this); 57 | } 58 | } 59 | } 60 | } 61 | 62 | 63 | public void addChildren(@Nullable Collection nodes) { 64 | if (nodes != null) { 65 | for (Node n : nodes) { 66 | if (n != null) { 67 | n.setParent(this); 68 | } 69 | } 70 | } 71 | } 72 | 73 | 74 | public void setFile(String file) { 75 | this.file = file; 76 | } 77 | 78 | 79 | @NotNull 80 | public static Type transformExpr(@NotNull Node n, State s) { 81 | return n.transform(s); 82 | } 83 | 84 | 85 | @NotNull 86 | protected abstract Type transform(State s); 87 | 88 | 89 | protected void addWarning(String msg) { 90 | Analyzer.self.putProblem(this, msg); 91 | } 92 | 93 | 94 | protected void addError(String msg) { 95 | Analyzer.self.putProblem(this, msg); 96 | } 97 | 98 | 99 | @NotNull 100 | protected Type resolveUnion(@NotNull Collection nodes, State s) { 101 | Type result = Type.UNKNOWN; 102 | for (Node node : nodes) { 103 | Type nodeType = transformExpr(node, s); 104 | result = UnionType.union(result, nodeType); 105 | } 106 | return result; 107 | } 108 | 109 | 110 | @Nullable 111 | static protected List resolveList(@Nullable Collection nodes, State s) { 112 | if (nodes == null) { 113 | return null; 114 | } else { 115 | List ret = new ArrayList<>(); 116 | for (Node n : nodes) { 117 | ret.add(transformExpr(n, s)); 118 | } 119 | return ret; 120 | } 121 | } 122 | 123 | 124 | // nodes are equal if they are from the same file and same starting point 125 | @Override 126 | public boolean equals(Object obj) { 127 | if (!(obj instanceof Node)) { 128 | return false; 129 | } else { 130 | Node node = (Node) obj; 131 | return (this.start == node.start && 132 | this.end == node.end && 133 | _.same(this.file, node.file)); 134 | } 135 | } 136 | 137 | 138 | @Override 139 | public int hashCode() { 140 | return (file + ":" + start + ":" + end).hashCode(); 141 | } 142 | 143 | 144 | public String toDisplay() { 145 | return ""; 146 | } 147 | 148 | 149 | @NotNull 150 | @Override 151 | public String toString() { 152 | return "(node:" + file + ":" + name + ":" + start + ")"; 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Op.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.yinwang.rubysonar._; 4 | 5 | 6 | public enum Op { 7 | // numeral 8 | Add, 9 | Sub, 10 | Mul, 11 | Div, 12 | Mod, 13 | Pow, 14 | FloorDiv, 15 | 16 | // comparison 17 | Eq, 18 | Eqv, 19 | Equal, 20 | Lt, 21 | Gt, 22 | 23 | // bit 24 | BitAnd, 25 | BitOr, 26 | BitXor, 27 | In, 28 | LShift, 29 | RShift, 30 | Invert, 31 | 32 | // boolean 33 | And, 34 | Or, 35 | Not, 36 | 37 | // synthetic 38 | NotEqual, 39 | NotEq, 40 | LtE, 41 | GtE, 42 | NotIn, 43 | 44 | // ruby 45 | Defined, 46 | Match, 47 | NotMatch; 48 | 49 | 50 | public static Op invert(Op op) { 51 | if (op == Op.Lt) { 52 | return Op.Gt; 53 | } 54 | 55 | if (op == Op.Gt) { 56 | return Op.Lt; 57 | } 58 | 59 | if (op == Op.Eq) { 60 | return Op.Eq; 61 | } 62 | 63 | if (op == Op.And) { 64 | return Op.Or; 65 | } 66 | 67 | if (op == Op.Or) { 68 | return Op.And; 69 | } 70 | 71 | _.die("invalid operator name for invert: " + op); 72 | return null; // unreacheable 73 | } 74 | 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Raise.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | public class Raise extends Node { 9 | 10 | public Node exceptionType; 11 | public Node inst; 12 | public Node traceback; 13 | 14 | 15 | public Raise(Node exceptionType, Node inst, Node traceback, String file, int start, int end) { 16 | super(file, start, end); 17 | this.exceptionType = exceptionType; 18 | this.inst = inst; 19 | this.traceback = traceback; 20 | addChildren(exceptionType, inst, traceback); 21 | } 22 | 23 | 24 | @NotNull 25 | @Override 26 | public Type transform(State s) { 27 | if (exceptionType != null) { 28 | transformExpr(exceptionType, s); 29 | } 30 | if (inst != null) { 31 | transformExpr(inst, s); 32 | } 33 | if (traceback != null) { 34 | transformExpr(traceback, s); 35 | } 36 | return Type.CONT; 37 | } 38 | 39 | 40 | @NotNull 41 | @Override 42 | public String toString() { 43 | return "(raise:" + traceback + ":" + exceptionType + ")"; 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/RbFloat.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | public class RbFloat extends Node { 9 | 10 | public double value; 11 | 12 | 13 | public RbFloat(String s, String file, int start, int end) { 14 | super(file, start, end); 15 | s = s.replaceAll("_", ""); 16 | this.value = Double.parseDouble(s); 17 | } 18 | 19 | 20 | @NotNull 21 | @Override 22 | public Type transform(State s) { 23 | return Type.FLOAT; 24 | } 25 | 26 | 27 | @NotNull 28 | @Override 29 | public String toString() { 30 | return "(float:" + value + ")"; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/RbInt.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | import java.math.BigInteger; 8 | 9 | 10 | public class RbInt extends Node { 11 | 12 | public BigInteger value; 13 | 14 | 15 | public RbInt(String s, String file, int start, int end) { 16 | super(file, start, end); 17 | 18 | s = s.toLowerCase(); 19 | s = s.replaceAll("_", ""); 20 | int sign = 1; 21 | 22 | if (s.startsWith("+")) { 23 | s = s.substring(1); 24 | } else if (s.startsWith("-")) { 25 | s = s.substring(1); 26 | sign = -1; 27 | } 28 | 29 | int base; 30 | if (s.startsWith("0b")) { 31 | base = 2; 32 | s = s.substring(2); 33 | } else if (s.startsWith("0o")) { 34 | base = 8; 35 | s = s.substring(2); 36 | } else if (s.startsWith("0x")) { 37 | base = 16; 38 | s = s.substring(2); 39 | } else if (s.startsWith("x")) { 40 | base = 16; 41 | s = s.substring(1); 42 | } else if (s.startsWith("0d")) { 43 | base = 10; 44 | s = s.substring(2); 45 | } else { 46 | base = 10; 47 | } 48 | 49 | value = new BigInteger(s, base); 50 | if (sign == -1) { 51 | value = value.negate(); 52 | } 53 | } 54 | 55 | 56 | @NotNull 57 | @Override 58 | public Type transform(State s) { 59 | return Type.INT; 60 | } 61 | 62 | 63 | @NotNull 64 | @Override 65 | public String toString() { 66 | return "(int:" + value + ")"; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/RbModule.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.*; 5 | import org.yinwang.rubysonar.types.ModuleType; 6 | import org.yinwang.rubysonar.types.Type; 7 | 8 | 9 | public class RbModule extends Node { 10 | 11 | public Node locator; 12 | public Name name; 13 | public Block body; 14 | public Str docstring; 15 | 16 | 17 | public RbModule(Node locator, Block body, Str docstring, String file, int start, int end) { 18 | super(file, start, end); 19 | this.locator = locator; 20 | this.body = body; 21 | this.docstring = docstring; 22 | if (locator instanceof Attribute) { 23 | this.name = ((Attribute) locator).attr; 24 | } else if (locator instanceof Name) { 25 | this.name = (Name) locator; 26 | } else { 27 | _.die("illegal module locator: " + locator); 28 | } 29 | addChildren(locator, body); 30 | } 31 | 32 | 33 | @NotNull 34 | @Override 35 | public Type transform(@NotNull State s) { 36 | if (name.id.equals("ClassMethods")) { 37 | boolean saved = Analyzer.self.staticContext; 38 | Analyzer.self.setStaticContext(true); 39 | transformExpr(body, s); 40 | Analyzer.self.setStaticContext(saved); 41 | return Type.NIL; 42 | } else { 43 | ModuleType mt = s.lookupOrCreateModule(locator, file); 44 | mt.table.insert(Constants.SELFNAME, name, mt, Binding.Kind.SCOPE); 45 | transformExpr(body, mt.table); 46 | return mt; 47 | } 48 | } 49 | 50 | 51 | @NotNull 52 | @Override 53 | public String toString() { 54 | return "(module:" + locator + ")"; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Regexp.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | public class Regexp extends Node { 9 | public Node pattern; 10 | public Node end; 11 | 12 | 13 | public Regexp(Node pattern, Node regexpEnd, String file, int start, int end) { 14 | super(file, start, end); 15 | this.pattern = pattern; 16 | this.end = regexpEnd; 17 | } 18 | 19 | 20 | @NotNull 21 | @Override 22 | protected Type transform(State s) { 23 | return Type.STR; 24 | } 25 | 26 | 27 | @NotNull 28 | @Override 29 | public String toString() { 30 | return "(regexp:" + pattern + end + ")"; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Return.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | public class Return extends Node { 9 | 10 | public Node value; 11 | 12 | 13 | public Return(Node n, String file, int start, int end) { 14 | super(file, start, end); 15 | this.value = n; 16 | addChildren(n); 17 | } 18 | 19 | 20 | @NotNull 21 | @Override 22 | public Type transform(State s) { 23 | if (value == null) { 24 | return Type.NIL; 25 | } else { 26 | return transformExpr(value, s); 27 | } 28 | } 29 | 30 | 31 | @NotNull 32 | @Override 33 | public String toString() { 34 | return "(return:" + value + ")"; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Slice.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.ListType; 6 | import org.yinwang.rubysonar.types.Type; 7 | 8 | 9 | public class Slice extends Node { 10 | 11 | public Node lower; 12 | public Node step; 13 | public Node upper; 14 | 15 | 16 | public Slice(Node lower, Node step, Node upper, String file, int start, int end) { 17 | super(file, start, end); 18 | this.lower = lower; 19 | this.step = step; 20 | this.upper = upper; 21 | addChildren(lower, step, upper); 22 | } 23 | 24 | 25 | @NotNull 26 | @Override 27 | public Type transform(State s) { 28 | if (lower != null) { 29 | transformExpr(lower, s); 30 | } 31 | if (step != null) { 32 | transformExpr(step, s); 33 | } 34 | if (upper != null) { 35 | transformExpr(upper, s); 36 | } 37 | return new ListType(); 38 | } 39 | 40 | 41 | @NotNull 42 | @Override 43 | public String toString() { 44 | return "(slice:" + lower + ":" + step + ":" + upper + ")"; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Starred.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | public class Starred extends Node { 9 | 10 | public Node value; 11 | 12 | 13 | public Starred(Node n, String file, int start, int end) { 14 | super(file, start, end); 15 | this.value = n; 16 | addChildren(n); 17 | } 18 | 19 | 20 | @NotNull 21 | @Override 22 | public Type transform(State s) { 23 | return transformExpr(value, s); 24 | } 25 | 26 | 27 | @NotNull 28 | @Override 29 | public String toString() { 30 | return "*" + value; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Str.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.StrType; 6 | import org.yinwang.rubysonar.types.Type; 7 | 8 | 9 | public class Str extends Node { 10 | 11 | public String value; 12 | 13 | 14 | public Str(@NotNull Object value, String file, int start, int end) { 15 | super(file, start, end); 16 | this.value = value.toString(); 17 | } 18 | 19 | 20 | @NotNull 21 | @Override 22 | public Type transform(State s) { 23 | return new StrType(value); 24 | } 25 | 26 | 27 | @NotNull 28 | @Override 29 | public String toString() { 30 | String summary; 31 | if (value.length() > 10) { 32 | summary = value.substring(0, 10) + "..."; 33 | } else { 34 | summary = value; 35 | } 36 | return "'" + summary + "'"; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/StrEmbed.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | public class StrEmbed extends Node { 9 | 10 | public Node value; 11 | 12 | 13 | public StrEmbed(@NotNull Node value, String file, int start, int end) { 14 | super(file, start, end); 15 | this.value = value; 16 | } 17 | 18 | 19 | @NotNull 20 | @Override 21 | public Type transform(State s) { 22 | Type valueType = value.transform(s); 23 | return Type.STR; 24 | } 25 | 26 | 27 | @NotNull 28 | @Override 29 | public String toString() { 30 | return "#{" + value + "}"; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Subscript.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.yinwang.rubysonar.State; 6 | import org.yinwang.rubysonar.types.*; 7 | 8 | 9 | public class Subscript extends Node { 10 | 11 | @NotNull 12 | public Node value; 13 | @Nullable 14 | public Node slice; // an NIndex or NSlice 15 | 16 | 17 | public Subscript(@NotNull Node value, @Nullable Node slice, String file, int start, int end) { 18 | super(file, start, end); 19 | this.value = value; 20 | this.slice = slice; 21 | addChildren(value, slice); 22 | } 23 | 24 | 25 | @NotNull 26 | @Override 27 | public Type transform(State s) { 28 | Type vt = transformExpr(value, s); 29 | Type st = slice == null ? null : transformExpr(slice, s); 30 | 31 | if (vt instanceof UnionType) { 32 | Type retType = Type.UNKNOWN; 33 | for (Type t : ((UnionType) vt).types) { 34 | retType = UnionType.union(retType, getSubscript(t, st, s)); 35 | } 36 | return retType; 37 | } else { 38 | return getSubscript(vt, st, s); 39 | } 40 | } 41 | 42 | 43 | @NotNull 44 | private Type getSubscript(@NotNull Type vt, @Nullable Type st, State s) { 45 | if (vt.isUnknownType()) { 46 | return Type.UNKNOWN; 47 | } else { 48 | if (vt instanceof ListType) { 49 | return getListSubscript(vt, st, s); 50 | } else if (vt instanceof TupleType) { 51 | return getListSubscript(((TupleType) vt).toListType(), st, s); 52 | } else if (vt instanceof DictType) { 53 | DictType dt = (DictType) vt; 54 | if (!dt.keyType.equals(st)) { 55 | addWarning("Possible KeyError (wrong type for subscript)"); 56 | } 57 | return ((DictType) vt).valueType; 58 | } else if (vt.isStrType()) { 59 | if (st != null && (st instanceof ListType || st.isNumType())) { 60 | return vt; 61 | } else { 62 | addWarning("Possible KeyError (wrong type for subscript)"); 63 | return Type.UNKNOWN; 64 | } 65 | } else { 66 | return Type.UNKNOWN; 67 | } 68 | } 69 | } 70 | 71 | 72 | @NotNull 73 | private Type getListSubscript(@NotNull Type vt, @Nullable Type st, State s) { 74 | if (vt instanceof ListType) { 75 | if (st != null && st instanceof ListType) { 76 | return vt; 77 | } else if (st == null || st.isNumType()) { 78 | return ((ListType) vt).eltType; 79 | } else { 80 | addError("The type can't be subscripted: " + vt); 81 | return Type.UNKNOWN; 82 | } 83 | } else { 84 | return Type.UNKNOWN; 85 | } 86 | } 87 | 88 | 89 | @NotNull 90 | @Override 91 | public String toString() { 92 | return value + "[" + slice + "]"; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Symbol.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.SymbolType; 6 | import org.yinwang.rubysonar.types.Type; 7 | 8 | 9 | public class Symbol extends Node { 10 | 11 | @NotNull 12 | public final String id; // identifier 13 | 14 | 15 | public Symbol(@NotNull String id, String file, int start, int end) { 16 | super(file, start, end); 17 | this.id = id; 18 | } 19 | 20 | 21 | @NotNull 22 | @Override 23 | public Type transform(@NotNull State s) { 24 | return new SymbolType(id); 25 | } 26 | 27 | 28 | @NotNull 29 | @Override 30 | public String toString() { 31 | return ":" + id; 32 | } 33 | 34 | 35 | @NotNull 36 | @Override 37 | public String toDisplay() { 38 | return id; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Try.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | import org.yinwang.rubysonar.types.UnionType; 7 | 8 | 9 | public class Try extends Node { 10 | 11 | public Node rescue; 12 | public Node body; 13 | public Node orelse; 14 | public Node finalbody; 15 | 16 | 17 | public Try(Node rescue, Node body, Node orelse, Node finalbody, 18 | String file, int start, int end) 19 | { 20 | super(file, start, end); 21 | this.rescue = rescue; 22 | this.body = body; 23 | this.orelse = orelse; 24 | this.finalbody = finalbody; 25 | addChildren(rescue); 26 | addChildren(body, orelse); 27 | } 28 | 29 | 30 | @NotNull 31 | @Override 32 | public Type transform(State s) { 33 | Type tp1 = Type.UNKNOWN; 34 | Type tp2 = Type.UNKNOWN; 35 | Type tph = Type.UNKNOWN; 36 | Type tpFinal = Type.UNKNOWN; 37 | 38 | if (rescue != null) { 39 | tph = UnionType.union(tph, transformExpr(rescue, s)); 40 | } 41 | 42 | if (body != null) { 43 | tp1 = transformExpr(body, s); 44 | } 45 | 46 | if (orelse != null) { 47 | tp2 = transformExpr(orelse, s); 48 | } 49 | 50 | if (finalbody != null) { 51 | tpFinal = transformExpr(finalbody, s); 52 | } 53 | 54 | return new UnionType(tp1, tp2, tph, tpFinal); 55 | } 56 | 57 | 58 | @NotNull 59 | @Override 60 | public String toString() { 61 | return "(try:" + rescue + ":" + body + ":" + orelse + ")"; 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/UnaryOp.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | public class UnaryOp extends Node { 9 | 10 | public Op op; 11 | public Node operand; 12 | 13 | 14 | public UnaryOp(Op op, Node operand, String file, int start, int end) { 15 | super(file, start, end); 16 | this.op = op; 17 | this.operand = operand; 18 | addChildren(operand); 19 | } 20 | 21 | 22 | @NotNull 23 | @Override 24 | public Type transform(State s) { 25 | return transformExpr(operand, s); 26 | } 27 | 28 | 29 | @NotNull 30 | @Override 31 | public String toString() { 32 | return "(" + op + " " + operand + ")"; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Undef.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | import java.util.List; 8 | 9 | 10 | public class Undef extends Node { 11 | 12 | public List targets; 13 | 14 | 15 | public Undef(List elts, String file, int start, int end) { 16 | super(file, start, end); 17 | this.targets = elts; 18 | addChildren(elts); 19 | } 20 | 21 | 22 | @NotNull 23 | @Override 24 | public Type transform(@NotNull State s) { 25 | for (Node n : targets) { 26 | transformExpr(n, s); 27 | if (n instanceof Name) { 28 | s.remove(((Name) n).id); 29 | } 30 | } 31 | return Type.CONT; 32 | } 33 | 34 | 35 | @NotNull 36 | @Override 37 | public String toString() { 38 | return "(undef:" + targets + ")"; 39 | } 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Url.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | /** 9 | * virtual-AST node used to represent virtual source locations for builtins 10 | * as external urls. 11 | */ 12 | public class Url extends Node { 13 | 14 | private String url; 15 | 16 | 17 | public Url(String url) { 18 | this.url = url; 19 | } 20 | 21 | 22 | public String getURL() { 23 | return url; 24 | } 25 | 26 | 27 | @NotNull 28 | @Override 29 | public Type transform(State s) { 30 | return Type.STR; 31 | } 32 | 33 | 34 | @NotNull 35 | @Override 36 | public String toString() { 37 | return "(url:" + url + ")"; 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Void.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | 7 | 8 | public class Void extends Node { 9 | 10 | public Void(String file, int start, int end) { 11 | super(file, start, end); 12 | } 13 | 14 | 15 | @NotNull 16 | @Override 17 | public Type transform(State s) { 18 | return Type.CONT; 19 | } 20 | 21 | 22 | @NotNull 23 | @Override 24 | public String toString() { 25 | return "(void)"; 26 | } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/While.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.Type; 6 | import org.yinwang.rubysonar.types.UnionType; 7 | 8 | 9 | public class While extends Node { 10 | 11 | public Node test; 12 | public Node body; 13 | public Node orelse; 14 | 15 | 16 | public While(Node test, Node body, Node orelse, String file, int start, int end) { 17 | super(file, start, end); 18 | this.test = test; 19 | this.body = body; 20 | this.orelse = orelse; 21 | addChildren(test, body, orelse); 22 | } 23 | 24 | 25 | @NotNull 26 | @Override 27 | public Type transform(State s) { 28 | transformExpr(test, s); 29 | Type t = Type.UNKNOWN; 30 | 31 | if (body != null) { 32 | t = transformExpr(body, s); 33 | } 34 | 35 | if (orelse != null) { 36 | t = UnionType.union(t, transformExpr(orelse, s)); 37 | } 38 | 39 | return t; 40 | } 41 | 42 | 43 | @NotNull 44 | @Override 45 | public String toString() { 46 | return "(while:" + test + ":" + body + ":" + orelse + ")"; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/ast/Yield.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.ast; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.State; 5 | import org.yinwang.rubysonar.types.ListType; 6 | import org.yinwang.rubysonar.types.Type; 7 | 8 | 9 | public class Yield extends Node { 10 | 11 | public Node value; 12 | 13 | 14 | public Yield(Node n, String file, int start, int end) { 15 | super(file, start, end); 16 | this.value = n; 17 | addChildren(n); 18 | } 19 | 20 | 21 | @NotNull 22 | @Override 23 | public Type transform(State s) { 24 | if (value != null) { 25 | return new ListType(transformExpr(value, s)); 26 | } else { 27 | return Type.NIL; 28 | } 29 | } 30 | 31 | 32 | @NotNull 33 | @Override 34 | public String toString() { 35 | return "(yield " + value + ")"; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/demos/Demo.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.demos; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.yinwang.rubysonar.Analyzer; 5 | import org.yinwang.rubysonar.Options; 6 | import org.yinwang.rubysonar.Progress; 7 | import org.yinwang.rubysonar._; 8 | 9 | import java.io.File; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | 15 | public class Demo { 16 | 17 | private static File OUTPUT_DIR; 18 | 19 | private static final String CSS = _.readResource("org/yinwang/rubysonar/css/demo.css"); 20 | private static final String JS = _.readResource("org/yinwang/rubysonar/javascript/highlight.js"); 21 | private static final String JS_DEBUG = _.readResource("org/yinwang/rubysonar/javascript/highlight-debug.js"); 22 | 23 | private Analyzer analyzer; 24 | private String rootPath; 25 | private Linker linker; 26 | 27 | 28 | private void makeOutputDir() { 29 | if (!OUTPUT_DIR.exists()) { 30 | OUTPUT_DIR.mkdirs(); 31 | _.msg("Created directory: " + OUTPUT_DIR.getAbsolutePath()); 32 | } 33 | } 34 | 35 | 36 | private void start(@NotNull String fileOrDir, Map options) throws Exception { 37 | File f = new File(fileOrDir); 38 | File rootDir = f.isFile() ? f.getParentFile() : f; 39 | try { 40 | rootPath = _.unifyPath(rootDir); 41 | } catch (Exception e) { 42 | _.die("File not found: " + f); 43 | } 44 | 45 | analyzer = new Analyzer(options); 46 | _.msg("Loading and analyzing files"); 47 | analyzer.analyze(f.getPath()); 48 | analyzer.finish(); 49 | 50 | generateHtml(); 51 | analyzer.close(); 52 | } 53 | 54 | 55 | private void generateHtml() { 56 | _.msg("\nGenerating HTML"); 57 | makeOutputDir(); 58 | 59 | linker = new Linker(rootPath, OUTPUT_DIR); 60 | linker.findLinks(analyzer); 61 | 62 | int rootLength = rootPath.length(); 63 | 64 | int total = 0; 65 | for (String path : analyzer.getLoadedFiles()) { 66 | if (path.startsWith(rootPath)) { 67 | total++; 68 | } 69 | } 70 | 71 | _.msg("\nWriting HTML"); 72 | Progress progress = new Progress(total, 50); 73 | 74 | for (String path : analyzer.getLoadedFiles()) { 75 | if (path.startsWith(rootPath)) { 76 | progress.tick(); 77 | File destFile = _.joinPath(OUTPUT_DIR, path.substring(rootLength)); 78 | destFile.getParentFile().mkdirs(); 79 | String destPath = destFile.getAbsolutePath() + ".html"; 80 | String html = markup(path); 81 | try { 82 | _.writeFile(destPath, html); 83 | } catch (Exception e) { 84 | _.msg("Failed to write: " + destPath); 85 | } 86 | } 87 | } 88 | 89 | _.msg("\nWrote " + analyzer.getLoadedFiles().size() + " files to " + OUTPUT_DIR); 90 | } 91 | 92 | 93 | @NotNull 94 | private String markup(String path) { 95 | String source; 96 | 97 | try { 98 | source = _.readFile(path); 99 | } catch (Exception e) { 100 | _.die("Failed to read file: " + path); 101 | return ""; 102 | } 103 | 104 | List\n") 118 | 119 | .append("\n") 122 | 123 | .append("\n\n") 124 | 125 | .append("
")
126 |                 .append(addLineNumbers(styledSource))
127 |                 .append("
") 128 | 129 | .append(""); 130 | return sb.toString(); 131 | } 132 | 133 | 134 | @NotNull 135 | private String addLineNumbers(@NotNull String source) { 136 | StringBuilder result = new StringBuilder((int) (source.length() * 1.2)); 137 | int count = 1; 138 | for (String line : source.split("\n")) { 139 | result.append(""); 140 | result.append(String.format("%1$4d", count++)); 141 | result.append(" "); 142 | result.append(line); 143 | result.append("\n"); 144 | } 145 | return result.toString(); 146 | } 147 | 148 | 149 | private static void usage() { 150 | _.msg("Usage: java -jar rubysonar-2.0-SNAPSHOT.jar "); 151 | _.msg("Example that generates an index for Python 2.7 standard library:"); 152 | _.msg(" java -jar rubysonar-2.0-SNAPSHOT.jar /usr/lib/python2.7 ./html"); 153 | System.exit(0); 154 | } 155 | 156 | 157 | @NotNull 158 | private static File checkFile(String path) { 159 | File f = new File(path); 160 | if (!f.canRead()) { 161 | _.die("Path not found or not readable: " + path); 162 | } 163 | return f; 164 | } 165 | 166 | 167 | public static void main(@NotNull String[] args) throws Exception { 168 | Options options = new Options(args); 169 | List argList = options.getArgs(); 170 | String fileOrDir = argList.get(0); 171 | OUTPUT_DIR = new File(argList.get(1)); 172 | 173 | new Demo().start(fileOrDir, options.getOptionsMap()); 174 | _.msg(_.getGCStats()); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/org/yinwang/rubysonar/demos/Style.java: -------------------------------------------------------------------------------- 1 | package org.yinwang.rubysonar.demos; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.yinwang.rubysonar._; 6 | 7 | import java.util.List; 8 | 9 | 10 | /** 11 | * Represents a simple style run for purposes of source highlighting. 12 | */ 13 | public class Style implements Comparable