├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── shard.lock ├── shard.yml ├── spec ├── completion_spec.cr ├── cracker_spec.cr └── spec_helper.cr └── src ├── cracker.cr └── cracker ├── client.cr ├── commands ├── client.cr └── server.cr ├── completion_context.cr ├── db.cr ├── db_visitor.cr ├── generator.cr ├── message_builder.cr ├── messages └── messages.cr ├── server.cr ├── version.cr └── visitor.cr /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /libs/ 3 | /lib/ 4 | /bin/ 5 | /.shards/ 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Arnaud Fernandés 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cracker 2 | 3 | Provide auto-completion for the crystal language ( racer like ) 4 | 5 | For Emacs integration see [TechMagister/emacs-cracker](https://github.com/TechMagister/emacs-cracker) 6 | ![Screenshot](https://github.com/TechMagister/emacs-cracker/raw/master/screenshot.png) 7 | 8 | For Sublime Text integration see [TechMagister/CrystalAutoComplete](https://github.com/TechMagister/CrystalAutoComplete) 9 | ![screenshot1](https://raw.githubusercontent.com/TechMagister/CrystalAutoComplete/master/screenshots/screenshot1.png) 10 | 11 | ## Installation 12 | 13 | - Clone the repository 14 | - Build 15 | - Copy the binary to your $PATH 16 | 17 | ## Usage 18 | Start the server 19 | 20 | ``` shell 21 | cracker server /path/to/crystal/source 22 | ``` 23 | 24 | And query : 25 | 26 | ``` shell 27 | $ cracker client --help 28 | Options: 29 | --add-path Source path to add to completion database 30 | -p Server port 31 | (default: 1234) 32 | --starts-with format : Class#method for instance method 33 | Class.method for class method 34 | --context Take a context in stdin 35 | --stop-server Stop the server 36 | -h, --help show this help 37 | ``` 38 | 39 | Example : 40 | 41 | ``` shell 42 | $ cracker client --starts-with Array.e 43 | ``` 44 | Outputs all the Array class methods starting with e : 45 | ``` json 46 | { 47 | "status": "success", 48 | "results": [ 49 | { 50 | "name": "Array.each_product(arrays)", 51 | "file": "/path/to/crystal/source/array.cr", 52 | "location": ":1138:3", 53 | "type": "Function", 54 | "signature": "def self.each_product(arrays)" 55 | }, 56 | { 57 | "name": "Array.each_product(arrays : Array)", 58 | "file": "/path/to/crystal/source/array.cr", 59 | "location": ":1162:3", 60 | "type": "Function", 61 | "signature": "def self.each_product(*arrays : Array)" 62 | } 63 | ] 64 | } 65 | ``` 66 | 67 | ``` 68 | $ cracker client --starts-with Array#e 69 | ``` 70 | Outputs all the Array instance methods starting with e : 71 | 72 | ``` json 73 | { 74 | "status": "success", 75 | "results": [ 76 | { 77 | "name": "Array#each_permutation(size : Int = self.size)", 78 | "file": "/path/to/crystal/source/array.cr", 79 | "location": ":964:3", 80 | "type": "Function", 81 | "signature": "def each_permutation(size : Int = self.size)" 82 | }, 83 | ... 84 | { 85 | "name": "Array#each_repeated_permutation(size : Int = self.size)", 86 | "file": "/path/to/crystal/source/array.cr", 87 | "location": ":1176:3", 88 | "type": "Function", 89 | "signature": "def each_repeated_permutation(size : Int = self.size)" 90 | } 91 | ] 92 | } 93 | ``` 94 | 95 | ``` shell 96 | $ echo "@test : String\n@test.to_f" | cracker client --context 97 | ``` 98 | 99 | ``` json 100 | { 101 | "status": "success", 102 | "results": [ 103 | { 104 | "name": "String#to_f(whitespace = true,strict = true)", 105 | "file": "/home/arnaud/workspace/repos/crystal/src/string.cr", 106 | "location": ":608:3", 107 | "type": "Function", 108 | "signature": "def to_f(whitespace = true, strict = true)" 109 | }, 110 | { 111 | "name": "String#to_f?(whitespace = true,strict = true)", 112 | "file": "/home/arnaud/workspace/repos/crystal/src/string.cr", 113 | "location": ":626:3", 114 | "type": "Function", 115 | "signature": "def to_f?(whitespace = true, strict = true)" 116 | }, 117 | { 118 | "name": "String#to_f32(whitespace = true,strict = true)", 119 | "file": "/home/arnaud/workspace/repos/crystal/src/string.cr", 120 | "location": ":631:3", 121 | "type": "Function", 122 | "signature": "def to_f32(whitespace = true, strict = true)" 123 | }, 124 | { 125 | "name": "String#to_f32?(whitespace = true,strict = true)", 126 | "file": "/home/arnaud/workspace/repos/crystal/src/string.cr", 127 | "location": ":636:3", 128 | "type": "Function", 129 | "signature": "def to_f32?(whitespace = true, strict = true)" 130 | }, 131 | { 132 | "name": "String#to_f64(whitespace = true,strict = true)", 133 | "file": "/home/arnaud/workspace/repos/crystal/src/string.cr", 134 | "location": ":644:3", 135 | "type": "Function", 136 | "signature": "def to_f64(whitespace = true, strict = true)" 137 | }, 138 | { 139 | "name": "String#to_f64?(whitespace = true,strict = true)", 140 | "file": "/home/arnaud/workspace/repos/crystal/src/string.cr", 141 | "location": ":649:3", 142 | "type": "Function", 143 | "signature": "def to_f64?(whitespace = true, strict = true)" 144 | } 145 | ] 146 | } 147 | 148 | ``` 149 | 150 | ## Development 151 | 152 | - [x] Allow to search using starts_with? 153 | - [x] Add line number to the result 154 | - [x] Replate type by a string 155 | - [x] Daemonize 156 | - [x] Append new source path for completion ( project path for instance ) 157 | - [x] Method completion 158 | - [ ] Enum completion 159 | - [ ] Macro completion 160 | - [ ] Alias Completion 161 | - [ ] Lib completion 162 | - [ ] Constants completion 163 | - [ ] IDE integration 164 | 165 | ## Contributing 166 | 167 | 1. Fork it ( https://github.com/TechMagister/cracker/fork ) 168 | 2. Create your feature branch (git checkout -b my-new-feature) 169 | 3. Commit your changes (git commit -am 'Add some feature') 170 | 4. Push to the branch (git push origin my-new-feature) 171 | 5. Create a new Pull Request 172 | 173 | ## Contributors 174 | 175 | - [TechMagister](https://github.com/TechMagister) Arnaud Fernandés - creator, maintainer 176 | -------------------------------------------------------------------------------- /shard.lock: -------------------------------------------------------------------------------- 1 | version: 1.0 2 | shards: 3 | callback: 4 | github: mosop/callback 5 | version: 0.6.3 6 | 7 | cli: 8 | github: amberframework/cli 9 | version: 0.7.0 10 | 11 | optarg: 12 | github: mosop/optarg 13 | version: 0.5.8 14 | 15 | string_inflection: 16 | github: mosop/string_inflection 17 | version: 0.2.1 18 | 19 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: cracker 2 | version: 0.1.0 3 | 4 | authors: 5 | - Arnaud Fernandés 6 | 7 | targets: 8 | cracker: 9 | main: src/cracker.cr 10 | 11 | crystal: 0.20.5+70 12 | 13 | license: MIT 14 | 15 | dependencies: 16 | cli: 17 | github: mosop/cli 18 | -------------------------------------------------------------------------------- /spec/completion_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Cracker::CompletionContext do 4 | 5 | it "should match true boolean" do 6 | ctx = Cracker::CompletionContext.new "test = true\n test" 7 | ctx.get_type.should eq "Bool" 8 | end 9 | 10 | it "should match false boolean" do 11 | ctx = Cracker::CompletionContext.new "test = false\n test" 12 | ctx.get_type.should eq "Bool" 13 | end 14 | 15 | it "should match Integers" do 16 | ctx = integer_ctx 'a' 17 | ctx.get_type.should eq "Int32" 18 | 19 | ctx = integer_ctx 'b' 20 | ctx.get_type.should eq "Int8" 21 | 22 | ctx = integer_ctx 'c' 23 | ctx.get_type.should eq "Int16" 24 | 25 | ctx = integer_ctx 'd' 26 | ctx.get_type.should eq "Int32" 27 | 28 | ctx = integer_ctx 'e' 29 | ctx.get_type.should eq "Int64" 30 | 31 | ctx = integer_ctx 'f' 32 | ctx.get_type.should eq "UInt8" 33 | 34 | ctx = integer_ctx 'g' 35 | ctx.get_type.should eq "UInt16" 36 | 37 | ctx = integer_ctx 'h' 38 | ctx.get_type.should eq "UInt32" 39 | 40 | ctx = integer_ctx 'i' 41 | ctx.get_type.should eq "UInt64" 42 | 43 | ctx = integer_ctx 'j' 44 | ctx.get_type.should eq "Int32" 45 | 46 | ctx = integer_ctx 'k' 47 | ctx.get_type.should eq "Int32" 48 | 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /spec/cracker_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/cracker/**" 3 | 4 | INTEGER_CTX = " 5 | a = 1 # Int32 6 | 7 | b = 1_i8 # Int8 8 | c = 1_i16 # Int16 9 | d = 1_i32 # Int32 10 | e = 1_i64 # Int64 11 | 12 | f = 1_u8 # UInt8 13 | g = 1_u16 # UInt16 14 | h = 1_u32 # UInt32 15 | i = 1_u64 # UInt64 16 | 17 | j = +10 # Int32 18 | k = -20 # Int32 19 | 20 | l = 2147483648 # Int64 21 | m = 9223372036854775808 # UInt64 22 | " 23 | 24 | def integer_ctx(var) 25 | Cracker::CompletionContext.new(INTEGER_CTX + var) 26 | end 27 | -------------------------------------------------------------------------------- /src/cracker.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "cli" 3 | require "./cracker/*" 4 | 5 | require "./cracker/commands/client" 6 | require "./cracker/commands/server" 7 | 8 | Cracker::Commands::Cracker.run ARGV 9 | -------------------------------------------------------------------------------- /src/cracker/client.cr: -------------------------------------------------------------------------------- 1 | require "socket" 2 | 3 | require "./messages/messages" 4 | 5 | module Cracker 6 | class Client 7 | def initialize(@port : Int32) 8 | @client = TCPSocket.new("localhost", @port) 9 | end 10 | 11 | def send(cmd : Messages::Command) 12 | @client << cmd.to_json << "\n" 13 | response = @client.gets 14 | @client.close 15 | puts response 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /src/cracker/commands/client.cr: -------------------------------------------------------------------------------- 1 | require "cli" 2 | 3 | require "../client" 4 | require "../message_builder" 5 | 6 | module Cracker::Commands 7 | class Cracker < Cli::Supercommand 8 | class Client < Cli::Command 9 | 10 | class Help 11 | header "Auto completion client for Cracker server" 12 | footer "(C) 2016 Ghilde Sud" 13 | end 14 | 15 | class Options 16 | string "-p", desc: "Server port", default: "1234" 17 | string "--starts-with", desc: "format : Class#method for instance method\n" + 18 | " Class.method for class method" 19 | bool "--context", desc: "Take a context in stdin" 20 | string "--add-path", desc: "Source path to add to completion database" 21 | bool "--stop-server", desc: "Stop the server" 22 | help 23 | end 24 | 25 | def port? 26 | options.p.to_u32? 27 | end 28 | 29 | def port 30 | options.p.to_i 31 | end 32 | 33 | def validate 34 | error! "Invalid port" unless port? 35 | if !(options.starts_with? || options.add_path? || options.stop_server? || options.context?) 36 | error! "You should specify at least one option" 37 | end 38 | end 39 | 40 | def run 41 | validate 42 | begin 43 | client = ::Cracker::Client.new port 44 | cmd = if options.starts_with? 45 | MessageBuilder.match options.starts_with 46 | elsif options.add_path? 47 | MessageBuilder.add_path options.add_path 48 | elsif options.stop_server? 49 | MessageBuilder.stop_server 50 | elsif options.context? 51 | ctx = STDIN.gets '\0' || " " 52 | ctx = ctx.not_nil![0...-1] 53 | MessageBuilder.context ctx 54 | else 55 | nil 56 | end 57 | client.send cmd if cmd 58 | rescue err 59 | STDERR.puts "Fail to send completion request to the server on port : #{options.p}" 60 | STDERR.puts err.to_s 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /src/cracker/commands/server.cr: -------------------------------------------------------------------------------- 1 | require "cli" 2 | 3 | module Cracker::Commands 4 | class Cracker < Cli::Supercommand 5 | class Server < Cli::Command 6 | class Help 7 | header "Auto completion server for the crystal language" 8 | footer "(C) 2016 Ghilde Sud" 9 | end 10 | 11 | class Options 12 | string "-p", default: "1234", desc: "Server port" 13 | arg "crystal_source", desc: "Path to the crystal source", required: true 14 | help 15 | end 16 | 17 | def port? 18 | options.p.to_u32? 19 | end 20 | 21 | def port 22 | options.p.to_i 23 | end 24 | 25 | def validate 26 | error! "Invalid port given : #{port?}" unless port? 27 | end 28 | 29 | def run 30 | validate 31 | generator = ::Cracker::Generator.new [args.crystal_source] 32 | db = generator.db 33 | server = ::Cracker::Server.new db, "localhost", port 34 | server.run 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /src/cracker/completion_context.cr: -------------------------------------------------------------------------------- 1 | module Cracker 2 | 3 | TYPE_REGEXP = /[A-Z][a-zA-Z_:]+/ 4 | NEW_REGEXP = /(?#{TYPE_REGEXP})(?:\(.+\))?\.new/ 5 | FUNC_CALL_REGEXP = /(?#{TYPE_REGEXP})\.(?\w+)[\(\s]/ 6 | 7 | class CompletionContext 8 | 9 | getter context, content 10 | 11 | @db : Db? 12 | @context : String 13 | @content : String 14 | @splitted : Array(String) 15 | 16 | def initialize(@context : String, @db = nil) 17 | content = "" 18 | (@context.size-1).downto 0 do |i| 19 | break unless @context[i].to_s.match /[@A-Za-z0-9_\.:]/ 20 | content += @context[i] 21 | end 22 | @content = content.reverse 23 | @splitted = @content.split '.' 24 | end 25 | 26 | def get_type 27 | if raw_type = get_raw_type 28 | raw_type 29 | elsif m = @context.match /#{@splitted.first} = #{NEW_REGEXP}/ 30 | m["type"] 31 | elsif m = @context.match /#{@splitted.first} : (?#{TYPE_REGEXP})/ 32 | m["type"] 33 | elsif (m = @context.match /#{@splitted.first} = #{FUNC_CALL_REGEXP}/) && (db = @db) 34 | pattern = "#{m["class"]}.#{m["method"]}" 35 | res = db.match pattern 36 | if res.size == 1 && (entry = res.first) && 37 | (m = entry.signature.match /:\s?(#{TYPE_REGEXP})(?:\(.+\))?\s*$/) 38 | return m[1] 39 | else 40 | Server.logger.debug "Fail to get the return type of function call : #{m}" 41 | end 42 | elsif match = @context.match /def #{@splitted.first}\(.*\) : (?#{TYPE_REGEXP})/ 43 | match["type"] 44 | else 45 | Server.logger.debug "Can't find the type of \"#{@splitted.first}\"" 46 | nil 47 | end 48 | 49 | end 50 | 51 | def get_raw_type 52 | if @context.match /#{@splitted.first} = (true|false)\s/ 53 | "Bool" 54 | elsif m = @context.match /#{@splitted.first} = [-+]?[0-9]+(_([iu])(8|16|32|64))?/ 55 | if m[1]? 56 | "#{(m[2] == "i" ? "Int" : "UInt" )}#{m[3]}" 57 | else 58 | "Int32" 59 | end 60 | end 61 | end 62 | 63 | def internal_match 64 | splitted = @content.split "::" 65 | 66 | if splitted.size == 1 67 | if scan = @context.scan(/ (#{@content}\w+)/) 68 | res = Array(DbEntry).new 69 | scan.each do |m| 70 | res << DbEntry.new m[1], "", EntryType::NameSpace 71 | end 72 | res 73 | else 74 | Array(DbEntry).new 75 | end 76 | else 77 | Array(DbEntry).new 78 | end 79 | end 80 | 81 | 82 | def is_namespace 83 | @context.ends_with? "::" 84 | end 85 | 86 | def is_class 87 | @splitted.first.match /^[A-Z]/ 88 | end 89 | 90 | def is_dotted 91 | @splitted.size != 1 92 | end 93 | 94 | def class_method_pattern(type = @splitted.first) 95 | "#{type}.#{@splitted[1]?}" 96 | end 97 | 98 | def instance_method_pattern(type = @splitted.first) 99 | "#{type}##{@splitted[1]?}" 100 | end 101 | 102 | def namespace_pattern 103 | @content[0...-2] 104 | end 105 | 106 | def content_pattern 107 | @content 108 | end 109 | 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /src/cracker/db.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | require "./completion_context" 4 | 5 | module Cracker 6 | enum EntryType 7 | NameSpace 8 | Function 9 | 10 | def to_json(builder) 11 | builder.string(to_s) 12 | end 13 | end 14 | 15 | struct DbEntry 16 | JSON.mapping( 17 | name: String, 18 | file: String, 19 | location: String?, 20 | type: EntryType, 21 | signature: String 22 | ) 23 | 24 | def initialize(@name : String, node : Crystal::Def, @file : String, @type = EntryType::Function) 25 | @signature = node.to_s.partition("\n")[0] 26 | @location = node.location.to_s if node.location 27 | end 28 | 29 | def initialize(@name, @file, @type, @signature = "", @location = nil) 30 | end 31 | 32 | def_equals_and_hash @name, @type 33 | end 34 | 35 | class Db 36 | @raw_storage = Set(DbEntry).new 37 | 38 | @path_stack = Array(String).new 39 | 40 | def with_context(ctx : String) : Array(DbEntry) 41 | 42 | context = CompletionContext.new ctx, self 43 | res = Array(DbEntry).new 44 | 45 | if context.is_namespace 46 | res = match context.namespace_pattern, EntryType::NameSpace 47 | elsif context.is_class && !context.is_dotted 48 | res = match context.content_pattern, EntryType::NameSpace 49 | if res.empty? 50 | res = context.internal_match 51 | end 52 | elsif context.is_class 53 | res = match context.class_method_pattern 54 | elsif (var_type = context.get_type) && context.is_dotted 55 | res = match context.instance_method_pattern var_type 56 | elsif var_type # && !context.is_dotted 57 | tmp = match context.instance_method_pattern(var_type) 58 | # do not return methods that can only be called with a dot 59 | res = tmp.select { |e| e.name.match /#[^\w]/ } 60 | else 61 | Server.logger.debug "Can't extract anything : #{ctx[-10..-1]}" 62 | end 63 | 64 | if res.empty? 65 | Server.logger.debug "No match found for #{context.content_pattern}" 66 | end 67 | 68 | res 69 | end 70 | 71 | def match(pattern : String, etype = EntryType::Function) : Array(DbEntry) 72 | @raw_storage.select do |entry| 73 | entry.name.includes?(pattern) && entry.type == etype 74 | end 75 | end 76 | 77 | def starts_with?(pattern : String) : Array(DbEntry) 78 | res = Array(DbEntry).new 79 | @raw_storage.each do |entry| 80 | if entry.name.starts_with?(pattern) 81 | res << entry 82 | end 83 | end 84 | res.uniq 85 | end 86 | 87 | def push_module(name : String, file = "") 88 | @path_stack << name 89 | @raw_storage << DbEntry.new @path_stack.join("::"), file, EntryType::NameSpace 90 | end 91 | 92 | def push_class(node : Crystal::ClassDef, file = "") 93 | @path_stack << node.name.to_s.split("::").last 94 | @raw_storage << DbEntry.new @path_stack.join("::"), file, EntryType::NameSpace 95 | end 96 | 97 | def push_def(func : Crystal::Def, file : String) 98 | func_sep = (func.receiver ? "." : "#") 99 | full = @path_stack.join("::") + "#{func_sep}#{func.name}(#{func.args.join(',')})" 100 | full += ": #{func.return_type}" if func.return_type 101 | 102 | @raw_storage << DbEntry.new full, func, file 103 | end 104 | 105 | def pop_module 106 | @path_stack.pop? 107 | end 108 | 109 | def pop_class 110 | @path_stack.pop? 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /src/cracker/db_visitor.cr: -------------------------------------------------------------------------------- 1 | require "./visitor" 2 | require "./db" 3 | 4 | module Cracker 5 | class DbVisitor < Visitor 6 | property current_file 7 | 8 | @current_file : String 9 | @class_pop = Hash(Crystal::ClassDef, Int32).new 10 | @module_pop = Hash(Crystal::ModuleDef, Int32).new 11 | 12 | def initialize(@db : Db) 13 | @current_file = "" 14 | end 15 | 16 | def visit(node : Crystal::ModuleDef) 17 | ns = node.name.to_s.split "::" 18 | @module_pop[node] = ns.size 19 | 20 | if ns.size > 1 21 | ns[0..-2].each do |namespace| 22 | @db.push_module namespace, @current_file 23 | end 24 | end 25 | 26 | @db.push_module ns.last, @current_file 27 | true 28 | end 29 | 30 | def visit(node : Crystal::ClassDef) 31 | s = node.name.to_s.split "::" 32 | @class_pop[node] = s.size - 1 33 | 34 | if s.size > 1 35 | s[0..-2].each do |namespace| 36 | @db.push_module namespace, @current_file 37 | end 38 | end 39 | 40 | @db.push_class node, @current_file 41 | true 42 | end 43 | 44 | def visit(node : Crystal::Def) 45 | @db.push_def node, current_file if node.visibility == Crystal::Visibility::Public 46 | false 47 | end 48 | 49 | def visit(node : Crystal::Expressions) 50 | true 51 | end 52 | 53 | def end_visit(node : Crystal::ModuleDef) 54 | times = @module_pop[node] 55 | times.times do |_| 56 | @db.pop_module 57 | end 58 | @module_pop.delete node 59 | end 60 | 61 | def end_visit(node : Crystal::ClassDef) 62 | @class_pop[node].times do |_| 63 | @db.pop_module 64 | end 65 | @db.pop_class 66 | end 67 | 68 | def visit(node) 69 | false 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /src/cracker/generator.cr: -------------------------------------------------------------------------------- 1 | require "compiler/crystal/syntax" 2 | 3 | require "./db" 4 | require "./db_visitor" 5 | 6 | module Cracker 7 | class Generator 8 | getter db 9 | getter processed_files 10 | 11 | @db : Db 12 | @visitor : DbVisitor 13 | 14 | @processed_files = 0 15 | 16 | def self.add_paths(db : Db, paths : Array(String)) 17 | gen = Generator.new db, paths 18 | gen.processed_files 19 | end 20 | 21 | def initialize(@paths : Array(String)) 22 | @db = Db.new 23 | @visitor = DbVisitor.new @db 24 | compute_paths 25 | end 26 | 27 | def initialize(@db : Db, @paths : Array(String)) 28 | @visitor = DbVisitor.new @db 29 | compute_paths 30 | end 31 | 32 | def compute_paths 33 | @paths.each do |path| 34 | Dir.glob "#{path}/**/*.cr" do |filename| 35 | parse filename 36 | end 37 | end 38 | end 39 | 40 | def parse(filename : String) 41 | @processed_files += 1 42 | node = Crystal::Parser.parse File.read(filename) 43 | @visitor.current_file = filename 44 | node.accept @visitor 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /src/cracker/message_builder.cr: -------------------------------------------------------------------------------- 1 | require "./messages/*" 2 | 3 | module Cracker 4 | 5 | class MessageBuilder 6 | 7 | def self.match(content : String) 8 | Messages::Command.match_command content 9 | end 10 | 11 | def self.add_path(path : String) 12 | Messages::Command.add_path_command path 13 | end 14 | 15 | def self.stop_server 16 | Messages::Command.exit_command 17 | end 18 | 19 | def self.context(content : String) 20 | Messages::Command.context_command content 21 | end 22 | 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /src/cracker/messages/messages.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "../db" 3 | 4 | module Cracker::Messages 5 | enum CommandType 6 | Match = 0 7 | AddPath 8 | Context 9 | Exit = 99 10 | end 11 | 12 | class Command 13 | JSON.mapping({ 14 | type: CommandType, 15 | content: String, 16 | }) 17 | 18 | def initialize(@type, @content) 19 | end 20 | 21 | def self.match_command(content : String) 22 | new CommandType::Match, content 23 | end 24 | 25 | def self.add_path_command(path : String) 26 | new CommandType::AddPath, path 27 | end 28 | 29 | def self.exit_command 30 | new CommandType::Exit, "" 31 | end 32 | 33 | def self.context_command(content : String) 34 | new CommandType::Context, content 35 | end 36 | 37 | end 38 | 39 | struct Result 40 | JSON.mapping({ 41 | name: String, 42 | file: String, 43 | location: String?, 44 | type: Cracker::EntryType, 45 | signature: String, 46 | }) 47 | 48 | def initialize(dbentry : DbEntry) 49 | @name = dbentry.name 50 | @file = dbentry.file 51 | @location = dbentry.location 52 | @type = dbentry.type 53 | @signature = dbentry.signature 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /src/cracker/server.cr: -------------------------------------------------------------------------------- 1 | require "socket" 2 | require "logger" 3 | 4 | require "./db" 5 | require "./messages/messages" 6 | 7 | module Cracker 8 | class Server 9 | 10 | @@logger : Logger? 11 | 12 | @db : Db 13 | @hostname : String 14 | @port : Int32 15 | @exit = false 16 | 17 | macro success(results) 18 | {status: "success", results: {{results}}}.to_json 19 | end 20 | 21 | def self.logger 22 | @@logger ||= begin 23 | logger = Logger.new(STDOUT) 24 | logger.level = Logger::DEBUG 25 | logger 26 | end 27 | end 28 | 29 | def initialize(@db : Db, @hostname : String, @port : Int32) 30 | end 31 | 32 | def process(message : String) 33 | cmd = Messages::Command.from_json(message) 34 | 35 | case cmd.type 36 | when Messages::CommandType::Match 37 | success(@db.starts_with?(cmd.content)) 38 | when Messages::CommandType::AddPath 39 | if Dir.exists? cmd.content 40 | begin 41 | files = Generator.add_paths @db, [cmd.content] 42 | Server.logger.info "Path successfully added : #{cmd.content}, #{files} files processed" 43 | {status: "success", message: "#{files} files processed"}.to_json 44 | rescue e 45 | Server.logger.error "Failure parsing files in #{cmd.content} folder." 46 | {status: "error", message: e.to_s}.to_json 47 | end 48 | else 49 | {status: "error", message: "#{cmd.content} folder does not exists."}.to_json 50 | end 51 | when Messages::CommandType::Exit 52 | @exit = true 53 | Server.logger.info "Completion server shutting down.." 54 | {status: "success", message: "Completion server stoped"}.to_json 55 | when Messages::CommandType::Context 56 | res = @db.with_context cmd.content 57 | success res 58 | end 59 | end 60 | 61 | def run 62 | server = TCPServer.new(@hostname, @port) 63 | Server.logger.info "Listening on port #{@port}" 64 | loop do 65 | server.accept do |client| 66 | message = client.gets 67 | client << process(message) if message 68 | end 69 | break if @exit 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /src/cracker/version.cr: -------------------------------------------------------------------------------- 1 | module Cracker 2 | VERSION = "0.4.0" 3 | end 4 | -------------------------------------------------------------------------------- /src/cracker/visitor.cr: -------------------------------------------------------------------------------- 1 | require "compiler/crystal/syntax" 2 | 3 | module Cracker 4 | class Visitor < Crystal::Visitor 5 | def visit(any) 6 | false 7 | end 8 | end 9 | end 10 | --------------------------------------------------------------------------------