├── .gitignore ├── .travis.yml ├── FEATURES.md ├── LICENSE ├── README.md ├── arg_scanner ├── .gitignore ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── arg_scanner.gemspec ├── bin │ ├── arg-scanner │ ├── console │ ├── rubymine-type-tracker │ └── setup ├── ext │ └── arg_scanner │ │ ├── arg_scanner.c │ │ ├── arg_scanner.h │ │ └── extconf.rb ├── lib │ ├── arg_scanner.rb │ └── arg_scanner │ │ ├── options.rb │ │ ├── require_all.rb │ │ ├── starter.rb │ │ ├── state_tracker.rb │ │ ├── type_tracker.rb │ │ ├── version.rb │ │ └── workspace.rb ├── test │ ├── helper.rb │ ├── test_args_info.rb │ ├── test_call_info.rb │ └── test_state_tracker.rb └── util │ └── state_filter.rb ├── build.gradle ├── common ├── build.gradle └── src │ └── main │ └── java │ └── org │ └── jetbrains │ └── ruby │ └── codeInsight │ ├── Injector.kt │ ├── Logger.kt │ └── PrintToStdoutLogger.kt ├── contract-creator ├── build.gradle └── src │ └── org │ └── jetbrains │ └── ruby │ └── runtime │ └── signature │ └── server │ ├── SignatureServer.kt │ ├── SignatureServerInjector.kt │ └── serialisation │ └── ServerResponseBean.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ide-plugin ├── CHANGELOG.md ├── build.gradle ├── resources │ ├── META-INF │ │ └── plugin.xml │ ├── runWithTypeTracker.svg │ └── runWithTypeTracker_dark.svg └── src │ ├── com │ └── intellij │ │ └── execution │ │ └── executors │ │ ├── CollectStateExecutor.kt │ │ └── RunWithTypeTrackerExecutor.java │ ├── org │ └── jetbrains │ │ └── plugins │ │ └── ruby │ │ ├── IdePluginLogger.kt │ │ ├── PluginResourceUtil.java │ │ ├── RubyDynamicCodeInsightPluginInjector.kt │ │ ├── ancestorsextractor │ │ ├── AncestorsExtractor.kt │ │ └── RailsConsoleRunner.kt │ │ ├── ruby │ │ ├── actions │ │ │ ├── ExportAncestorsActions.kt │ │ │ ├── ExportAncesttorsDiffAction.kt │ │ │ ├── ExportFileActionBase.kt │ │ │ └── ImportExportContractsAction.kt │ │ ├── codeInsight │ │ │ ├── ProjectLifecycleListenerImpl.kt │ │ │ ├── RubyDynamicCodeInsightPluginAppLifecyctlListener.kt │ │ │ ├── TrackerDataLoader.kt │ │ │ ├── stateTracker │ │ │ │ ├── ClassHierarchySymbolProvider.kt │ │ │ │ └── RubyClassHierarchyWithCaching.kt │ │ │ ├── symbols │ │ │ │ └── structure │ │ │ │ │ └── RMethodSyntheticSymbol.java │ │ │ └── types │ │ │ │ ├── RubyCollectStateRunner.kt │ │ │ │ ├── RubyRunWithTypeTrackerRunner.kt │ │ │ │ └── RubyTypeProvider.kt │ │ ├── intentions │ │ │ ├── AddContractAnnotationIntention.java │ │ │ ├── BaseRubyMethodIntentionAction.kt │ │ │ └── RemoveCollectedInfoIntention.kt │ │ ├── persistent │ │ │ └── TypeInferenceDirectory.kt │ │ └── run │ │ │ └── configuration │ │ │ ├── CollectExecSettings.java │ │ │ └── RunWithTypeTrackerRunConfigurationExtension.java │ │ ├── settings │ │ ├── RubyTypeContractsConfigurable.kt │ │ ├── RubyTypeContractsConfigurableUI.kt │ │ └── RubyTypeContractsSettings.kt │ │ └── util │ │ └── SignatureServerUtil.kt │ └── test │ ├── java │ ├── CallStatCompletionTest.kt │ └── org │ │ └── jetbrains │ │ └── plugins │ │ └── ruby │ │ └── ruby │ │ └── actions │ │ └── ImportExportTests.kt │ └── testData │ ├── anonymous_module_method_call_test.rb │ ├── call_info_of_nested_class_test.rb │ ├── duplicates_in_callinfo_table_test.rb │ ├── forget_call_info_when_arguments_number_changed_test_part_1.rb │ ├── forget_call_info_when_arguments_number_changed_test_part_2.rb │ ├── in_project_root_test │ ├── gem_like.rb │ └── in_project_root_test.rb │ ├── merge_test1.rb │ ├── merge_test1_to_run.rb │ ├── merge_test2.rb │ ├── merge_test2_to_run.rb │ ├── method_without_parameters_test.rb │ ├── multiple_execution_test1.rb │ ├── multiple_execution_test2.rb │ ├── multiple_execution_test2_to_run.rb │ ├── ref_links_test.rb │ ├── ref_links_test_to_run.rb │ ├── ruby_exec_part_2.rb │ ├── ruby_exec_test.rb │ ├── sample_kw_test.rb │ ├── sample_kw_test_to_run.rb │ ├── sample_test.rb │ ├── sample_test_to_run.rb │ ├── save_types_between_launches_test_part_1.rb │ ├── save_types_between_launches_test_part_2.rb │ ├── simple_call_info_collection_test.rb │ ├── simple_call_info_collection_test_multiple_functions_test.rb │ ├── simple_call_info_collection_with_multiple_arguments_test.rb │ └── top_level_methods_call_info_collection_test.rb ├── ruby-call-signature ├── build.gradle └── src │ ├── main │ └── java │ │ └── org │ │ └── jetbrains │ │ └── ruby │ │ └── codeInsight │ │ └── types │ │ └── signature │ │ ├── CallInfo.kt │ │ ├── ClassInfo.kt │ │ ├── GemInfo.kt │ │ ├── MethodInfo.kt │ │ ├── ParameterInfo.java │ │ ├── RSignatureContract.java │ │ ├── RSignatureContractContainer.kt │ │ ├── RSignatureContractNode.java │ │ ├── RTuple.java │ │ ├── SignatureContract.kt │ │ ├── SignatureInfo.kt │ │ ├── contractTransition │ │ ├── ContractTransition.java │ │ ├── ReferenceContractTransition.java │ │ ├── TransitionHelper.java │ │ └── TypedContractTransition.java │ │ └── serialization │ │ ├── MethodInfoSerialization.kt │ │ ├── RmcDirectory.kt │ │ ├── SignatureContractSerialization.kt │ │ └── TestSerialization.kt │ └── test │ └── java │ └── org │ └── jetbrains │ └── ruby │ └── codeInsight │ └── types │ └── signature │ ├── GemInfoFromPathTest.kt │ ├── SignatureContractMergeTest.kt │ ├── SignatureContractSerializationTest.kt │ └── SignatureContractTestBase.kt ├── screenshots ├── parameter_type_providing.png ├── return_type_providing.png └── run_with_type_tracker.png ├── settings.gradle ├── signature-viewer ├── build.gradle └── src │ └── org │ └── jetbrains │ └── ruby │ └── runtime │ └── signature │ ├── DBViewer.kt │ ├── EraseLocation.kt │ ├── SignatureExport.kt │ ├── SignatureImport.kt │ ├── SignatureViewer.kt │ └── SplitDB.kt ├── state-tracker ├── build.gradle └── src │ ├── main │ └── java │ │ └── org │ │ └── jetbrains │ │ └── ruby │ │ └── stateTracker │ │ ├── RubyClassHierarchy.kt │ │ └── RubyClassHierarchyLoader.kt │ └── test │ └── java │ ├── org │ └── jetbrains │ │ └── ruby │ │ └── stateTracker │ │ ├── RubyClassHierarchyLoaderNonStandardModuleTypeTest.kt │ │ └── RubyClassHierarchyLoaderTest.kt │ └── testData │ ├── classes.json │ └── non-standard-module-type.json └── storage-server-api ├── build.gradle └── src ├── main └── java │ └── org │ └── jetbrains │ └── ruby │ └── codeInsight │ └── types │ ├── signature │ └── serialization │ │ └── BlobSerialization.kt │ └── storage │ └── server │ ├── DatabaseProvider.kt │ ├── RSignatureProvider.java │ ├── RSignatureStorage.java │ ├── StorageException.java │ ├── impl │ ├── IntIdTableWithPossibleDependency.kt │ ├── RSignatureProviderImpl.kt │ ├── RowConversions.kt │ └── Schema.kt │ └── testutil │ └── DatabaseTestUtils.kt └── test └── java └── org └── jetbrains └── ruby └── codeInsight └── types └── storage └── server └── impl └── RSignatureProviderTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | out/ 3 | */build/ 4 | .gradle 5 | 6 | .idea/ 7 | 8 | **/*.iml 9 | **/.rakeTasks 10 | arg_scanner/arg_scanner.iml 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | dist: trusty 3 | os: 4 | - linux 5 | # - osx 6 | 7 | rvm: 8 | - 2.3.3 9 | - 2.4.2 10 | - ruby-head 11 | 12 | matrix: 13 | fast_finish: true 14 | allow_failures: 15 | - rvm: ruby-head 16 | 17 | services: 18 | - mysql 19 | 20 | cache: 21 | directories: 22 | - $HOME/.gradle/caches/ 23 | - $HOME/.gradle/wrapper/ 24 | 25 | before_install: 26 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update ; fi 27 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install mysql; fi 28 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then mysql.server start; fi 29 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then mysql -u root -e "CREATE USER 'travis'@'127.0.0.1' IDENTIFIED BY '';"; fi 30 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then mysql -u root -e "FLUSH PRIVILEGES;"; fi 31 | - mysql -u root -e 'CREATE DATABASE ruby_type_contracts;' 32 | - mysql -u root -e 'GRANT ALL ON ruby_type_contracts.* TO 'travis'@'127.0.0.1';' 33 | - cd arg_scanner 34 | 35 | script: 36 | - gem install rake 37 | - rake test 38 | - rake install 39 | - cd .. 40 | - travis_wait 40 ./gradlew tasks 41 | - ./gradlew -Dmysql.user.name=travis -Dmysql.user.password="" test -------------------------------------------------------------------------------- /FEATURES.md: -------------------------------------------------------------------------------- 1 | # ruby-type-inference features 2 | 3 | This doc contains `ruby-type-inference` features which can be useful 4 | for you after running your ruby program under type tracker: 5 | 6 | ![Run with type tracker](screenshots/run_with_type_tracker.png) 7 | 8 | ## Type providing for method parameters 9 | 10 | ![Parameter type providing](screenshots/parameter_type_providing.png) 11 | 12 | 13 | ## Type providing for return value 14 | 15 | ![Return type providing](screenshots/return_type_providing.png) 16 | 17 | ## Side notes 18 | 19 | As now RubyMine has more information about types it can provide 20 | more reliable code completion, code analysis and other code insight features 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Automated Type Contracts Generation [![JetBrains incubator project](http://jb.gg/badges/incubator.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![Build Status](https://travis-ci.org/JetBrains/ruby-type-inference.svg?branch=master)](https://travis-ci.org/JetBrains/ruby-type-inference) 2 | =================================== 3 | 4 | `ruby-type-inference` project is a completely new approach to 5 | tackle the problems of Ruby dynamic nature and provide more reliable 6 | symbol resolution and type inference. It collects some run time data 7 | to build type contracts for the methods. 8 | 9 | Every time a method is being called, some arguments of 10 | particular types are being passed to it. Type Tracker collects 11 | all such argument combinations and then builds a special contract 12 | which satisfies all encountered argument type tuples. 13 | 14 | The approach has its own pros and cons: 15 | * The obtained contracts utilize real-world usages of code of 16 | any complexity so it provides true results even if a method 17 | utilizes dynamic Ruby features heavily. 18 | * The completeness of the contracts obtained for a method highly 19 | depends on the coverage of that method, including its callees. 20 | That implies the need to merge the data obtained from the 21 | different sources (e.g. different projects using the same gem). 22 | 23 | This implementation addresses the stated coverage problem by providing 24 | the possibility to merge any type contracts at any time. 25 | 26 | ## Usage 27 | 28 | For simple usage you need to install the [Ruby Dynamic Code Insight](https://plugins.jetbrains.com/plugin/10227-ruby-dynamic-code-insight) 29 | plugin for RubyMine. Then this plugin will require the [arg_scanner](https://rubygems.org/gems/arg_scanner) gem to be installed. 30 | See [arg_scanner installation instruction](arg_scanner/README.md#installation) if you have problems while installation. 31 | 32 | After that, you will have the possibility to run your programs under type tracker: 33 | 34 | ![Run with type tracker](screenshots/run_with_type_tracker.png) 35 | 36 | Or you can run your programs in terminal via the `rubymine-type-tracker` binary (But you have to keep your project opened 37 | in RubyMine). E.g.: 38 | ``` 39 | rubymine-type-tracker bin/rails server 40 | ``` 41 | 42 | The `rubymine-type-tracker` binary is included into the [arg_scanner](https://rubygems.org/gems/arg_scanner) gem. 43 | 44 | See [FEATURES.md](FEATURES.md) for understanding what benefits you will have after running your program under type tracker. 45 | 46 | ## Architecture 47 | 48 | * **arg_scanner** is a gem with a native extension to attach to 49 | ruby processes and trace and intercept all method calls to log 50 | type-wise data flow in runtime. 51 | 52 | See [`arg_scanner`] documentation for details on usage. 53 | 54 | * The [**type contract processor**](contract-creator) server listens for 55 | incoming type data (from `arg_scanner`) and processes it to a compact format. 56 | 57 | The data stored may be used later for better code analysis and also 58 | can be shared with other users. 59 | 60 | * Code analysis clients (a RubyMine/IJ+Ruby [plugin](ide-plugin)) use the contract data 61 | to provide features for the users such as code completion, better resolution, etc. 62 | 63 | * (_todo_) Signature server receives contracts anonymously from the users and provides 64 | a compiled contract collections for popular gems. 65 | 66 | ## Running project from sources 67 | 68 | #### Prerequisites 69 | 70 | The [`arg_scanner`] gem is used for collecting type information. It can be installed manually 71 | to the target SDK and requires MRI Ruby at least 2.3. 72 | 73 | #### Running type tracker 74 | 75 | There are two possibilities to use the type tracker: 76 | _(I)_ using IJ/RubyMine plugin or _(II)_ requiring it from Ruby code. 77 | 78 | ##### Using RubyMine plugin 79 | 80 | The easiest way to run the plugin (and the most convenient for its development) is 81 | running it with special gradle task against IJ Ultimate snapshot: 82 | 83 | ``` 84 | ./gradlew ide-plugin:runIde 85 | ``` 86 | 87 | The task will compile the plugin, run IJ Ultimate with plugin "installed" in it. 88 | There is no need in running anything manually in that case. 89 | 90 | If you want to try it with existing RubyMine instance, 91 | you should: 92 | 93 | 1. Build it via `./gradlew ide-plugin:buildPlugin` 94 | 2. Install plugin in the IDE 95 | * Navigate to `File | Settings | Plugins | Install plugin from disk...` 96 | * Locate plugin in `ide-plugin/build/distributions` and select. 97 | * Restart IDE. 98 | 99 | Note that due to API changes the plugin may be incompatible with older RM instances. 100 | 101 | ##### Using command line 102 | 103 | 1. In order to collect the data for the script needs a contract server to be up and running; 104 | it could be run by running 105 | ```sh 106 | ./gradlew contract-creator:runServer --args path-to-db.mv.db 107 | ``` 108 | where `path-to-db.mv.db` is path where type contracts will be stored (H2 database file). 109 | 110 | 1. Run the ruby script to be processed via [`arg-scanner`](arg_scanner/bin/arg-scanner) 111 | binary. 112 | 113 | 1. Use the data collected by the contract server. 114 | 115 | ## Contributions 116 | 117 | Any kind of ideas, use cases, contributions and questions are very welcome 118 | as the project is just incubating. 119 | Please feel free to create issues for any sensible request. 120 | 121 | [`arg_scanner`]: arg_scanner/README.md -------------------------------------------------------------------------------- /arg_scanner/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | 3 | .bundle/ 4 | .yardoc 5 | Gemfile.lock 6 | _yardoc/ 7 | coverage/ 8 | doc/ 9 | pkg/ 10 | spec/reports/ 11 | tmp/ 12 | *.bundle 13 | *.so 14 | *.o 15 | *.a 16 | mkmf.log 17 | -------------------------------------------------------------------------------- /arg_scanner/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in arg_scanner.gemspec 4 | gemspec 5 | 6 | group :test do 7 | gem 'test-unit' 8 | end 9 | -------------------------------------------------------------------------------- /arg_scanner/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 JetBrains 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 | -------------------------------------------------------------------------------- /arg_scanner/README.md: -------------------------------------------------------------------------------- 1 | # ArgScanner [![Gem Version](https://badge.fury.io/rb/arg_scanner.svg)](https://badge.fury.io/rb/arg_scanner) 2 | 3 | `arg_scanner` is a gem with the purpose to track all method calls and 4 | deliver the following information: 5 | 6 | * Method signature (arguments, their names and kinds) and declaration place 7 | * The types of argument variables given to each method call done 8 | 9 | This information can be used then to calculate and use type contracts 10 | for the analysed methods. 11 | 12 | `arg_scanner` is meant to be used as a binary to run any other ruby executable 13 | manually so including it in the `Gemfile` is not necessary. 14 | 15 | ## Installation 16 | 17 | The recommended way to install it is to execute command: 18 | 19 | ``` 20 | gem install arg_scanner 21 | ``` 22 | **You will possibly need to install [native dependencies](#dependencies)** 23 | 24 | ## Building from sources 25 | 26 | If you want to compile the gem from sources, just run the following commands: 27 | 28 | ``` 29 | bundle install 30 | bundle exec rake install 31 | ``` 32 | 33 | If you have problems with native extension compilation, make sure you have 34 | actual version of [ruby-core-source gem](https://github.com/os97673/debase-ruby_core_source) and 35 | have [native dependencies](#dependencies) installed. 36 | 37 | ## Dependencies 38 | 39 | ##### [Glib](https://developer.gnome.org/glib/) 40 | 41 | macOS: `brew install glib` 42 | Debian/Ubuntu: `sudo apt install libglib2.0-dev` 43 | Arch Linux: `sudo pacman -S glib2` 44 | 45 | ## Usage 46 | 47 | `arg_scanner` provides the `arg-scanner` binary which receives any number of 48 | arguments and executes the given command in type tracking mode, 49 | for example: 50 | 51 | ``` 52 | arg-scanner --type-tracker --pipe-file-path=[pipe_file_path] bundle exec rake spec 53 | ``` 54 | `pipe_file_path` here is path to pipe file which is printed by server's stdout 55 | 56 | ## Contributing 57 | 58 | Bug reports and pull requests are welcome on GitHub at https://github.com/JetBrains/ruby-type-inference 59 | 60 | ## License 61 | 62 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 63 | 64 | -------------------------------------------------------------------------------- /arg_scanner/Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/extensiontask" 3 | require 'rake/testtask' 4 | 5 | BASE_TEST_FILE_LIST = Dir['test/**/test_*.rb'] 6 | 7 | task :build => :compile 8 | 9 | Rake::ExtensionTask.new("arg_scanner") do |ext| 10 | ext.lib_dir = "lib/arg_scanner" 11 | end 12 | 13 | desc "Test arg_scanner." 14 | Rake::TestTask.new(:test => [:clean, :compile]) do |t| 15 | t.libs += %w(./ext ./lib) 16 | t.test_files = FileList[BASE_TEST_FILE_LIST] 17 | t.verbose = true 18 | end 19 | 20 | task :test => :lib 21 | 22 | task :default => [:clobber, :compile, :test] 23 | -------------------------------------------------------------------------------- /arg_scanner/arg_scanner.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'arg_scanner/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "arg_scanner" 8 | spec.version = ArgScanner::VERSION 9 | spec.authors = ["Nickolay Viuginov", "Valentin Fondaratov", "Vladimir Koshelev"] 10 | spec.email = ["viuginov.nickolay@gmail.com", "fondarat@gmail.com", "vkkoshelev@gmail.com"] 11 | 12 | spec.summary = %q{Program execution tracker to retrieve data types information} 13 | spec.homepage = "https://github.com/jetbrains/ruby-type-inference" 14 | spec.license = "MIT" 15 | 16 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 17 | # to allow pushing to a single host or delete this section to allow pushing to any host. 18 | # if spec.respond_to?(:metadata) 19 | # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'" 20 | # else 21 | # raise "RubyGems 2.0 or newer is required to protect against " \ 22 | # "public gem pushes." 23 | # end 24 | 25 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 26 | f.match(%r{^(test|spec|features)/}) 27 | end 28 | spec.bindir = "bin" 29 | spec.executables = spec.files.grep(%r{^bin/}) {|f| File.basename(f)} 30 | spec.require_paths = ["lib"] 31 | spec.extensions = ["ext/arg_scanner/extconf.rb"] 32 | 33 | spec.add_development_dependency "bundler", ">= 1.13" 34 | spec.add_development_dependency "rake", ">= 12.0" 35 | spec.add_development_dependency "rake-compiler" 36 | spec.add_dependency "debase-ruby_core_source", ">= 0.10.4" 37 | spec.add_dependency "native-package-installer", ">= 1.0.0" 38 | end 39 | -------------------------------------------------------------------------------- /arg_scanner/bin/arg-scanner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | require 'arg_scanner/options' 5 | require 'arg_scanner/version' 6 | 7 | options = ArgScanner::OPTIONS 8 | option_parser = OptionParser.new do |opts| 9 | opts.banner = "arg-scanner #{ArgScanner::VERSION}" + <<~EOB 10 | 11 | Usage: arg-scanner [OPTIONS] 12 | arg-scanner is a ruby script mediator supposed to be run from the command line or IDE. 13 | The data will be sent to a signature server so it must be running during arg-scanner execution. 14 | EOB 15 | 16 | opts.separator "Options:" 17 | opts.on("--type-tracker", "enable type tracker") do 18 | options.enable_type_tracker = true 19 | end 20 | opts.on("--state-tracker", "enable state tracker") do 21 | options.enable_state_tracker = true 22 | end 23 | 24 | opts.on("--no-type-tracker", "disable type tracker") do 25 | options.enable_type_tracker = false 26 | end 27 | opts.on("--no-state-tracker", "disable state tracker") do 28 | options.enable_state_tracker = false 29 | end 30 | 31 | opts.on("--output-dir=[Dir]", String, "specify output directory (ignored by type tracker)") do |dir| 32 | options.output_dir = dir 33 | end 34 | 35 | opts.on("--catch-only-every-N-call=[N]", Integer, "randomly catches only 1/N of all calls to speed up performance (by default N = 1)") do |n| 36 | options.catch_only_every_n_call = n 37 | end 38 | opts.on("--project-root=[PATH]", String, "Specify project's root directory to catch every call from this directory. "\ 39 | "Calls from other directories aren't guaranteed to be caught") do |path| 40 | options.project_root = path 41 | end 42 | 43 | opts.on("--pipe-file-path=[PATH]", String, "Specify pipe file path to connect to server") do |path| 44 | options.pipe_file_path = path 45 | end 46 | 47 | opts.on("--buffering", "enable buffering between arg-scanner and server. It speeds up arg-scanner but doesn't allow "\ 48 | "to use arg-scanner \"interactively\". Disabled by default") do |buffering| 49 | options.buffering = buffering 50 | end 51 | end 52 | 53 | begin 54 | option_parser.parse! ARGV 55 | rescue StandardError => e 56 | puts option_parser 57 | puts 58 | puts e.message 59 | exit 1 60 | end 61 | 62 | if ARGV.size < 1 63 | puts option_parser 64 | puts 65 | puts "Ruby program to trace must be specified." 66 | exit 1 67 | end 68 | 69 | options.set_env 70 | 71 | old_opts = ENV['RUBYOPT'] || '' 72 | starter = "-r #{File.expand_path(File.dirname(__FILE__))}/../lib/arg_scanner/starter" 73 | unless old_opts.include? starter 74 | ENV['RUBYOPT'] = starter 75 | ENV['RUBYOPT'] += " #{old_opts}" if old_opts != '' 76 | end 77 | 78 | $0 = ARGV[0] 79 | Kernel.exec *ARGV 80 | -------------------------------------------------------------------------------- /arg_scanner/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "arg_scanner" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /arg_scanner/bin/rubymine-type-tracker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This is small script for launching type tracker under RubyMine's provided server. Acts like arg-scanner wrapper 3 | require 'optparse' 4 | require 'arg_scanner/version' 5 | require 'tmpdir' 6 | require 'json' 7 | 8 | option_parser = OptionParser.new do |opts| 9 | opts.banner = <<~EOB 10 | rubymine-type-tracker #{ArgScanner::VERSION} 11 | 12 | Usage: rubymine-type-tracker 13 | rubymine-type-tracker is a ruby script for easy launching some command under 14 | RubyMine's type tracker. The data will be sent to a server run by RubyMine. 15 | So before launching this script be sure project is opened in RubyMine with 16 | "Ruby Dynamic Code Insight" plugin installed. 17 | EOB 18 | end 19 | 20 | begin 21 | option_parser.parse! ARGV 22 | if ARGV.size == 0 23 | raise StandardError.new("") 24 | end 25 | rescue StandardError => e 26 | puts option_parser 27 | exit 1 28 | end 29 | 30 | dot_ruby_type_inference_dir = File.join(Dir.tmpdir, ".ruby-type-inference") 31 | if File.directory?(dot_ruby_type_inference_dir) 32 | match_jsons = Dir.foreach(dot_ruby_type_inference_dir).map do |file_name| 33 | if file_name == '.' || file_name == '..' 34 | next nil 35 | end 36 | json = JSON.parse(IO.read(File.join(dot_ruby_type_inference_dir, file_name))) 37 | if json["projectPath"] != Dir.pwd 38 | next nil 39 | end 40 | next json 41 | end.select { |x| x != nil } 42 | else 43 | match_jsons = [] 44 | end 45 | 46 | if match_jsons.count == 1 47 | json = match_jsons[0] 48 | elsif match_jsons.count > 1 49 | STDERR.puts <<~EOB 50 | Critical error! You may try to:\n 51 | 1. Close RubyMine 52 | 2. Clean #{dot_ruby_type_inference_dir} 53 | 3. Open RubyMine 54 | EOB 55 | exit 1 56 | elsif match_jsons.count == 0 57 | STDERR.puts <<~EOB 58 | Error! You are possibly... 59 | * launching this script under directory different from project 60 | opened in RubyMine (please `cd` to dir firstly) 61 | * haven't opened project in RubyMine 62 | * haven't installed "Ruby Dynamic Code Insight" plugin in RubyMine 63 | EOB 64 | exit 1 65 | end 66 | 67 | to_exec = ["arg-scanner", 68 | "--type-tracker", 69 | "--project-root=#{json["projectPath"]}", 70 | "--pipe-file-path=#{json["pipeFilePath"]}", 71 | *ARGV] 72 | 73 | Kernel.exec(*to_exec) 74 | -------------------------------------------------------------------------------- /arg_scanner/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /arg_scanner/ext/arg_scanner/arg_scanner.h: -------------------------------------------------------------------------------- 1 | #ifndef ARG_SCANNER_H 2 | #define ARG_SCANNER_H 1 3 | 4 | #include "ruby.h" 5 | #include "vm_core.h" 6 | #include "version.h" 7 | #include "iseq.h" 8 | #include "method.h" 9 | 10 | #endif /* ARG_SCANNER_H */ 11 | -------------------------------------------------------------------------------- /arg_scanner/ext/arg_scanner/extconf.rb: -------------------------------------------------------------------------------- 1 | require "mkmf" 2 | 3 | RbConfig::MAKEFILE_CONFIG['CC'] = ENV['CC'] if ENV['CC'] 4 | 5 | require "debase/ruby_core_source" 6 | require "native-package-installer" 7 | 8 | class NilClass 9 | def empty?; true; end 10 | end 11 | 12 | # Just a replacement of have_header because have_header searches not recursively :( 13 | def real_have_header(header_name) 14 | if (have_header(header_name)) 15 | return true 16 | end 17 | yes_msg = "checking for #{header_name}... yes" 18 | no_msg = "checking for #{header_name}... no" 19 | 20 | include_env = ENV["C_INCLUDE_PATH"] 21 | if !include_env.empty? && !Dir.glob("#{include_env}/**/#{header_name}").empty? 22 | puts yes_msg 23 | return true 24 | end 25 | if !Dir.glob("/usr/include/**/#{header_name}").empty? 26 | puts yes_msg 27 | return true 28 | end 29 | puts no_msg 30 | return false 31 | end 32 | 33 | if !real_have_header('glib.h') && 34 | !NativePackageInstaller.install(:alt_linux => "glib2-devel", 35 | :debian => "libglib2.0-dev", 36 | :redhat => "glib2-devel", 37 | :arch_linux => "glib2", 38 | :homebrew => "glib", 39 | :macports => "glib2", 40 | :msys2 => "glib2") 41 | exit(false) 42 | end 43 | 44 | hdrs = proc { 45 | have_header("vm_core.h") and 46 | have_header("iseq.h") and 47 | have_header("version.h") and 48 | have_header("vm_core.h") and 49 | have_header("vm_insnhelper.h") and 50 | have_header("vm_core.h") and 51 | have_header("method.h") 52 | } 53 | 54 | # Allow use customization of compile options. For example, the 55 | # following lines could be put in config_options to to turn off 56 | # optimization: 57 | # $CFLAGS='-fPIC -fno-strict-aliasing -g3 -ggdb -O2 -fPIC' 58 | config_file = File.join(File.dirname(__FILE__), 'config_options.rb') 59 | load config_file if File.exist?(config_file) 60 | 61 | if ENV['debase_debug'] 62 | $CFLAGS+=' -Wall -Werror -g3' 63 | end 64 | 65 | $CFLAGS += ' `pkg-config --cflags --libs glib-2.0`' 66 | $DLDFLAGS += ' `pkg-config --cflags --libs glib-2.0`' 67 | 68 | dir_config("ruby") 69 | if !Debase::RubyCoreSource.create_makefile_with_core(hdrs, "arg_scanner/arg_scanner") 70 | STDERR.print("Makefile creation failed\n") 71 | STDERR.print("*************************************************************\n\n") 72 | STDERR.print(" NOTE: If your headers were not found, try passing\n") 73 | STDERR.print(" --with-ruby-include=PATH_TO_HEADERS \n\n") 74 | STDERR.print("*************************************************************\n\n") 75 | exit(1) 76 | end -------------------------------------------------------------------------------- /arg_scanner/lib/arg_scanner.rb: -------------------------------------------------------------------------------- 1 | require "arg_scanner/version" 2 | require "arg_scanner/arg_scanner" 3 | require "arg_scanner/type_tracker" 4 | require "arg_scanner/state_tracker" 5 | 6 | module ArgScanner 7 | # Your code goes here... 8 | end 9 | -------------------------------------------------------------------------------- /arg_scanner/lib/arg_scanner/options.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module ArgScanner 4 | OPTIONS = OpenStruct.new( 5 | :enable_type_tracker => ENV['ARG_SCANNER_ENABLE_TYPE_TRACKER'], 6 | :enable_state_tracker => ENV['ARG_SCANNER_ENABLE_STATE_TRACKER'], 7 | :output_directory => ENV['ARG_SCANNER_DIR'], 8 | :catch_only_every_n_call => ENV['ARG_SCANNER_CATCH_ONLY_EVERY_N_CALL'] || 1, 9 | :project_root => ENV['ARG_SCANNER_PROJECT_ROOT'], 10 | :pipe_file_path => ENV['ARG_SCANNER_PIPE_FILE_PATH'] || '', 11 | :buffering => ENV['ARG_SCANNER_BUFFERING'] 12 | ) 13 | 14 | def OPTIONS.set_env 15 | ENV['ARG_SCANNER_ENABLE_TYPE_TRACKER'] = self.enable_type_tracker ? "1" : nil 16 | ENV['ARG_SCANNER_ENABLE_STATE_TRACKER'] = self.enable_state_tracker ? "1" : nil 17 | ENV['ARG_SCANNER_DIR'] = self.output_directory 18 | ENV['ARG_SCANNER_CATCH_ONLY_EVERY_N_CALL'] = self.catch_only_every_n_call.to_s 19 | ENV['ARG_SCANNER_PROJECT_ROOT'] = self.project_root 20 | ENV['ARG_SCANNER_PIPE_FILE_PATH'] = self.pipe_file_path 21 | ENV['ARG_SCANNER_BUFFERING'] = self.buffering ? "1" : nil 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /arg_scanner/lib/arg_scanner/starter.rb: -------------------------------------------------------------------------------- 1 | # starter.rb is loaded with "ruby -r" option from bin/arg-scanner 2 | # or by IDEA also with "ruby -r" option 3 | 4 | unless ENV["ARG_SCANNER_ENABLE_STATE_TRACKER"].nil? 5 | require_relative 'state_tracker' 6 | ArgScanner::StateTracker.new 7 | end 8 | 9 | unless ENV["ARG_SCANNER_ENABLE_TYPE_TRACKER"].nil? 10 | require_relative 'arg_scanner' 11 | require_relative 'type_tracker' 12 | 13 | # instantiating type tracker will enable calls tracing and sending the data 14 | ArgScanner::TypeTracker.instance 15 | end 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /arg_scanner/lib/arg_scanner/state_tracker.rb: -------------------------------------------------------------------------------- 1 | require "set" 2 | require_relative "require_all" 3 | require_relative "workspace" 4 | 5 | 6 | module ArgScanner 7 | class StateTracker 8 | def initialize 9 | @workspace = Workspace.new 10 | @workspace.on_process_start 11 | at_exit do 12 | begin 13 | require_extra_libs 14 | @workspace.open_output_json("classes") { |file| print_json(file) } 15 | ensure 16 | @workspace.on_process_exit 17 | end 18 | end 19 | end 20 | 21 | private 22 | def require_extra_libs 23 | begin 24 | RequireAll.require_all Rails.root.join('lib') 25 | rescue Exception => e 26 | end 27 | begin 28 | Rails.application.eager_load! 29 | rescue Exception => e 30 | end 31 | end 32 | 33 | def print_json(file) 34 | result = { 35 | :top_level_constants => parse_top_level_constants, 36 | :modules => modules_to_json, 37 | :load_path => $: 38 | } 39 | require "json" 40 | file.puts(JSON.dump(result)) 41 | end 42 | 43 | def parse_top_level_constants 44 | Module.constants.select { |const| Module.const_defined?(const)}.map do |const| 45 | begin 46 | value = Module.const_get(const) 47 | (!value.is_a? Module) ? { 48 | :name => const, 49 | :class_name => value.class, 50 | :extended => get_extra_methods(value)} : nil 51 | rescue Exception => e 52 | end 53 | end.compact 54 | end 55 | 56 | def get_extra_methods(value) 57 | value.methods - value.public_methods 58 | end 59 | 60 | def method_to_json(method) 61 | ret = { 62 | :name => method.name, 63 | :parameters => method.parameters 64 | } 65 | unless method.source_location.nil? 66 | ret[:path] = method.source_location[0] 67 | ret[:line] = method.source_location[1] 68 | end 69 | ret 70 | rescue Exception => e 71 | nil 72 | end 73 | 74 | def module_to_json(mod) 75 | ret = { 76 | :name => mod.to_s, 77 | :type => mod.class.to_s, 78 | :singleton_class_ancestors => mod.singleton_class.ancestors.map{|it| it.to_s}, 79 | :ancestors => mod.ancestors.map{|it| it.to_s}, # map to_s is needed because for example "Psych" parsed not correctly into JSON format 80 | # it's parsed as: "{}\n" check it by launching in rails console: "JSON.generate(Psych)" 81 | :class_methods => mod.methods(false).map {|method| method_to_json(mod.method(method))}.compact, 82 | :instance_methods => mod.instance_methods(false).map {|method| method_to_json(mod.instance_method(method))}.compact 83 | } 84 | ret[:superclass] = mod.superclass if mod.is_a? Class 85 | ret 86 | rescue Exception => e 87 | nil 88 | end 89 | 90 | def modules_to_json 91 | ObjectSpace.each_object(Module).map {|mod| module_to_json(mod)} 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /arg_scanner/lib/arg_scanner/type_tracker.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | require 'socket' 3 | require 'singleton' 4 | require 'thread' 5 | 6 | require_relative 'options' 7 | 8 | module ArgScanner 9 | 10 | class TypeTrackerPerformanceMonitor 11 | def initialize 12 | @enable_debug = ENV["ARG_SCANNER_DEBUG"] 13 | @call_counter = 0 14 | @handled_call_counter = 0 15 | @submitted_call_counter = 0 16 | @old_handled_call_counter = 0 17 | @time = Time.now 18 | end 19 | 20 | 21 | def on_call 22 | @submitted_call_counter += 1 23 | end 24 | 25 | def on_return 26 | @call_counter += 1 27 | 28 | if enable_debug && call_counter % 100000 == 0 29 | $stderr.puts("calls #{call_counter} handled #{handled_call_counter} submitted #{submitted_call_counter}"\ 30 | "delta #{handled_call_counter - old_handled_call_counter} time #{Time.now - @time}") 31 | @old_handled_call_counter = handled_call_counter 32 | @time = Time.now 33 | end 34 | end 35 | 36 | def on_handled_return 37 | @handled_call_counter += 1 38 | end 39 | 40 | private 41 | 42 | attr_accessor :submitted_call_counter 43 | attr_accessor :handled_call_counter 44 | attr_accessor :old_handled_call_counter 45 | attr_accessor :call_counter 46 | attr_accessor :enable_debug 47 | 48 | end 49 | 50 | 51 | class TypeTracker 52 | include Singleton 53 | 54 | def initialize 55 | ArgScanner.init(ENV['ARG_SCANNER_PIPE_FILE_PATH'], ENV['ARG_SCANNER_BUFFERING'], 56 | ENV['ARG_SCANNER_PROJECT_ROOT'], ENV['ARG_SCANNER_CATCH_ONLY_EVERY_N_CALL']) 57 | 58 | @enable_debug = ENV["ARG_SCANNER_DEBUG"] 59 | @performance_monitor = if @enable_debug then TypeTrackerPerformanceMonitor.new else nil end 60 | 61 | TracePoint.trace(:call, &ArgScanner.method(:handle_call)) 62 | 63 | TracePoint.trace(:return, &ArgScanner.method(:handle_return)) 64 | 65 | error_msg = ArgScanner.check_if_arg_scanner_ready() 66 | if error_msg != nil 67 | STDERR.puts error_msg 68 | Process.exit(1) 69 | end 70 | 71 | ObjectSpace.define_finalizer(self, proc { ArgScanner.destructor() }) 72 | end 73 | 74 | attr_accessor :enable_debug 75 | attr_accessor :performance_monitor 76 | attr_accessor :prefix 77 | 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /arg_scanner/lib/arg_scanner/version.rb: -------------------------------------------------------------------------------- 1 | module ArgScanner 2 | VERSION = "0.3.3" 3 | end 4 | -------------------------------------------------------------------------------- /arg_scanner/lib/arg_scanner/workspace.rb: -------------------------------------------------------------------------------- 1 | module ArgScanner 2 | class Workspace 3 | 4 | def initialize 5 | @dir = ENV["ARG_SCANNER_DIR"] || "." 6 | @pid_file = @dir+"/#{Process.pid}.pid" 7 | end 8 | 9 | def on_process_start 10 | File.open(@pid_file, "w") {} 11 | end 12 | 13 | 14 | def open_output_json(prefix) 15 | path = @dir + "/#{prefix}-#{Time.now.strftime('%Y-%m-%d_%H-%M-%S')}-#{Process.pid}.json" 16 | path_tmp_name = path + ".temp" 17 | File.open(path_tmp_name, "w") { |file| yield file } 18 | require 'fileutils' 19 | FileUtils.mv(path_tmp_name, path) 20 | end 21 | 22 | def on_process_exit 23 | require 'fileutils' 24 | FileUtils.rm(@pid_file) 25 | end 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /arg_scanner/test/helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__dir__) + '/../lib') 2 | require "test-unit" 3 | require "arg_scanner" 4 | 5 | class TestTypeTracker 6 | include Singleton 7 | 8 | attr_reader :last_args_info 9 | attr_reader :last_call_info 10 | 11 | def initialize 12 | @tp = TracePoint.new(:call, :return) do |tp| 13 | case tp.event 14 | when :call 15 | ArgScanner.handle_call(tp) 16 | 17 | @last_args_info = ArgScanner.get_args_info.split ';' 18 | @last_call_info = ArgScanner.get_call_info 19 | when :return 20 | ArgScanner.handle_return(tp) 21 | end 22 | end 23 | end 24 | 25 | def enable(*args, &b) 26 | @tp.enable *args, &b 27 | end 28 | 29 | def signatures 30 | Thread.current[:signatures] ||= Array.new 31 | end 32 | 33 | end -------------------------------------------------------------------------------- /arg_scanner/test/test_args_info.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.expand_path("helper", File.dirname(__FILE__)) 3 | require 'date' 4 | 5 | class TestArgsInfoWrapper 6 | 7 | def foo(a); end 8 | 9 | def foo2(a, b = 1); end 10 | 11 | def foo3(**rest); end 12 | 13 | def foo4(kw: :symbol, **rest1); end 14 | 15 | def foo5(kw:, **rest); end 16 | 17 | def foo6(a, *rest, b); end 18 | 19 | def initialize 20 | 21 | # @trace = TracePoint.new(:call) do |tp| 22 | # case tp.event 23 | # when :call 24 | # tp.binding.local_variables.each { |v| p tp.binding.eval v.to_s } 25 | # ArgScanner.handle_call(tp.lineno, tp.method_id, tp.path) 26 | # @args_info = ArgScanner.get_args_info 27 | # p @args_info 28 | # end 29 | # end 30 | end 31 | end 32 | 33 | class TestArgsInfo < Test::Unit::TestCase 34 | 35 | # @!attribute [r] type_tracker 36 | # @return [TestTypeTracker] 37 | attr_reader :type_tracker 38 | 39 | 40 | def setup 41 | @args_info_wrapper = TestArgsInfoWrapper.new 42 | @type_tracker = TestTypeTracker.instance 43 | end 44 | 45 | def teardown 46 | 47 | end 48 | 49 | def test_simple_kwrest 50 | type_tracker.enable do 51 | @args_info_wrapper.foo3(a: Date.new, kkw: 'hi') 52 | end 53 | 54 | assert_equal ["KEYREST,Date,a", "KEYREST,String,kkw"], type_tracker.last_args_info 55 | end 56 | 57 | def test_empty_kwrest 58 | type_tracker.enable do 59 | @args_info_wrapper.foo3() 60 | end 61 | 62 | assert_equal [], type_tracker.last_args_info 63 | end 64 | 65 | def test_req_and_opt_arg 66 | type_tracker.enable do 67 | @args_info_wrapper.foo2(Date.new) 68 | end 69 | 70 | assert_equal "REQ,Date,a", type_tracker.last_args_info[0] 71 | assert type_tracker.last_args_info[1] == "OPT,Fixnum,b" || type_tracker.last_args_info[1] == "OPT,Integer,b" 72 | end 73 | 74 | def test_optkw_and_empty_kwrest 75 | type_tracker.enable do 76 | @args_info_wrapper.foo4(kw: Date.new) 77 | end 78 | 79 | assert_equal ["KEY,Date,kw"], type_tracker.last_args_info 80 | end 81 | 82 | def test_reqkw_and_empty_kwrest 83 | type_tracker.enable do 84 | @args_info_wrapper.foo5(kw: Date.new) 85 | end 86 | 87 | assert_equal ["KEYREQ,Date,kw"], type_tracker.last_args_info 88 | end 89 | 90 | def test_reqkw_and_kwrest 91 | type_tracker.enable do 92 | @args_info_wrapper.foo5(kw: Date.new, aa: true, bb: '1') 93 | end 94 | 95 | assert_equal ["KEYREQ,Date,kw", "KEYREST,TrueClass,aa", "KEYREST,String,bb"], type_tracker.last_args_info 96 | end 97 | 98 | def test_optkw_and_kwrest 99 | type_tracker.enable do 100 | @args_info_wrapper.foo4(aa: :symbol, bb: '1') 101 | end 102 | 103 | assert_equal ["KEYREST,Symbol,aa", "KEYREST,String,bb"], type_tracker.last_args_info 104 | end 105 | 106 | def test_optkw_passed_and_kwrest 107 | type_tracker.enable do 108 | @args_info_wrapper.foo4(kw: 'bla-bla', aa: :symbol, bb: '1') 109 | end 110 | 111 | assert_equal ["KEY,String,kw", "KEYREST,Symbol,aa", "KEYREST,String,bb"], type_tracker.last_args_info 112 | end 113 | 114 | def test_rest 115 | type_tracker.enable do 116 | @args_info_wrapper.foo6(1, 'hi', Date.new, '1') 117 | end 118 | 119 | assert type_tracker.last_args_info[0] == "REQ,Fixnum,a" || type_tracker.last_args_info[0] == "REQ,Integer,a" 120 | assert type_tracker.last_args_info[1] == "REST,Array,rest" 121 | assert type_tracker.last_args_info[2] == "POST,String,b" 122 | end 123 | 124 | def test_empty_rest 125 | type_tracker.enable do 126 | @args_info_wrapper.foo6(1, '1') 127 | end 128 | 129 | assert type_tracker.last_args_info[0] == "REQ,Fixnum,a" || type_tracker.last_args_info[0] == "REQ,Integer,a" 130 | assert type_tracker.last_args_info[1] == "REST,Array,rest" 131 | assert type_tracker.last_args_info[2] == "POST,String,b" 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /arg_scanner/test/test_call_info.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.expand_path("helper", File.dirname(__FILE__)) 3 | 4 | class TestCallInfoWrapper 5 | 6 | def sqr(z1 = 10, z2 = 11, z3 = 13, z4 = 14, z5, z6, z7, z8, y: '0', x: "40") 7 | 8 | end 9 | 10 | def sqr2(z0, z1 = 2, z2 = 10, z3 = 2, z4 = 0, y: 1, x: 30, z: '40') 11 | 12 | end 13 | 14 | def foo(a, b, c, *d, e) 15 | 16 | end 17 | 18 | def foo2(*args) 19 | 20 | end 21 | 22 | def foo3(b: 2, c: '3', **args) 23 | 24 | end 25 | 26 | def foo4(b: 2, c:, d: "1", dd: 1, ddd: '111', **args) 27 | 28 | end 29 | 30 | def foo5(b) 31 | 32 | end 33 | 34 | end 35 | 36 | class TestCallInfo < Test::Unit::TestCase 37 | 38 | # @!attribute [r] type_tracker 39 | # @return [TestTypeTracker] 40 | attr_reader :type_tracker 41 | 42 | def setup 43 | @call_info_wrapper = TestCallInfoWrapper.new 44 | @type_tracker = TestTypeTracker.instance 45 | end 46 | 47 | def teardown 48 | 49 | end 50 | 51 | def test_simple 52 | type_tracker.enable do 53 | @call_info_wrapper.sqr2(10, 11) 54 | end 55 | 56 | assert_not_nil type_tracker.last_call_info 57 | #assert type_tracker.last_call_info.size == 2 58 | #assert type_tracker.last_call_info[0] == "sqr2" 59 | assert_equal 2, type_tracker.last_call_info[0] 60 | end 61 | 62 | def test_simple_req_arg 63 | type_tracker.enable do 64 | @call_info_wrapper.foo5(10) 65 | end 66 | 67 | assert_nil type_tracker.last_call_info 68 | end 69 | 70 | def test_simple_kw 71 | type_tracker.enable do 72 | @call_info_wrapper.sqr2(10, 11, x: 10, y: 1) 73 | end 74 | 75 | assert_not_nil type_tracker.last_call_info 76 | #assert type_tracker.last_call_info.size == 3 77 | #assert type_tracker.last_call_info[0] == "sqr2" 78 | assert_equal 4, type_tracker.last_call_info[0] 79 | assert_equal "x,y", type_tracker.last_call_info[1] 80 | end 81 | 82 | def test_rest 83 | type_tracker.enable do 84 | @call_info_wrapper.foo2(1, 2, 3, 4, 5, 6, 7, 8) 85 | end 86 | 87 | assert_not_nil type_tracker.last_call_info 88 | #assert type_tracker.last_call_info.size == 2 89 | #assert type_tracker.last_call_info[0] == "foo2" 90 | assert_equal 8, type_tracker.last_call_info[0] 91 | end 92 | 93 | def test_post_and_rest 94 | type_tracker.enable do 95 | @call_info_wrapper.foo(1, 2, 3, 4, 5, 6, 7, 8) 96 | end 97 | 98 | #coz it is obvious that all the arguments were passed (they are all required) 99 | assert_not_nil type_tracker.last_call_info 100 | #assert type_tracker.last_call_info.size == 2 101 | #assert type_tracker.last_call_info[0] == "foo" 102 | #assert type_tracker.last_call_info[0] == 8 103 | end 104 | 105 | def test_kwrest 106 | type_tracker.enable do 107 | @call_info_wrapper.foo3(a: 1, b: 2, c: 3, d: 4) 108 | end 109 | 110 | assert_not_nil type_tracker.last_call_info 111 | #assert type_tracker.last_call_info.size == 3 112 | #assert type_tracker.last_call_info[0] == "foo3" 113 | assert_equal 4, type_tracker.last_call_info[0] 114 | assert_equal "a,b,c,d", type_tracker.last_call_info[1] 115 | end 116 | 117 | def test_rest_and_reqkw_args 118 | type_tracker.enable do 119 | @call_info_wrapper.foo4(b: "hello", c: 'world', e: 1, f: "not") 120 | end 121 | 122 | assert_not_nil type_tracker.last_call_info 123 | #assert type_tracker.last_call_info.size == 3 124 | #assert type_tracker.last_call_info[0] == "foo4" 125 | assert_equal 4, type_tracker.last_call_info[0] 126 | assert_equal "b,c,e,f", type_tracker.last_call_info[1] 127 | 128 | end 129 | end -------------------------------------------------------------------------------- /arg_scanner/test/test_state_tracker.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'tempfile' 3 | require 'fileutils' 4 | require 'json' 5 | 6 | class StateTrackerTest < Test::Unit::TestCase 7 | 8 | class << self 9 | #Runs only once at start 10 | def startup 11 | file = Tempfile.new("StateTracker") 12 | dirname = file.path 13 | FileUtils.rm(dirname) 14 | file.close 15 | begin 16 | FileUtils.makedirs(dirname) 17 | system("echo exit | ARG_SCANNER_DIR=\"#{dirname}\" ARG_SCANNER_ENABLE_STATE_TRACKER=\"1\" irb -r\"#{File.dirname(__dir__)}/lib/arg_scanner/starter.rb\" 2> /dev/null") 18 | files = Dir["#{dirname}/*.json"] 19 | @@json = JSON.parse(File.read(files[0])) 20 | ensure 21 | FileUtils.rm_rf(dirname) 22 | end 23 | end 24 | end 25 | 26 | def test_has_struct 27 | assert_not_nil(get_class_with_name("Struct")) 28 | end 29 | 30 | def test_symbol_is_fine 31 | symbol = get_class_with_name("Symbol") 32 | assert_not_nil(symbol) 33 | assert_equal(symbol["type"], "Class") 34 | assert_equal(symbol["superclass"], "Object") 35 | assert_not_nil(symbol["singleton_class_ancestors"].find_index("Kernel")) 36 | assert_not_nil(symbol["ancestors"].find_index("Comparable")) 37 | assert_not_nil(get_class_method(symbol, "all_symbols")) 38 | assert_not_nil(get_instance_method(symbol, "match")) 39 | parameters = get_instance_method(symbol, "match")['parameters'] 40 | assert_not_nil(parameters) 41 | assert_equal(parameters[0][0], (RUBY_VERSION < "2.4.0") ? "req" : "rest") 42 | end 43 | 44 | def test_loaded_path_is_fine 45 | assert_not_nil(@@json["load_path"]) 46 | assert_not_nil(@@json["load_path"][0]) 47 | end 48 | 49 | def test_constant_is_fine 50 | assert_not_nil(@@json["top_level_constants"]) 51 | assert_not_nil(@@json["top_level_constants"][0]) 52 | assert_not_nil(@@json["top_level_constants"][0]["name"]) 53 | assert_not_nil(@@json["top_level_constants"][0]["class_name"]) 54 | assert_not_nil(@@json["top_level_constants"][0]["extended"]) 55 | end 56 | 57 | private 58 | 59 | def get_class_method(symbol, name) 60 | get_named_entity(symbol, "class_methods", name) 61 | end 62 | 63 | def get_instance_method(symbol, name) 64 | get_named_entity(symbol, "instance_methods", name) 65 | end 66 | 67 | def get_class_with_name(name) 68 | get_named_entity(@@json, "modules", name) 69 | end 70 | 71 | def get_named_entity(obj, index, name) 72 | obj[index].find {|entity| entity["name"] == name} 73 | end 74 | 75 | end 76 | -------------------------------------------------------------------------------- /arg_scanner/util/state_filter.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'json' 4 | require 'set' 5 | 6 | if ARGV.length < 3 7 | puts("state_filter.rb [ output_modules, :load_path => json["load_path"]})) 36 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 9 | classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7" 10 | classpath 'org.apache.httpcomponents:httpclient:4.5.2' 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | mavenCentral() 17 | maven { 18 | url 'https://dl.bintray.com/kotlin/exposed' 19 | } 20 | } 21 | 22 | apply plugin: 'java' 23 | apply plugin: 'kotlin' 24 | 25 | def project = it 26 | dependencies { 27 | if (project.name != 'ide-plugin') { 28 | compile 'org.jetbrains:annotations:15.0' 29 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 30 | compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 31 | } 32 | 33 | testCompile 'junit:junit:4.12' 34 | testCompile 'com.h2database:h2:1.4.193' 35 | } 36 | 37 | compileKotlin { 38 | kotlinOptions.jvmTarget = "1.8" 39 | } 40 | 41 | test { 42 | systemProperties System.properties 43 | testLogging { 44 | exceptionFormat = 'full' 45 | } 46 | } 47 | } 48 | 49 | task wrapper(type: Wrapper) { 50 | gradleVersion = '4.10.2' 51 | } 52 | 53 | subprojects { 54 | if (it.name in ['storage-server-api', 'lambda-update-handler', 'lambda-put-handler', 'contract-creator', 'state-tracker', 'ide-plugin']) { 55 | dependencies { 56 | compile 'com.google.code.gson:gson:2.8.0' 57 | } 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | 6 | dependencies { 7 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 8 | } 9 | } 10 | 11 | dependencies { 12 | } 13 | 14 | sourceSets { 15 | main.java.srcDirs = ['src/main/java'] 16 | main.kotlin.srcDirs = ['src/main/java'] 17 | 18 | test.kotlin.srcDirs = ['src/test/java'] 19 | } 20 | -------------------------------------------------------------------------------- /common/src/main/java/org/jetbrains/ruby/codeInsight/Injector.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight 2 | 3 | /** 4 | * Dependency injection mechanism 5 | */ 6 | interface Injector { 7 | fun getLogger(cl: Class): Logger 8 | } 9 | 10 | @Volatile 11 | private var _injector: Injector? = null 12 | val injector: Injector 13 | get() { 14 | return _injector ?: throw IllegalStateException("Injector must be initialized before any usage") 15 | } 16 | 17 | // Because the we don't know anything about injector initializators we assume that it can be 18 | // potentially multi threaded but necessity of injector initialization thread safety isn't really investigated 19 | @Synchronized 20 | fun initInjector(injector: Injector) { 21 | check(_injector == null) { 22 | "Injector must be initialized only once" 23 | } 24 | _injector = injector 25 | } 26 | -------------------------------------------------------------------------------- /common/src/main/java/org/jetbrains/ruby/codeInsight/Logger.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight 2 | 3 | interface Logger { 4 | fun info(msg: String) 5 | } 6 | -------------------------------------------------------------------------------- /common/src/main/java/org/jetbrains/ruby/codeInsight/PrintToStdoutLogger.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.* 5 | 6 | private val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") 7 | 8 | /** 9 | * Most basic [Logger] implementation 10 | */ 11 | class PrintToStdoutLogger(private val category: String) : Logger { 12 | constructor(cl : Class<*>) : this(cl.name) 13 | 14 | override fun info(msg: String) { 15 | println("${format.format(Calendar.getInstance())} [$category] $msg") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contract-creator/build.gradle: -------------------------------------------------------------------------------- 1 | sourceSets { 2 | main.java.srcDirs = ['src'] 3 | } 4 | 5 | dependencies { 6 | compile project(':common') 7 | compile project(':ruby-call-signature') 8 | compile project(':storage-server-api') 9 | 10 | // compile 'com.h2database:h2:1.4.193' 11 | } 12 | 13 | task runServer(type: JavaExec) { 14 | classpath sourceSets.main.runtimeClasspath 15 | main = 'org.jetbrains.ruby.runtime.signature.server.SignatureServerKt' 16 | } 17 | -------------------------------------------------------------------------------- /contract-creator/src/org/jetbrains/ruby/runtime/signature/server/SignatureServerInjector.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.runtime.signature.server 2 | 3 | import org.jetbrains.ruby.codeInsight.Injector 4 | import org.jetbrains.ruby.codeInsight.Logger 5 | import org.jetbrains.ruby.codeInsight.PrintToStdoutLogger 6 | 7 | object SignatureServerInjector : Injector { 8 | override fun getLogger(cl: Class): Logger { 9 | return PrintToStdoutLogger(cl) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contract-creator/src/org/jetbrains/ruby/runtime/signature/server/serialisation/ServerResponseBean.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.runtime.signature.server.serialisation 2 | 3 | import org.jetbrains.ruby.codeInsight.types.signature.* 4 | 5 | data class ServerResponseBean( 6 | val method_name: String, 7 | /** 8 | * Number of unnamedArguments passed by user explicitly 9 | * 10 | * For example for method: 11 | * def foo(a, b = 1); end 12 | * 13 | * This method invocation have only one explicit argument 14 | * foo(4) 15 | * 16 | * But this method invocation have two explicit unnamedArguments 17 | * foo(4, 5) 18 | */ 19 | val call_info_argc: Int, 20 | val args_info: String, 21 | val visibility: String, 22 | val path: String, 23 | val lineno: Int, 24 | val receiver_name: String, 25 | val return_type_name: String) 26 | 27 | // explicit here means that this unnamedArguments was explicitly provided by user 28 | // for example: 29 | // def foo(a, b = 1); end 30 | // foo(1) # here only `a` is explicitly provided 31 | // foo(1, 5) # here `a` and `b` are both explicitly provided 32 | private data class Arg(val paramInfo: ParameterInfo, val type: String, var explicit: Boolean) 33 | 34 | private const val PARAMETER_MODIFIER_INDEX_IN_ATTRIBUTES = 0 35 | private const val PARAMETER_TYPE_INDEX_IN_ATTRIBUTES = 1 36 | private const val PARAMETER_NAME_INDEX_IN_ATTRIBUTES = 2 37 | private const val NUMBER_OF_ATTRIBUTES_FOR_PARAMETER = 3 38 | 39 | /** 40 | * @throws IllegalStateException if [ServerResponseBean] is not correctly formed 41 | */ 42 | fun ServerResponseBean.toCallInfo(): CallInfo { 43 | var argc = this.call_info_argc 44 | 45 | val args = this.args_info.takeIf { it != "" }?.split(";")?.map { 46 | val parts: List = it.split(",") 47 | val modifier = parts[PARAMETER_MODIFIER_INDEX_IN_ATTRIBUTES] 48 | val type = parts[PARAMETER_TYPE_INDEX_IN_ATTRIBUTES] 49 | 50 | val name = if (parts.size == NUMBER_OF_ATTRIBUTES_FOR_PARAMETER) { 51 | // It's possible that parameter in ruby doesn't have name, for example: 52 | // def foo(*); end 53 | parts[PARAMETER_NAME_INDEX_IN_ATTRIBUTES] 54 | } else { 55 | "" 56 | } 57 | 58 | // If argc == -1 then all args are explicitly passed 59 | return@map Arg(ParameterInfo(name, ParameterInfo.Type.valueOf(modifier)), type, explicit = argc == -1) 60 | } ?: emptyList() 61 | 62 | if (argc != -1) { 63 | for (arg in args) { 64 | if (arg.paramInfo.isNamedParameter || 65 | arg.paramInfo.modifier == ParameterInfo.Type.REQ || 66 | arg.paramInfo.modifier == ParameterInfo.Type.POST) { 67 | arg.explicit = true 68 | argc-- 69 | } 70 | } 71 | 72 | for (arg in args) { 73 | if (argc <= 0) { 74 | break 75 | } 76 | if (arg.paramInfo.modifier == ParameterInfo.Type.OPT) { 77 | arg.explicit = true 78 | argc-- 79 | } 80 | } 81 | 82 | for (arg in args) { 83 | if (argc <= 0) { 84 | break 85 | } 86 | if (arg.paramInfo.modifier == ParameterInfo.Type.REST) { 87 | arg.explicit = true 88 | argc-- 89 | } 90 | } 91 | 92 | check(argc == 0 || args.any { it.paramInfo.modifier == ParameterInfo.Type.BLOCK } && argc == 1) { 93 | "Failed to parse this bean: ${this.toString()}" 94 | } 95 | } 96 | 97 | val namedArgumentsNamesToTypes = args.asSequence().filter { it.paramInfo.isNamedParameter } 98 | .map { ArgumentNameAndType(it.paramInfo.name, it.type) }.toList() 99 | 100 | val unnamedArgumentsTypes = args.asSequence().filter { !it.paramInfo.isNamedParameter } 101 | .map { arg -> 102 | ArgumentNameAndType(arg.paramInfo.name, arg.type.takeIf { arg.explicit } 103 | ?: ArgumentNameAndType.IMPLICITLY_PASSED_ARGUMENT_TYPE) 104 | }.toList() 105 | 106 | val methodInfo = MethodInfo.Impl( 107 | ClassInfo.Impl(gemInfoFromFilePathOrNull(this.path), this.receiver_name), 108 | this.method_name, 109 | RVisibility.valueOf(this.visibility), 110 | Location(this.path, this.lineno)) 111 | 112 | return CallInfoImpl(methodInfo, namedArgumentsNamesToTypes, unnamedArgumentsTypes, this.return_type_name) 113 | } 114 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Available idea versions: 2 | # https://www.jetbrains.com/intellij-repository/releases 3 | # https://www.jetbrains.com/intellij-repository/snapshots 4 | 5 | # ruby plugin versions can be found here: 6 | # https://plugins.jetbrains.com/plugin/1293-ruby/versions 7 | 8 | kotlin_version=1.2.70 9 | ideaVersion=IU-193.5233.102 10 | rubyPluginVersion=193.5233.57 11 | exposedVersion=0.17.3 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/ruby-type-inference/df63525a226c4926614a3937546b570b68bc42aa/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Nov 07 19:25:40 MSK 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /ide-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.1 (15 Dec 2017) 2 | 3 | * (#17) Fix "find usages" action for dynamic symbols which resolve to text-based 4 | definitions. 5 | 6 | ## 0.1 (29 Nov 2017) 7 | 8 | Initial plugin version 9 | 10 | * Collect State action 11 | 12 | Adds on_exit hook which dumps class/module includes structure and contained methods 13 | which can be used for resolution/completion later. 14 | 15 | * Collect Type action 16 | 17 | Enables call tracing (with a considerable slowdown) and dumps return types which 18 | can be used for better type inference. 19 | 20 | * Symbol/Type provider to improve resolution and type inference based on the collected 21 | data. -------------------------------------------------------------------------------- /ide-plugin/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | maven { url 'https://dl.bintray.com/jetbrains/intellij-plugin-service' } 4 | } 5 | } 6 | 7 | plugins { 8 | id "org.jetbrains.intellij" version "0.3.11" 9 | } 10 | 11 | dependencies { 12 | def withoutKotlinAndMySql = { 13 | exclude group: 'org.jetbrains.kotlin' 14 | exclude group: 'mysql' 15 | } 16 | def withoutSlfAndKotlinAndMySql = { 17 | exclude group: 'org.slf4j'//, module: 'slf4j-api' 18 | exclude group: 'org.jetbrains.kotlin' 19 | exclude group: 'mysql' 20 | } 21 | 22 | compile project(':common') 23 | compile project(':ruby-call-signature'), withoutKotlinAndMySql 24 | compile project(':storage-server-api'), withoutSlfAndKotlinAndMySql 25 | compile project(':contract-creator'), withoutSlfAndKotlinAndMySql 26 | compile project(':state-tracker'), withoutSlfAndKotlinAndMySql 27 | 28 | // https://mvnrepository.com/artifact/com.h2database/h2 29 | compile group: 'com.h2database', name: 'h2', version: '1.4.199' 30 | 31 | } 32 | 33 | sourceSets { 34 | main.java.srcDirs = ['src'] 35 | main.resources.srcDirs = ['resources'] 36 | test.java.srcDirs = ['src/test/java'] 37 | test.resources.srcDirs=['src/test/testData'] 38 | } 39 | 40 | intellij { 41 | version ideaVersion 42 | pluginName 'ruby-runtime-stats' 43 | plugins = ["yaml", "org.jetbrains.plugins.ruby:$rubyPluginVersion"] 44 | } 45 | 46 | patchPluginXml { 47 | sinceBuild '193.5233.102' 48 | untilBuild '193.*' 49 | version '0.3.3' 50 | } 51 | 52 | prepareSandbox.doLast { 53 | def destDir = "$it.destinationDir/$intellij.pluginName" 54 | copy { 55 | from sourceSets.main.resources.include("**/*.rb", "**/*.db") 56 | into destDir 57 | } 58 | } 59 | 60 | test { 61 | testLogging.showStandardStreams = true 62 | } 63 | 64 | -------------------------------------------------------------------------------- /ide-plugin/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | org.jetbrains.ruby-runtime-stats 3 | Ruby Dynamic Code Insight 4 | JetBrains 5 | com.intellij.modules.ruby 6 | This plugin provides additional Code Insight intelligence to improve resolution, find usages and refactoring 8 | capabilities.

9 | 10 |

The data is obtained via user project execution altered by a special tracker which stores symbol 11 | hierarchy, method return types, etc.

12 | ]]>
13 | 14 | Changelog 16 | ]]> 17 | 18 | 19 | 20 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | org.jetbrains.plugins.ruby.ruby.intentions.AddContractAnnotationIntention 38 | 39 | 40 | 41 | org.jetbrains.plugins.ruby.ruby.intentions.RemoveCollectedInfoIntention 42 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 61 | 62 | 63 | 64 | 67 | 68 | 71 | 74 | 75 | 78 | 79 | 82 | 85 | 88 | 89 | 90 | 91 |
-------------------------------------------------------------------------------- /ide-plugin/resources/runWithTypeTracker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ide-plugin/resources/runWithTypeTracker_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ide-plugin/src/com/intellij/execution/executors/CollectStateExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.intellij.execution.executors 2 | 3 | import com.intellij.execution.Executor 4 | import com.intellij.icons.AllIcons 5 | import com.intellij.openapi.util.text.StringUtil 6 | import com.intellij.openapi.wm.ToolWindowId 7 | import javax.swing.Icon 8 | 9 | class CollectStateExecutor : Executor() { 10 | 11 | private val myIcon = AllIcons.General.GearPlain 12 | 13 | override fun getToolWindowId(): String { 14 | return ToolWindowId.RUN 15 | } 16 | 17 | override fun getToolWindowIcon(): Icon { 18 | return myIcon 19 | } 20 | 21 | override fun getIcon(): Icon { 22 | return myIcon 23 | } 24 | 25 | override fun getDisabledIcon(): Icon? { 26 | return null 27 | } 28 | 29 | override fun getDescription(): String { 30 | return "Run selected configuration with collecting state" 31 | } 32 | 33 | override fun getActionName(): String { 34 | return "CollectState" 35 | } 36 | 37 | override fun getId(): String { 38 | return EXECUTOR_ID 39 | } 40 | 41 | override fun getStartActionText(): String { 42 | return "Run with Collecting State" 43 | } 44 | 45 | override fun getContextActionId(): String { 46 | return "RunCollectState" 47 | } 48 | 49 | override fun getHelpId(): String? { 50 | return null 51 | } 52 | 53 | override fun getStartActionText(configurationName: String): String { 54 | val name = escapeMnemonicsInConfigurationName( 55 | StringUtil.first(configurationName, 30, true)) 56 | return "Run" + (if (StringUtil.isEmpty(name)) "" else " '$name'") + " with Collecting State" 57 | } 58 | 59 | private fun escapeMnemonicsInConfigurationName(configurationName: String): String { 60 | return configurationName.replace("_", "__") 61 | } 62 | 63 | companion object { 64 | val EXECUTOR_ID = "CollectState" 65 | } 66 | } -------------------------------------------------------------------------------- /ide-plugin/src/com/intellij/execution/executors/RunWithTypeTrackerExecutor.java: -------------------------------------------------------------------------------- 1 | package com.intellij.execution.executors; 2 | 3 | import com.intellij.execution.Executor; 4 | import com.intellij.icons.AllIcons; 5 | import com.intellij.openapi.util.IconLoader; 6 | import com.intellij.openapi.util.text.StringUtil; 7 | import com.intellij.openapi.wm.ToolWindowId; 8 | import com.intellij.util.ui.UIUtil; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | import javax.swing.*; 13 | import java.net.URL; 14 | 15 | public class RunWithTypeTrackerExecutor extends Executor { 16 | @NotNull 17 | public static final String EXECUTOR_ID = "RunWithTypeTracker"; 18 | 19 | @NotNull 20 | private final Icon myIcon; 21 | 22 | public RunWithTypeTrackerExecutor() { 23 | final URL iconURL = RunWithTypeTrackerExecutor.class.getClassLoader().getResource( 24 | UIUtil.isUnderDarcula() ? "runWithTypeTracker_dark.svg" : "runWithTypeTracker.svg"); 25 | final Icon icon = IconLoader.findIcon(iconURL); 26 | myIcon = icon != null ? icon : AllIcons.General.Error; 27 | } 28 | 29 | @Override 30 | public String getToolWindowId() { 31 | return ToolWindowId.RUN; 32 | } 33 | 34 | @Override 35 | public Icon getToolWindowIcon() { 36 | return myIcon; 37 | } 38 | 39 | @NotNull 40 | @Override 41 | public Icon getIcon() { 42 | return myIcon; 43 | } 44 | 45 | @Nullable 46 | @Override 47 | public Icon getDisabledIcon() { 48 | return null; 49 | } 50 | 51 | @NotNull 52 | @Override 53 | public String getDescription() { 54 | return "Run selected configuration with Type Tracker"; 55 | } 56 | 57 | @NotNull 58 | @Override 59 | public String getActionName() { 60 | return "Run with Type Tracker"; 61 | } 62 | 63 | @NotNull 64 | @Override 65 | public String getId() { 66 | return EXECUTOR_ID; 67 | } 68 | 69 | @NotNull 70 | public String getStartActionText() { 71 | return "Run with Type Tracker"; 72 | } 73 | 74 | @NotNull 75 | @Override 76 | public String getContextActionId() { 77 | return "Run with Type Tracker"; 78 | } 79 | 80 | @Nullable 81 | @Override 82 | public String getHelpId() { 83 | return null; 84 | } 85 | 86 | @NotNull 87 | @Override 88 | public String getStartActionText(@NotNull final String configurationName) { 89 | final String name = escapeMnemonicsInConfigurationName( 90 | StringUtil.first(configurationName, 30, true)); 91 | return "Run" + (StringUtil.isEmpty(name) ? "" : " '" + name + "'") + " with Type Tracker"; 92 | } 93 | 94 | @NotNull 95 | private static String escapeMnemonicsInConfigurationName(@NotNull final String configurationName) { 96 | return configurationName.replace("_", "__"); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/IdePluginLogger.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby 2 | 3 | import org.jetbrains.ruby.codeInsight.Logger 4 | 5 | class IdePluginLogger(private val intellijPlatformLogger: com.intellij.openapi.diagnostic.Logger) : Logger { 6 | override fun info(msg: String) { 7 | intellijPlatformLogger.info(msg) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/PluginResourceUtil.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby; 2 | 3 | import com.intellij.ide.plugins.IdeaPluginDescriptor; 4 | import com.intellij.ide.plugins.PluginManager; 5 | import com.intellij.openapi.extensions.PluginId; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.io.File; 9 | 10 | public final class PluginResourceUtil { 11 | private static final String PLUGIN_ID = "org.jetbrains.ruby-runtime-stats"; 12 | 13 | private PluginResourceUtil() { 14 | } 15 | 16 | @NotNull 17 | public static String getPluginResourcesPath() { 18 | final IdeaPluginDescriptor plugin = PluginManager.getPlugin(PluginId.getId(PLUGIN_ID)); 19 | if (plugin == null) { 20 | throw new AssertionError("Nonsense: this plugin is not registered"); 21 | } 22 | final File pluginHome = plugin.getPath(); 23 | if (pluginHome == null) { 24 | throw new AssertionError("Corrupted plugin: could not find home"); 25 | } 26 | return pluginHome.getPath() + "/"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/RubyDynamicCodeInsightPluginInjector.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby 2 | 3 | import org.jetbrains.ruby.codeInsight.Injector 4 | import org.jetbrains.ruby.codeInsight.Logger 5 | 6 | object RubyDynamicCodeInsightPluginInjector : Injector { 7 | override fun getLogger(cl: Class): Logger { 8 | return IdePluginLogger(com.intellij.openapi.diagnostic.Logger.getInstance(cl)) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/actions/ExportAncestorsActions.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.actions 2 | 3 | import com.intellij.openapi.module.Module 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.projectRoots.Sdk 6 | import org.jetbrains.plugins.ruby.ancestorsextractor.AncestorsExtractorBase 7 | import org.jetbrains.plugins.ruby.ancestorsextractor.AncestorsExtractorByObjectSpace 8 | import org.jetbrains.plugins.ruby.ancestorsextractor.AncestorsExtractorByRubyMine 9 | import org.jetbrains.plugins.ruby.ancestorsextractor.RubyModule 10 | import java.io.PrintWriter 11 | 12 | /** 13 | * Base class representation for exporting Ruby on Rails project's modules' ancestors 14 | */ 15 | abstract class ExportAncestorsActionBase( 16 | whatToExport: String, 17 | generateFilename: (Project) -> String, 18 | private val extractor: AncestorsExtractorBase 19 | ) : ExportFileActionBase(whatToExport, generateFilename, extensions = arrayOf("txt"), 20 | numberOfProgressBarFractions = 5) { 21 | 22 | override fun backgroundProcess(absoluteFilePath: String, module: Module?, sdk: Sdk?, project: Project) { 23 | moveProgressBarForward() 24 | extractor.listener = ProgressListener() 25 | val ancestors: List = try { 26 | extractor.extractAncestors(project, sdk ?: throw IllegalStateException("Ruby SDK is not set")) 27 | } catch(ex: Throwable) { 28 | PrintWriter(absoluteFilePath).use { 29 | it.println(ex.message) 30 | } 31 | return 32 | } 33 | moveProgressBarForward() 34 | PrintWriter(absoluteFilePath).use { printWriter -> 35 | ancestors.forEach { 36 | printWriter.println("Module: ${it.name}") 37 | printWriter.print("Ancestors: ") 38 | if (it.ancestors.isEmpty()) printWriter.print("Nothing found") 39 | it.ancestors.forEach { printWriter.print("$it ") } 40 | printWriter.print("\n\n") 41 | } 42 | } 43 | moveProgressBarForward() 44 | } 45 | } 46 | 47 | class ExportAncestorsByObjectSpaceAction : ExportAncestorsActionBase( 48 | whatToExport = "ancestors by ObjectSpace", 49 | generateFilename = { project -> "ancestors-by-objectspace-${project.name}" }, 50 | extractor = AncestorsExtractorByObjectSpace() 51 | ) 52 | 53 | class ExportAncestorsByRubymineAction : ExportAncestorsActionBase( 54 | whatToExport = "ancestors by RubyMine", 55 | generateFilename = { project -> "ancestors-by-rubymine-${project.name}" }, 56 | extractor = AncestorsExtractorByRubyMine() 57 | ) -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/actions/ExportAncesttorsDiffAction.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.actions 2 | 3 | import com.intellij.openapi.module.Module 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.projectRoots.Sdk 6 | import org.jetbrains.plugins.ruby.ancestorsextractor.AncestorsExtractorByObjectSpace 7 | import org.jetbrains.plugins.ruby.ancestorsextractor.AncestorsExtractorByRubyMine 8 | import org.jetbrains.plugins.ruby.ancestorsextractor.RubyModule 9 | import java.io.PrintWriter 10 | 11 | class ExportAncestorsDiffAction : ExportFileActionBase(whatToExport = "ancestors diff", 12 | generateFilename = { project: Project -> "ancestors-diff-${project.name}" }, extensions = arrayOf("txt"), 13 | numberOfProgressBarFractions = 9) { 14 | override fun backgroundProcess(absoluteFilePath: String, module: Module?, sdk: Sdk?, project: Project) { 15 | moveProgressBarForward() 16 | val byObjectSpace: List 17 | val byRubyMine: List 18 | val ancestorHashSymbolIncluderToWhereIncluded: Map 19 | try { 20 | val listener = ProgressListener() 21 | val ancestorsExtractorByObjectSpace = AncestorsExtractorByObjectSpace(listener) 22 | 23 | // Here all listener methods would be called 24 | byObjectSpace = ancestorsExtractorByObjectSpace.extractAncestors(project, sdk ?: throw IllegalStateException("Ruby SDK is not set")) 25 | 26 | // The second place where all listener methods would be called 27 | ancestorHashSymbolIncluderToWhereIncluded = ancestorsExtractorByObjectSpace.extractIncludes(project, sdk) 28 | 29 | // Provide all modulesNames same as in byObjectSpace for easy ancestors comparison 30 | val allModulesNames: List = byObjectSpace.map { it.name } 31 | 32 | // The third place where all listener methods would be called 33 | byRubyMine = AncestorsExtractorByRubyMine(allModulesNames, listener) 34 | .extractAncestors(project, sdk) 35 | } catch (ex: Throwable) { 36 | PrintWriter(absoluteFilePath).use { 37 | it.println(ex.message) 38 | } 39 | return 40 | } 41 | 42 | moveProgressBarForward() 43 | PrintWriter(absoluteFilePath).use { printWriter -> 44 | val objectSpaceIterator = byObjectSpace.iterator() 45 | val rubymineIterator = byRubyMine.iterator() 46 | while (objectSpaceIterator.hasNext() && rubymineIterator.hasNext()) { 47 | val a = objectSpaceIterator.next() 48 | val b = rubymineIterator.next() 49 | assert(a.name == b.name) 50 | printWriter.println("Module: ${a.name}") 51 | var same = true 52 | a.ancestors.filter { !b.ancestors.contains(it) }.let { 53 | if (!it.isEmpty()) { 54 | same = false 55 | printWriter.print("Ancestors in ObjectSpace only: ") 56 | it.forEach { 57 | val whereIncluded = ancestorHashSymbolIncluderToWhereIncluded[it + "#" + a.name] 58 | var toPrint = it 59 | if (whereIncluded != null) { 60 | toPrint += "($whereIncluded)" 61 | } 62 | printWriter.print("$toPrint ") 63 | } 64 | printWriter.println() 65 | } 66 | } 67 | b.ancestors.filter { !a.ancestors.contains(it) }.let { 68 | if (!it.isEmpty()) { 69 | same = false 70 | printWriter.print("Ancestors in RubyMine only: ") 71 | it.forEach { printWriter.print("$it ") } 72 | printWriter.println() 73 | } 74 | } 75 | if (same) { 76 | printWriter.println("No difference in ancestors list") 77 | } 78 | printWriter.println() 79 | } 80 | } 81 | moveProgressBarForward() 82 | } 83 | } -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/actions/ExportFileActionBase.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.actions 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.fileChooser.FileChooserDescriptor 5 | import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory 6 | import com.intellij.openapi.fileChooser.FileSaverDescriptor 7 | import com.intellij.openapi.fileChooser.ex.FileChooserDialogImpl 8 | import com.intellij.openapi.fileChooser.ex.FileSaverDialogImpl 9 | import com.intellij.openapi.module.Module 10 | import com.intellij.openapi.progress.ProgressManager 11 | import com.intellij.openapi.progress.util.ProgressWindow 12 | import com.intellij.openapi.project.DumbAwareAction 13 | import com.intellij.openapi.project.Project 14 | import com.intellij.openapi.projectRoots.Sdk 15 | import com.intellij.openapi.ui.DialogBuilder 16 | import com.intellij.openapi.ui.Messages 17 | import com.intellij.openapi.util.ThrowableComputable 18 | import org.jetbrains.plugins.ruby.ancestorsextractor.AncestorsExtractorBase 19 | import org.jetbrains.plugins.ruby.ancestorsextractor.RailsConsoleRunner 20 | import org.jetbrains.plugins.ruby.ruby.RModuleUtil 21 | 22 | /** 23 | * Base class representing file export action with "save to" dialog 24 | * @param whatToExport Will be shown in "save to" dialog 25 | * @param generateFilename Generate filename for "save to" dialog 26 | * @param extensions Array of available extensions for exported file 27 | * @param description Description in "save to" dialog 28 | */ 29 | abstract class ExportFileActionBase( 30 | private val whatToExport: String, 31 | private val generateFilename: (Project) -> String, 32 | private val extensions: Array, 33 | private val description: String = "", 34 | private val numberOfProgressBarFractions: Int? = null 35 | ) : DumbAwareAction() { 36 | final override fun actionPerformed(e: AnActionEvent) { 37 | val project = e.project ?: return 38 | val dialog = FileSaverDialogImpl(FileSaverDescriptor( 39 | "Export $whatToExport", 40 | description, 41 | *extensions), project) 42 | val fileWrapper = dialog.save(null, generateFilename(project)) ?: return 43 | 44 | val module: Module? = RModuleUtil.getInstance().getModule(e.dataContext) 45 | val sdk: Sdk? = RModuleUtil.getInstance().findRubySdkForModule(module) 46 | 47 | try { 48 | ProgressManager.getInstance().runProcessWithProgressSynchronously(ThrowableComputable { 49 | return@ThrowableComputable backgroundProcess(fileWrapper.file.absolutePath, module, sdk, project) 50 | }, "Exporting $whatToExport", false, project) 51 | } catch (ex: Exception) { 52 | Messages.showErrorDialog(ex.message, "Error while exporting $whatToExport") 53 | } 54 | } 55 | 56 | /** 57 | * In this method implementation you can do you job needed for file export and then file exporting itself. 58 | * 59 | * @param absoluteFilePath absolute file path which user have chosen to save file to. 60 | * @param module module from the context of action it invoked 61 | * @param sdk sdk from the context of action it invoked 62 | * @param project project from the context of action it invoked 63 | */ 64 | protected abstract fun backgroundProcess(absoluteFilePath: String, module: Module?, sdk: Sdk?, project: Project) 65 | 66 | @Throws(IllegalStateException::class) 67 | protected fun moveProgressBarForward() { 68 | if (numberOfProgressBarFractions == null) throw IllegalStateException("You cannot call moveProgressBarForward() " + 69 | "method when progressBarFractions property is null") 70 | val progressIndicator = ProgressManager.getInstance().progressIndicator 71 | if (progressIndicator is ProgressWindow) { 72 | progressIndicator.fraction = minOf(1.0, progressIndicator.fraction + 1.0/numberOfProgressBarFractions) 73 | } 74 | } 75 | 76 | /** 77 | * You can use to set as [AncestorsExtractorBase.listener] because every [ProgressListener] 78 | * method call just calls [moveProgressBarForward] 79 | */ 80 | protected inner class ProgressListener : RailsConsoleRunner.Listener { 81 | override fun irbConsoleExecuted() { 82 | moveProgressBarForward() 83 | } 84 | 85 | override fun informationWasExtractedFromIRB() { 86 | moveProgressBarForward() 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/actions/ImportExportContractsAction.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.actions 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.fileChooser.FileChooserDescriptor 5 | import com.intellij.openapi.fileChooser.ex.FileChooserDialogImpl 6 | import com.intellij.openapi.module.Module 7 | import com.intellij.openapi.progress.ProgressIndicator 8 | import com.intellij.openapi.progress.ProgressManager 9 | import com.intellij.openapi.project.DumbAwareAction 10 | import com.intellij.openapi.project.Project 11 | import com.intellij.openapi.projectRoots.Sdk 12 | import com.intellij.openapi.ui.Messages 13 | import com.intellij.openapi.util.ThrowableComputable 14 | import org.jetbrains.exposed.sql.Database 15 | import org.jetbrains.exposed.sql.selectAll 16 | import org.jetbrains.exposed.sql.transactions.transaction 17 | import org.jetbrains.plugins.ruby.ruby.codeInsight.types.resetAllRubyTypeProviderAndIDEACaches 18 | import org.jetbrains.ruby.codeInsight.types.signature.CallInfo 19 | import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider 20 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoRow 21 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoTable 22 | import java.io.File 23 | 24 | const val CHUNK_SIZE = 1500 25 | 26 | fun Database.copyTo(destination: Database, moveProgressBar: Boolean) { 27 | var progressIndicator: ProgressIndicator? = null 28 | var count: Int? = null 29 | 30 | if (moveProgressBar) { 31 | progressIndicator = ProgressManager.getInstance().progressIndicator 32 | count = transaction(this) { CallInfoTable.selectAll().count() } 33 | } 34 | 35 | var offset = 0 36 | while (true) { 37 | val info: List = transaction(this) { 38 | CallInfoRow.wrapRows(CallInfoTable.selectAll().limit(CHUNK_SIZE, offset)).map { it.copy() } 39 | } 40 | if (info.isEmpty()) { 41 | break 42 | } 43 | 44 | transaction(destination) { 45 | info.forEach { CallInfoTable.insertInfoIfNotContains(it) } 46 | } 47 | 48 | offset += CHUNK_SIZE 49 | 50 | if (moveProgressBar) { 51 | progressIndicator!!.fraction = offset.toDouble() / count!! 52 | } 53 | } 54 | } 55 | 56 | class ExportContractsAction : ExportFileActionBase( 57 | whatToExport = "Type contracts", 58 | generateFilename = { project: Project -> "${project.name}-type-contracts" }, 59 | extensions = arrayOf("mv.db") 60 | ) { 61 | override fun backgroundProcess(absoluteFilePath: String, module: Module?, sdk: Sdk?, project: Project) { 62 | exportContractsToFile(absoluteFilePath, moveProgressBar = true) 63 | } 64 | 65 | companion object { 66 | fun exportContractsToFile(pathToExport: String, moveProgressBar: Boolean) { 67 | check(pathToExport.endsWith(DatabaseProvider.H2_DB_FILE_EXTENSION)) { 68 | "Path to export must end with .mv.db" 69 | } 70 | File(pathToExport).delete() 71 | 72 | val databaseToExportTo = DatabaseProvider.connectToDB(pathToExport) 73 | 74 | DatabaseProvider.defaultDatabase!!.copyTo(databaseToExportTo, moveProgressBar) 75 | } 76 | } 77 | } 78 | 79 | class ImportContractsAction : DumbAwareAction() { 80 | override fun actionPerformed(e: AnActionEvent) { 81 | val project = e.project 82 | val files = FileChooserDialogImpl( 83 | FileChooserDescriptor(true, false, false, false, false, false), 84 | project).choose(project) 85 | 86 | if (files.isEmpty()) { 87 | return 88 | } 89 | 90 | try { 91 | ProgressManager.getInstance().runProcessWithProgressSynchronously(ThrowableComputable { 92 | files.forEach { importContractsFromFile(it.path, moveProgressBar = true) } 93 | return@ThrowableComputable 94 | }, "Importing type contracts", false, project) 95 | resetAllRubyTypeProviderAndIDEACaches(project) 96 | } catch (ex: Exception) { 97 | Messages.showErrorDialog(ex.message, "Error while importing type contracts") 98 | } 99 | } 100 | 101 | companion object { 102 | fun importContractsFromFile(pathToImportFrom: String, moveProgressBar: Boolean) { 103 | check(pathToImportFrom.endsWith(DatabaseProvider.H2_DB_FILE_EXTENSION)) { 104 | "Path to import from must end with .mv.db" 105 | } 106 | 107 | val dbToImportFrom = DatabaseProvider.connectToDB(pathToImportFrom) 108 | 109 | dbToImportFrom.copyTo(DatabaseProvider.defaultDatabase!!, moveProgressBar) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/ProjectLifecycleListenerImpl.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.codeInsight 2 | 3 | import com.google.gson.Gson 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.project.ProjectManagerListener 6 | import org.jetbrains.plugins.ruby.ruby.persistent.TypeInferenceDirectory 7 | import org.jetbrains.plugins.ruby.util.runServerAsyncInIDEACompatibleMode 8 | import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider 9 | import org.jetbrains.ruby.runtime.signature.server.SignatureServer 10 | import java.io.File 11 | import java.io.PrintWriter 12 | import java.nio.file.Paths 13 | 14 | /** 15 | * Short [Project] description for `rubymine-type-tracer` 16 | */ 17 | data class ProjectDescription(val projectName: String, val projectPath: String, val pipeFilePath: String) { 18 | /** 19 | * @param project default projects are not allowed! 20 | */ 21 | constructor(project: Project, pipeFilePath: String) : this(project.name, project.basePath!!, pipeFilePath) 22 | } 23 | 24 | /** 25 | * This directory is needed for `rubymine-type-tracker` script 26 | * 27 | * In this directory we keep files named the same as currently opened projects in RubyMine. 28 | * Each file contains projectPath of pipe file required for arg-scanner. 29 | */ 30 | private val openedProjectsDir = File(System.getProperty("java.io.tmpdir")!!).resolve(".ruby-type-inference") 31 | .also { it.mkdirs() } 32 | 33 | /** 34 | * This registered in `plugin.xml` and it's constructor called every time RubyMine starts 35 | */ 36 | class ProjectLifecycleListenerImpl : ProjectManagerListener { 37 | private val gson = Gson() 38 | 39 | private companion object { 40 | @Volatile 41 | private var initialized: Boolean = false 42 | } 43 | 44 | override fun projectOpened(project: Project) { 45 | if (!project.isDefault) { 46 | connectToDB(project.name) 47 | 48 | // This server is used for `rubymine-type-tracker` script 49 | startNewBackgroundInfinityServer(project) 50 | } 51 | } 52 | 53 | override fun projectClosed(project: Project) { 54 | if (!project.isDefault) { 55 | val projectDescription = readProjectDescription(project, deleteJsonAfterRead = true) 56 | File(projectDescription.pipeFilePath).delete() 57 | } 58 | } 59 | 60 | private fun connectToDB(projectName: String) { 61 | val filePath = Paths.get( 62 | TypeInferenceDirectory.RUBY_TYPE_INFERENCE_DIRECTORY.toString(), 63 | projectName).toString() + DatabaseProvider.H2_DB_FILE_EXTENSION 64 | 65 | DatabaseProvider.connectToDB(filePath, isDefaultDatabase = true) 66 | } 67 | 68 | /** 69 | * Starts server for `rubymine-type-tracker` script 70 | */ 71 | private fun startNewBackgroundInfinityServer(project: Project): Boolean { 72 | if (project.isDefault) { 73 | return false 74 | } 75 | 76 | val server = SignatureServer() 77 | val pipeFilePath: String = server.runServerAsyncInIDEACompatibleMode(project) 78 | 79 | writeProjectDescription(ProjectDescription(project, pipeFilePath)) 80 | 81 | server.afterExitListener = { 82 | startNewBackgroundInfinityServer(project) 83 | } 84 | return true 85 | } 86 | 87 | private fun writeProjectDescription(description: ProjectDescription) { 88 | val jsonFile: File = openedProjectsDir.resolve(description.projectName) 89 | PrintWriter(jsonFile).use { it.println(gson.toJson(description)) } 90 | } 91 | 92 | private fun readProjectDescription(project: Project, deleteJsonAfterRead: Boolean = false): ProjectDescription { 93 | val jsonFile: File = openedProjectsDir.resolve(project.name) 94 | val json: String = jsonFile.bufferedReader().use { it.readText() } 95 | val description = gson.fromJson(json, ProjectDescription::class.java)!! 96 | if (deleteJsonAfterRead) { 97 | jsonFile.delete() 98 | } 99 | return description 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/RubyDynamicCodeInsightPluginAppLifecyctlListener.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.codeInsight 2 | 3 | import com.intellij.ide.AppLifecycleListener 4 | import com.intellij.openapi.project.Project 5 | import org.jetbrains.plugins.ruby.RubyDynamicCodeInsightPluginInjector 6 | import org.jetbrains.ruby.codeInsight.initInjector 7 | 8 | class RubyDynamicCodeInsightPluginAppLifecyctlListener : AppLifecycleListener { 9 | override fun appStarting(projectFromCommandLine: Project?) { 10 | initInjector(RubyDynamicCodeInsightPluginInjector) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/TrackerDataLoader.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.codeInsight 2 | 3 | import com.intellij.openapi.module.ModuleManager 4 | import com.intellij.openapi.project.DumbAware 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.openapi.startup.StartupActivity 7 | import org.jetbrains.plugins.ruby.ruby.codeInsight.stateTracker.RubyClassHierarchyWithCaching 8 | 9 | class TrackerDataLoader : StartupActivity, DumbAware { 10 | override fun runActivity(project: Project) { 11 | 12 | ModuleManager.getInstance(project).modules.forEach { 13 | RubyClassHierarchyWithCaching.loadFromSystemDirectory(it) 14 | } 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/stateTracker/ClassHierarchySymbolProvider.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.codeInsight.stateTracker 2 | 3 | import com.intellij.openapi.module.ModuleUtilCore 4 | import com.intellij.psi.PsiElement 5 | import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.RubySymbolProviderBase 6 | import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.fqn.FQN 7 | import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.structure.Symbol 8 | import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.v2.SymbolPsiProcessor 9 | import org.jetbrains.plugins.ruby.ruby.lang.psi.RPsiElement 10 | 11 | class ClassHierarchySymbolProvider : RubySymbolProviderBase() { 12 | override fun processDynamicSymbols(symbol: Symbol, element: RPsiElement?, fqn: FQN, processor: SymbolPsiProcessor, 13 | invocationPoint: PsiElement?): Boolean { 14 | if (element == null) { 15 | return true 16 | } 17 | 18 | val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return true 19 | val hierarchy = RubyClassHierarchyWithCaching.getInstance(module)?: return true 20 | hierarchy.getMembersWithCaching(fqn.fullPath, symbol.rootSymbol).forEach { 21 | if (!processor.process(it)) { 22 | return false 23 | } 24 | } 25 | return true 26 | } 27 | } -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/types/RubyCollectStateRunner.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.codeInsight.types 2 | 3 | import com.intellij.execution.ExecutionException 4 | import com.intellij.execution.configurations.RunProfile 5 | import com.intellij.execution.configurations.RunProfileState 6 | import com.intellij.execution.executors.CollectStateExecutor 7 | import com.intellij.execution.runners.ExecutionEnvironment 8 | import com.intellij.execution.ui.RunContentDescriptor 9 | import com.intellij.openapi.util.io.FileUtil 10 | import org.jetbrains.plugins.ruby.ruby.run.configuration.AbstractRubyRunConfiguration 11 | import org.jetbrains.plugins.ruby.ruby.run.configuration.CollectExecSettings 12 | import org.jetbrains.plugins.ruby.ruby.run.configuration.RubyAbstractCommandLineState 13 | import org.jetbrains.plugins.ruby.ruby.run.configuration.RubyProgramRunner 14 | import java.io.IOException 15 | 16 | class RubyCollectStateRunner : RubyProgramRunner() { 17 | 18 | override fun canRun(executorId: String, profile: RunProfile): Boolean { 19 | return executorId == CollectStateExecutor.EXECUTOR_ID && profile is AbstractRubyRunConfiguration<*> 20 | } 21 | 22 | @Throws(ExecutionException::class) 23 | override fun doExecute(state: RunProfileState, 24 | environment: ExecutionEnvironment): RunContentDescriptor? { 25 | if (state is RubyAbstractCommandLineState) { 26 | val newConfig = state.config.clone() 27 | val pathToState = tryGenerateTmpDirPath() 28 | 29 | CollectExecSettings.putTo(newConfig, 30 | CollectExecSettings.createSettings(true, 31 | false, 32 | true, 33 | pathToState 34 | )) 35 | val newState = newConfig.getState(environment.executor, environment) 36 | if (newState != null) { 37 | return super.doExecute(newState, environment) 38 | } 39 | } 40 | 41 | return null 42 | } 43 | 44 | 45 | private fun tryGenerateTmpDirPath(): String? { 46 | try { 47 | val tmpDir = FileUtil.createTempDirectory("state-tracker", "") 48 | return tmpDir.absolutePath 49 | } catch (ignored: IOException) { 50 | return null 51 | } 52 | 53 | } 54 | 55 | override fun getRunnerId(): String { 56 | return RUBY_COLLECT_STATE_RUNNER_ID 57 | } 58 | 59 | companion object { 60 | private val RUBY_COLLECT_STATE_RUNNER_ID = "RubyCollectState" 61 | } 62 | } -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/types/RubyRunWithTypeTrackerRunner.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.codeInsight.types 2 | 3 | import com.intellij.execution.ExecutionException 4 | import com.intellij.execution.configurations.RunProfile 5 | import com.intellij.execution.configurations.RunProfileState 6 | import com.intellij.execution.executors.RunWithTypeTrackerExecutor 7 | import com.intellij.execution.runners.ExecutionEnvironment 8 | import com.intellij.execution.ui.RunContentDescriptor 9 | import com.intellij.openapi.components.ServiceManager 10 | import com.intellij.openapi.util.io.FileUtil 11 | import org.jetbrains.plugins.ruby.ruby.run.configuration.AbstractRubyRunConfiguration 12 | import org.jetbrains.plugins.ruby.ruby.run.configuration.CollectExecSettings 13 | import org.jetbrains.plugins.ruby.ruby.run.configuration.RubyAbstractCommandLineState 14 | import org.jetbrains.plugins.ruby.ruby.run.configuration.RubyProgramRunner 15 | import org.jetbrains.plugins.ruby.settings.RubyTypeContractsSettings 16 | import java.io.IOException 17 | 18 | class RubyRunWithTypeTrackerRunner : RubyProgramRunner() { 19 | 20 | @Throws(ExecutionException::class) 21 | override fun doExecute(state: RunProfileState, 22 | environment: ExecutionEnvironment): RunContentDescriptor? { 23 | if (state is RubyAbstractCommandLineState) { 24 | val (_, _, typeTrackerEnabled) = ServiceManager.getService(environment.project, RubyTypeContractsSettings::class.java) 25 | val newConfig = state.config.clone() 26 | val pathToState = tryGenerateTmpDirPath() 27 | 28 | CollectExecSettings.putTo(newConfig, 29 | CollectExecSettings.createSettings(true, 30 | typeTrackerEnabled, 31 | false, 32 | pathToState 33 | )) 34 | val newState = newConfig.getState(environment.executor, environment) 35 | if (newState != null) { 36 | return super.doExecute(newState, environment) 37 | } 38 | } 39 | 40 | return null 41 | } 42 | 43 | override fun preloaderAllowed(): Boolean = false 44 | 45 | private fun tryGenerateTmpDirPath(): String? = try { 46 | val tmpDir = FileUtil.createTempDirectory("type-tracker", "") 47 | tmpDir.absolutePath 48 | } catch (ignored: IOException) { 49 | null 50 | } 51 | 52 | override fun canRun(executorId: String, profile: RunProfile): Boolean { 53 | return executorId == RunWithTypeTrackerExecutor.EXECUTOR_ID && profile is AbstractRubyRunConfiguration<*> 54 | } 55 | 56 | override fun getRunnerId(): String { 57 | return RUBY_COLLECT_TYPE_RUNNER_ID 58 | } 59 | 60 | companion object { 61 | private val RUBY_COLLECT_TYPE_RUNNER_ID = "RubyCollectType" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/intentions/BaseRubyMethodIntentionAction.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.intentions 2 | 3 | import com.intellij.codeInsight.intention.impl.BaseIntentionAction 4 | import com.intellij.openapi.editor.Editor 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.psi.PsiFile 7 | import com.intellij.psi.util.PsiTreeUtil 8 | import org.jetbrains.plugins.ruby.ruby.lang.psi.variables.RFName 9 | 10 | abstract class BaseRubyMethodIntentionAction : BaseIntentionAction() { 11 | private var _text: String? = null 12 | 13 | final override fun getText(): String = _text ?: getTextByRubyFunctionNamePsiElement(null) 14 | 15 | protected abstract fun getTextByRubyFunctionNamePsiElement(element: RFName?): String 16 | 17 | protected fun getRFName(editor: Editor, file: PsiFile): RFName? { 18 | val offset = editor.caretModel.offset 19 | val element = file.findElementAt(offset) 20 | return PsiTreeUtil.getParentOfType(element, RFName::class.java) 21 | } 22 | 23 | override fun isAvailable(project: Project, editor: Editor, file: PsiFile): Boolean { 24 | val methodNameElement: RFName = getRFName(editor, file) ?: return false 25 | _text = getTextByRubyFunctionNamePsiElement(methodNameElement) 26 | return true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/intentions/RemoveCollectedInfoIntention.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.intentions 2 | 3 | import com.intellij.openapi.editor.Editor 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.psi.PsiFile 6 | import org.jetbrains.plugins.ruby.ruby.codeInsight.types.resetAllRubyTypeProviderAndIDEACaches 7 | import org.jetbrains.plugins.ruby.ruby.lang.psi.RubyPsiUtil 8 | import org.jetbrains.plugins.ruby.ruby.lang.psi.variables.RFName 9 | import org.jetbrains.ruby.codeInsight.types.signature.ClassInfo 10 | import org.jetbrains.ruby.codeInsight.types.signature.MethodInfo 11 | import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider 12 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoTable 13 | 14 | class RemoveCollectedInfoIntention : BaseRubyMethodIntentionAction() { 15 | override fun getFamilyName(): String = getText() 16 | 17 | override fun getTextByRubyFunctionNamePsiElement(element: RFName?): String { 18 | return "Remove collected info about ${element?.name ?: "this"} method" 19 | } 20 | 21 | override fun invoke(project: Project, editor: Editor, file: PsiFile) { 22 | val method = getRFName(editor, file)?.let { RubyPsiUtil.getContainingRMethod(it) } ?: return 23 | val rubyModuleName = RubyPsiUtil.getContainingRClassOrModule(method)?.fqn?.fullPath ?: "Object" 24 | 25 | val info = MethodInfo.Impl(ClassInfo.Impl(null, rubyModuleName), method.fqn.shortName) 26 | 27 | DatabaseProvider.defaultDatabaseTransaction { CallInfoTable.deleteAllInfoRelatedTo(info) } 28 | 29 | resetAllRubyTypeProviderAndIDEACaches(project) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/persistent/TypeInferenceDirectory.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.persistent 2 | 3 | import com.intellij.openapi.application.PathManager 4 | import com.intellij.openapi.util.io.FileUtil 5 | import java.nio.file.Paths 6 | 7 | object TypeInferenceDirectory { 8 | val RUBY_TYPE_INFERENCE_DIRECTORY by lazy { 9 | val path = Paths.get(PathManager.getSystemPath(), "ruby-type-inference")!! 10 | FileUtil.createDirectory(path.toFile()) 11 | path 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/ruby/run/configuration/CollectExecSettings.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.run.configuration; 2 | 3 | import com.intellij.openapi.util.Key; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | public class CollectExecSettings { 8 | 9 | @NotNull 10 | private static final Key COLLECT_TYPE_EXEC_SETTINGS = new Key<>("CollectTypeExecSettings"); 11 | 12 | private boolean myArgScannerEnabled; 13 | private boolean myTypeTrackerEnabled; 14 | private boolean myStateTrackerEnabled; 15 | @Nullable 16 | private String myOutputDirectory; 17 | 18 | public boolean isArgScannerEnabled() { 19 | return myArgScannerEnabled; 20 | } 21 | 22 | public boolean isStateTrackerEnabled() { 23 | return myStateTrackerEnabled; 24 | } 25 | 26 | public void setStateTrackerEnabled(boolean myStateTrackerEnabled) { 27 | this.myStateTrackerEnabled = myStateTrackerEnabled; 28 | } 29 | 30 | public void setArgScannerEnabled(boolean myArgScannerEnabled) { 31 | this.myArgScannerEnabled = myArgScannerEnabled; 32 | } 33 | 34 | public boolean isTypeTrackerEnabled() { 35 | return myTypeTrackerEnabled; 36 | } 37 | 38 | public void setTypeTrackerEnabled(boolean myTypeTrackerEnabled) { 39 | this.myTypeTrackerEnabled = myTypeTrackerEnabled; 40 | } 41 | 42 | @Nullable 43 | public String getOutputDirectory() { 44 | return myOutputDirectory; 45 | } 46 | 47 | public void setReturnTypeTrackerPath(final @Nullable String path) { 48 | myOutputDirectory = path; 49 | } 50 | 51 | @NotNull 52 | public static CollectExecSettings getFrom(@NotNull final AbstractRubyRunConfiguration configuration) { 53 | final CollectExecSettings data = configuration.getCopyableUserData(COLLECT_TYPE_EXEC_SETTINGS); 54 | return data != null ? data : createSettings(false, false, false, null); 55 | } 56 | 57 | public static void putTo(@NotNull final AbstractRubyRunConfiguration configuration, 58 | @NotNull final CollectExecSettings settings) { 59 | configuration.putCopyableUserData(COLLECT_TYPE_EXEC_SETTINGS, settings); 60 | } 61 | 62 | public static CollectExecSettings createSettings(final boolean argScannerEnabled, 63 | final boolean typeTrackerEnabled, 64 | final boolean stateTrackerEnabled, 65 | final String tempDirectoryPath 66 | ) { 67 | final CollectExecSettings settings = new CollectExecSettings(); 68 | settings.setArgScannerEnabled(argScannerEnabled); 69 | settings.setTypeTrackerEnabled(typeTrackerEnabled); 70 | settings.setReturnTypeTrackerPath(tempDirectoryPath); 71 | settings.setStateTrackerEnabled(stateTrackerEnabled); 72 | 73 | return settings; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/settings/RubyTypeContractsConfigurable.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.settings 2 | 3 | import com.intellij.openapi.options.ConfigurableBase 4 | 5 | class RubyTypeContractsConfigurable(private val settings: RubyTypeContractsSettings) : 6 | ConfigurableBase(RubyTypeContractsConfigurable::class.java.name, 8 | "Ruby Type Contracts", null) { 9 | 10 | override fun getSettings(): RubyTypeContractsSettings { 11 | return settings 12 | } 13 | 14 | override fun createUi(): RubyTypeContractsConfigurableUI { 15 | return RubyTypeContractsConfigurableUI(settings) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/settings/RubyTypeContractsSettings.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.settings 2 | 3 | import com.intellij.openapi.components.PersistentStateComponent 4 | import com.intellij.openapi.components.State 5 | import com.intellij.openapi.components.Storage 6 | import com.intellij.util.xmlb.XmlSerializerUtil 7 | import com.intellij.util.xmlb.annotations.Attribute 8 | import com.intellij.util.xmlb.annotations.MapAnnotation 9 | import org.jetbrains.ruby.codeInsight.types.signature.GemInfo 10 | 11 | @State( 12 | name = "RubyTypeContractsSettings", 13 | storages = arrayOf(Storage("ruby_type_inference.xml")) 14 | ) 15 | data class RubyTypeContractsSettings @JvmOverloads constructor( 16 | @Attribute 17 | var localSourcesTrackingPolicy: LocalSourcesTrackingPolicy = LocalSourcesTrackingPolicy.ACCUMULATE, 18 | @MapAnnotation 19 | var perGemSettingsMap: MutableMap = HashMap(), 20 | @Attribute("typeTrackerEnabled") 21 | var typeTrackerEnabled: Boolean = true, 22 | @Attribute("stateTrackerEnabled") 23 | var stateTrackerEnabled: Boolean = true) 24 | 25 | : PersistentStateComponent { 26 | override fun loadState(state: RubyTypeContractsSettings) { 27 | XmlSerializerUtil.copyBean(state, this) 28 | } 29 | 30 | override fun getState(): RubyTypeContractsSettings? = this 31 | } 32 | 33 | enum class LocalSourcesTrackingPolicy { 34 | IGNORE, 35 | CLEAR_ON_CHANGES, 36 | ACCUMULATE 37 | } 38 | 39 | data class GemInfoBean(@Attribute("name") override val name: String = "", 40 | @Attribute("version") override val version: String = "") : GemInfo 41 | 42 | data class PerGemSettings(@Attribute("share") val share: Boolean) { 43 | @Suppress("unused") 44 | private constructor() : this(true) 45 | } -------------------------------------------------------------------------------- /ide-plugin/src/org/jetbrains/plugins/ruby/util/SignatureServerUtil.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.util 2 | 3 | import com.intellij.openapi.project.Project 4 | import org.jetbrains.plugins.ruby.ruby.codeInsight.types.resetAllRubyTypeProviderAndIDEACaches 5 | import org.jetbrains.ruby.runtime.signature.server.SignatureServer 6 | 7 | /** 8 | * Runs [SignatureServer] in IDEA compatible mode (for example IDEAs caches will be cleaned after server 9 | * flushes data to DB. Server is launched in daemon mode and so on) 10 | * 11 | * @return pipe filename path which should be passed to arg-scanner. 12 | */ 13 | fun SignatureServer.runServerAsyncInIDEACompatibleMode(project: Project): String { 14 | this.afterFlushListener = { 15 | resetAllRubyTypeProviderAndIDEACaches(project) 16 | } 17 | return this.runServerAsync(isDaemon = true) 18 | } 19 | -------------------------------------------------------------------------------- /ide-plugin/src/test/java/org/jetbrains/plugins/ruby/ruby/actions/ImportExportTests.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.plugins.ruby.ruby.actions 2 | 3 | import junit.framework.Assert 4 | import junit.framework.TestCase 5 | import org.jetbrains.exposed.sql.Database 6 | import org.jetbrains.exposed.sql.transactions.transaction 7 | import org.jetbrains.ruby.codeInsight.types.signature.* 8 | import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider 9 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoRow 10 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoTable 11 | import java.nio.file.Paths 12 | import java.util.* 13 | 14 | class ImportExportTests : TestCase() { 15 | 16 | fun testSimpleExport() { 17 | val data = (0 until 2 * CHUNK_SIZE + 1).map { 18 | createCallInfo("A$it", "foo", listOf("String", "Symbol"), "Integer") 19 | } 20 | 21 | DatabaseProvider.connectToDB(generateTempDBFilePath(), isDefaultDatabase = true) 22 | 23 | DatabaseProvider.defaultDatabaseTransaction { 24 | data.forEach { CallInfoTable.insertInfoIfNotContains(it) } 25 | } 26 | 27 | val exportedDB = generateTempDBFilePath().let { pathToExport: String -> 28 | ExportContractsAction.exportContractsToFile(pathToExport, moveProgressBar = false) 29 | 30 | return@let DatabaseProvider.connectToDB(pathToExport) 31 | } 32 | 33 | Assert.assertEquals(DatabaseProvider.defaultDatabase!!.allCallInfos, exportedDB.allCallInfos) 34 | } 35 | 36 | fun testSimpleImport() { 37 | val data = (0 until 2 * CHUNK_SIZE + 1).map { 38 | createCallInfo("A$it", "foo", listOf("String", "Symbol"), "Integer") 39 | } 40 | 41 | DatabaseProvider.connectToDB(generateTempDBFilePath(), isDefaultDatabase = true) 42 | 43 | val dbToImport = generateTempDBFilePath().let { pathToImport: String -> 44 | val db = DatabaseProvider.connectToDB(pathToImport) 45 | 46 | transaction(db) { 47 | data.forEach { CallInfoTable.insertInfoIfNotContains(it) } 48 | } 49 | 50 | ImportContractsAction.importContractsFromFile(pathToImport, moveProgressBar = false) 51 | 52 | return@let db 53 | } 54 | 55 | Assert.assertEquals(dbToImport.allCallInfos, DatabaseProvider.defaultDatabase!!.allCallInfos) 56 | } 57 | 58 | fun testImportWhenDefaultDBIsNotEmpty() { 59 | val data = setOf( 60 | createCallInfo("A", "foo", listOf("String", "Symbol"), "Integer"), 61 | createCallInfo("B", "bar", listOf("Integer"), "String"), 62 | createCallInfo("C", "foobar", listOf("String"), "String") 63 | ) 64 | 65 | val defaultDBData = setOf( 66 | createCallInfo("A", "foo", listOf("String", "Symbol"), "Integer"), 67 | createCallInfo("B", "bar", listOf("String"), "String"), 68 | createCallInfo("D", "baz", listOf("Integer"), "String"), 69 | createCallInfo("E", "foobar", listOf("String", "Symbol"), "String") 70 | ) 71 | 72 | DatabaseProvider.connectToDB(generateTempDBFilePath(), isDefaultDatabase = true) 73 | DatabaseProvider.defaultDatabaseTransaction { 74 | defaultDBData.forEach { CallInfoTable.insertInfoIfNotContains(it) } 75 | } 76 | 77 | val dbToImport = generateTempDBFilePath().let { pathToImport: String -> 78 | val db = DatabaseProvider.connectToDB(pathToImport) 79 | 80 | transaction(db) { 81 | data.forEach { CallInfoTable.insertInfoIfNotContains(it) } 82 | } 83 | 84 | ImportContractsAction.importContractsFromFile(pathToImport, moveProgressBar = false) 85 | 86 | return@let db 87 | } 88 | 89 | Assert.assertEquals(dbToImport.allCallInfos.union(defaultDBData), DatabaseProvider.defaultDatabase!!.allCallInfos) 90 | } 91 | 92 | private val Database.allCallInfos: Set 93 | get() = transaction(this) { CallInfoRow.all().map { it.copy() } }.toSet() 94 | 95 | private fun createCallInfo(className: String, methodName: String, unnamedArgsTypes: List, returnType: String): CallInfo { 96 | val args = unnamedArgsTypes.mapIndexed { index, s -> ArgumentNameAndType(('a' + index).toString(), s) } 97 | return CallInfoImpl(MethodInfo(ClassInfo(className), methodName, RVisibility.PUBLIC), emptyList(), args, returnType) 98 | } 99 | 100 | private fun generateTempDBFilePath(prefix: String = ""): String { 101 | val dirForTempFiles = System.getProperty("java.io.tmpdir") 102 | return Paths.get(dirForTempFiles, prefix + UUID.randomUUID()).toString() + DatabaseProvider.H2_DB_FILE_EXTENSION 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/anonymous_module_method_call_test.rb: -------------------------------------------------------------------------------- 1 | module A 2 | def self.foo(a, b) 3 | true 4 | end 5 | end 6 | 7 | A.foo("hey", :symbol) -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/call_info_of_nested_class_test.rb: -------------------------------------------------------------------------------- 1 | module M 2 | class A 3 | def foo(a) 4 | a 5 | end 6 | end 7 | end 8 | 9 | a = M::A.new 10 | a.foo(a) -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/duplicates_in_callinfo_table_test.rb: -------------------------------------------------------------------------------- 1 | def foo(a) 2 | if a == "str" 3 | return a 4 | end 5 | false 6 | end 7 | 8 | foo("str") 9 | foo("not str") 10 | 3.times { foo("str") } 11 | 3.times { foo(false) } 12 | -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/forget_call_info_when_arguments_number_changed_test_part_1.rb: -------------------------------------------------------------------------------- 1 | class A 2 | def foo(a) 3 | :symbol 4 | end 5 | end 6 | 7 | A.new.foo("Hey") 8 | -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/forget_call_info_when_arguments_number_changed_test_part_2.rb: -------------------------------------------------------------------------------- 1 | class A 2 | def foo(a, b) 3 | b 4 | end 5 | end 6 | 7 | A.new.foo(true, false) 8 | -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/in_project_root_test/gem_like.rb: -------------------------------------------------------------------------------- 1 | def catch(a); end 2 | 3 | def dont_catch_2(a); end 4 | 5 | def catch_2(a) 6 | dont_catch_2(a) 7 | end 8 | 9 | def dont_catch_3(&a) 10 | yield(a) 11 | end 12 | 13 | def catch_3(&a) 14 | dont_catch_3(&a) 15 | end 16 | -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/in_project_root_test/in_project_root_test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'gem_like' 2 | 3 | catch('hey') 4 | 5 | catch_2('bro') 6 | 7 | def foo(a); end 8 | 9 | catch_3(&method(:foo)) 10 | -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/merge_test1.rb: -------------------------------------------------------------------------------- 1 | class A 2 | 3 | end 4 | 5 | class C 6 | 7 | end 8 | 9 | class B1 10 | def test1 11 | 12 | end 13 | 14 | def test2 15 | 16 | end 17 | end 18 | 19 | class B2 20 | def test3 21 | 22 | end 23 | 24 | def test4 25 | 26 | end 27 | end 28 | 29 | A.class_eval < -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/merge_test1_to_run.rb: -------------------------------------------------------------------------------- 1 | class A 2 | 3 | end 4 | 5 | class C 6 | 7 | end 8 | 9 | class B1 10 | def test1 11 | 12 | end 13 | 14 | def test2 15 | 16 | end 17 | end 18 | 19 | class B2 20 | def test3 21 | 22 | end 23 | 24 | def test4 25 | 26 | end 27 | end 28 | 29 | A.class_eval < -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/merge_test2_to_run.rb: -------------------------------------------------------------------------------- 1 | class A 2 | 3 | end 4 | 5 | class C 6 | 7 | end 8 | 9 | class B1 10 | def test1 11 | 12 | end 13 | 14 | def test2 15 | 16 | end 17 | end 18 | 19 | class B2 20 | def test3 21 | 22 | end 23 | 24 | def test4 25 | 26 | end 27 | end 28 | 29 | A.class_eval < -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/multiple_execution_test2_to_run.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | 3 | class A 4 | 5 | end 6 | 7 | class C 8 | 9 | end 10 | 11 | class B 12 | def test1 13 | 14 | end 15 | 16 | def test2 17 | 18 | end 19 | end 20 | 21 | A.class_eval < -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/ref_links_test_to_run.rb: -------------------------------------------------------------------------------- 1 | class A 2 | 3 | end 4 | 5 | class B 6 | def test1 7 | 8 | end 9 | 10 | def test2 11 | 12 | end 13 | end 14 | 15 | A.class_eval < -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/sample_kw_test_to_run.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | 3 | class A 4 | 5 | end 6 | 7 | class C 8 | 9 | end 10 | 11 | class B 12 | def test1 13 | 14 | end 15 | 16 | def test2 17 | 18 | end 19 | end 20 | 21 | A.class_eval < -------------------------------------------------------------------------------- /ide-plugin/src/test/testData/sample_test_to_run.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | 3 | class A 4 | 5 | end 6 | 7 | class C 8 | 9 | end 10 | 11 | class B 12 | def test1 13 | 14 | end 15 | 16 | def test2 17 | 18 | end 19 | end 20 | 21 | A.class_eval < 12 | 13 | /** 14 | * Types of named arguments sorted alphabetically by [ArgumentNameAndType.name] 15 | */ 16 | val namedArguments: List 17 | 18 | val returnType: String 19 | 20 | /** 21 | * Join [unnamedArguments] to raw [String] which is used in database 22 | */ 23 | fun unnamedArgumentsTypesJoinToRawString(): String 24 | 25 | /** 26 | * Join [namedArgumentsJoinToRawString] to raw [String] which is used in database. 27 | * Should return concatenated string containing elements ordered by argument name alphabetically 28 | */ 29 | fun namedArgumentsJoinToRawString(): String 30 | 31 | fun getTypeNameByArgumentName(name: String): String? { 32 | return (unnamedArguments.find { it.name == name } ?: namedArguments.find { it.name == name })?.type 33 | } 34 | } 35 | 36 | data class ArgumentNameAndType(val name: String, val type: String) { 37 | companion object { 38 | const val NAME_AND_TYPE_SEPARATOR = "," 39 | /** 40 | * For such method: 41 | * def foo(a, b = 1); end 42 | * 43 | * And such call: 44 | * foo(a) 45 | * `b` is implicitly passed 46 | */ 47 | const val IMPLICITLY_PASSED_ARGUMENT_TYPE = "-" 48 | } 49 | } 50 | 51 | class CallInfoImpl(override val methodInfo: MethodInfo, 52 | namedArguments: List, 53 | override val unnamedArguments: List, 54 | override val returnType: String) : CallInfo { 55 | override val namedArguments = namedArguments.sortedBy { it.name } 56 | 57 | override fun namedArgumentsJoinToRawString(): String = 58 | namedArguments.joinToString(separator = ARGUMENTS_TYPES_SEPARATOR) { it.name + "," + it.type } 59 | 60 | override fun unnamedArgumentsTypesJoinToRawString(): String = 61 | unnamedArguments.joinToString(separator = ARGUMENTS_TYPES_SEPARATOR) { it.name + "," + it.type } 62 | 63 | override fun equals(other: Any?): Boolean { 64 | if (this === other) return true 65 | if (other !is CallInfo) return false 66 | 67 | other as CallInfoImpl 68 | 69 | return methodInfo == other.methodInfo && 70 | unnamedArguments == other.unnamedArguments && 71 | returnType == other.returnType && 72 | namedArguments == other.namedArguments 73 | } 74 | 75 | override fun hashCode(): Int { 76 | var result = methodInfo.hashCode() 77 | result = 31 * result + unnamedArguments.hashCode() 78 | result = 31 * result + returnType.hashCode() 79 | result = 31 * result + namedArguments.hashCode() 80 | return result 81 | } 82 | 83 | override fun toString(): String { 84 | return "CallInfoIml(methodInfo=$methodInfo, namedArguments=$namedArguments, unnamedArguments=$unnamedArguments, returnType=$returnType)" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/ClassInfo.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature 2 | 3 | 4 | interface ClassInfo { 5 | val gemInfo: GemInfo? 6 | val classFQN: String 7 | 8 | 9 | data class Impl(override val gemInfo: GemInfo?, override val classFQN: String) : ClassInfo 10 | 11 | fun validate(): Boolean { 12 | if (classFQN.length > LENGTH_OF_FQN) { 13 | return false 14 | } 15 | val gemInfoVal = gemInfo 16 | return gemInfoVal == null || gemInfoVal.validate() 17 | } 18 | 19 | companion object { 20 | val LENGTH_OF_FQN = 200 21 | } 22 | 23 | } 24 | 25 | 26 | fun ClassInfo(gemInfo: GemInfo?, classFQN: String) = ClassInfo.Impl(gemInfo, classFQN) 27 | 28 | fun ClassInfo(classFQN: String) = ClassInfo.Impl(null, classFQN) 29 | 30 | fun ClassInfo(copy: ClassInfo) = with(copy) { ClassInfo.Impl(gemInfo?.let { GemInfo(it) }, classFQN) } 31 | 32 | -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/GemInfo.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature 2 | 3 | interface GemInfo { 4 | val name: String 5 | val version: String 6 | 7 | data class Impl(override val name: String, override val version: String) : GemInfo 8 | 9 | fun validate(): Boolean { 10 | return name.length <= LENGTH_OF_GEMNAME && 11 | version.length <= LENGTH_OF_GEMVERSION 12 | } 13 | 14 | companion object { 15 | val NONE = Impl("", "") 16 | val LENGTH_OF_GEMNAME = 50 17 | val LENGTH_OF_GEMVERSION = 50 18 | } 19 | } 20 | 21 | fun GemInfo(name: String, version: String) = GemInfo.Impl(name, version) 22 | 23 | fun GemInfo(copy: GemInfo) = with(copy) { GemInfo.Impl(name, version) } 24 | 25 | fun GemInfoOrNull(name: String, version: String) = GemInfo(name, version).let { if (it == GemInfo.NONE) null else it} 26 | 27 | fun gemInfoFromFilePathOrNull(path: String): GemInfo? { 28 | val gemPathPattern = """([A-Za-z0-9_-]+)-(\d+[0-9A-Za-z.]+)""" 29 | val regex = Regex(gemPathPattern) 30 | val (gemName, gemVersion) = regex.findAll(path).lastOrNull()?.destructured ?: return null 31 | return GemInfo(gemName, gemVersion) 32 | } -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/MethodInfo.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature 2 | 3 | interface MethodInfo { 4 | val classInfo: ClassInfo 5 | val name: String 6 | val visibility: RVisibility 7 | val location: Location? 8 | 9 | data class Impl(override val classInfo: ClassInfo, 10 | override val name: String, 11 | override val visibility: RVisibility = RVisibility.PUBLIC, 12 | override val location: Location? = null) : MethodInfo 13 | 14 | fun validate(): Boolean { 15 | if (name.length > LENGTH_OF_NAME) { 16 | return false 17 | } 18 | val loc = location 19 | if (loc == null || loc.path.length > LENGTH_OF_PATH) { 20 | return false 21 | } 22 | return classInfo.validate() 23 | } 24 | 25 | companion object { 26 | val LENGTH_OF_NAME = 100 27 | val LENGTH_OF_PATH = 1000 28 | } 29 | } 30 | 31 | @JvmOverloads 32 | fun MethodInfo(classInfo: ClassInfo, name: String, visibility: RVisibility, location: Location? = null) = 33 | MethodInfo.Impl(classInfo, name, visibility, location) 34 | 35 | fun MethodInfo(copy: MethodInfo) = with(copy) { MethodInfo.Impl(ClassInfo(classInfo), name, visibility, location) } 36 | 37 | data class Location(val path: String, val lineno: Int) 38 | 39 | enum class RVisibility constructor(val value: Byte, val presentableName: String) { 40 | PRIVATE(0, "PRIVATE"), 41 | PROTECTED(1, "PROTECTED"), 42 | PUBLIC(2, "PUBLIC"); 43 | } -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/ParameterInfo.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public class ParameterInfo { 6 | @NotNull 7 | private final String myName; 8 | @NotNull 9 | private final ParameterInfo.Type myModifier; 10 | 11 | public ParameterInfo(@NotNull final String name, @NotNull final Type modifier) { 12 | myName = name; 13 | myModifier = modifier; 14 | } 15 | 16 | @NotNull 17 | public String getName() { 18 | return myName; 19 | } 20 | 21 | @NotNull 22 | public ParameterInfo.Type getModifier() { 23 | return myModifier; 24 | } 25 | 26 | public boolean isNamedParameter() { 27 | return myModifier == Type.KEY || myModifier == Type.KEYREQ || myModifier == Type.KEYREST; 28 | } 29 | 30 | @Override 31 | public boolean equals(Object o) { 32 | if (this == o) return true; 33 | if (o == null || getClass() != o.getClass()) return false; 34 | 35 | final ParameterInfo that = (ParameterInfo) o; 36 | 37 | //noinspection SimplifiableIfStatement 38 | if (!myName.equals(that.myName)) return false; 39 | return myModifier == that.myModifier; 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | int result = myName.hashCode(); 45 | result = 31 * result + myModifier.hashCode(); 46 | return result; 47 | } 48 | 49 | // parameter info: 50 | // 51 | // def foo(a, # mandatory (REQ) 52 | // b=1, # optional (OPT) 53 | // *c, # rest (REST) 54 | // d, # post (POST) 55 | // e:, # keywords (KEYREQ) 56 | // f:1, # optional keywords (KEY) 57 | // **g, # rest keywords (KEYREST) 58 | // &h) # block 59 | public enum Type { 60 | REQ, 61 | OPT, 62 | POST, 63 | REST, 64 | KEYREQ, 65 | KEY, 66 | KEYREST, 67 | BLOCK, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/RSignatureContractContainer.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature 2 | 3 | class RSignatureContractContainer { 4 | 5 | private val myContracts: MutableMap = HashMap() 6 | private val myNumberOfCalls: MutableMap = HashMap() 7 | 8 | fun acceptTuple(tuple: RTuple): Boolean { 9 | val currInfo = tuple.methodInfo 10 | 11 | val contract = myContracts[currInfo] 12 | return contract != null && tuple.argsInfo == contract.argsInfo && SignatureContract.accept(contract, tuple) 13 | } 14 | 15 | fun addTuple(tuple: RTuple) { 16 | val currInfo = tuple.methodInfo 17 | 18 | if (myContracts.containsKey(currInfo)) { 19 | val contract = myContracts[currInfo] 20 | 21 | if (tuple.argsInfo.size == contract?.argsInfo?.size) { 22 | contract.addRTuple(tuple) 23 | myNumberOfCalls.compute(currInfo) { _, oldNumber -> (oldNumber ?: 0) + 1 } 24 | } 25 | } else { 26 | val contract = RSignatureContract(tuple) 27 | myContracts.put(currInfo, contract) 28 | } 29 | } 30 | 31 | val registeredMethods: Set 32 | get() = myContracts.keys 33 | 34 | fun getSignature(info: MethodInfo): RSignatureContract? { 35 | return myContracts[info]?.apply { minimize() } 36 | } 37 | 38 | fun clear() { 39 | myContracts.clear() 40 | } 41 | 42 | val size: Int 43 | get() = myContracts.size 44 | } -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/RSignatureContractNode.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.ContractTransition; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | public class RSignatureContractNode implements SignatureNode { 10 | 11 | @NotNull 12 | private final Map myTransitions; 13 | 14 | public RSignatureContractNode() { 15 | myTransitions = new HashMap<>(); 16 | } 17 | 18 | public void addLink(final @NotNull ContractTransition transition, @NotNull SignatureNode arrivalNode) { 19 | myTransitions.put(transition, arrivalNode); 20 | } 21 | 22 | @NotNull 23 | @Override 24 | public Map getTransitions() { 25 | return myTransitions; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/RTuple.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.List; 6 | 7 | public class RTuple { 8 | 9 | @NotNull 10 | private final MethodInfo myMethodInfo; 11 | 12 | @NotNull 13 | private final List myArgsInfo; 14 | @NotNull 15 | private final List myArgsTypes; 16 | @NotNull 17 | private final String myReturnTypeName; 18 | 19 | public RTuple(@NotNull final MethodInfo methodInfo, 20 | @NotNull final List argsInfo, 21 | @NotNull final List argsTypeName, 22 | @NotNull final String returnTypeName) { 23 | myMethodInfo = methodInfo; 24 | myArgsInfo = argsInfo; 25 | myArgsTypes = argsTypeName; 26 | myReturnTypeName = returnTypeName; 27 | } 28 | 29 | @NotNull 30 | public MethodInfo getMethodInfo() { 31 | return myMethodInfo; 32 | } 33 | 34 | @NotNull 35 | public List getArgsInfo() { 36 | return myArgsInfo; 37 | } 38 | 39 | @NotNull 40 | List getArgsTypes() { 41 | return myArgsTypes; 42 | } 43 | 44 | @NotNull 45 | String getReturnTypeName() { 46 | return myReturnTypeName; 47 | } 48 | 49 | @Override 50 | public boolean equals(Object o) { 51 | if (this == o) return true; 52 | if (o == null || getClass() != o.getClass()) return false; 53 | 54 | RTuple that = (RTuple) o; 55 | 56 | return myMethodInfo.equals(that.myMethodInfo) && 57 | myArgsInfo.equals(that.myArgsInfo) && 58 | myArgsTypes.equals(that.myArgsTypes); 59 | 60 | } 61 | 62 | @Override 63 | public int hashCode() { 64 | int result = myMethodInfo.hashCode(); 65 | result = 31 * result + myArgsInfo.hashCode(); 66 | result = 31 * result + myArgsTypes.hashCode(); 67 | return result; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/SignatureInfo.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature 2 | 3 | interface SignatureInfo { 4 | val methodInfo: MethodInfo 5 | val contract: SignatureContract 6 | 7 | data class Impl(override val methodInfo: MethodInfo, override val contract: SignatureContract) : SignatureInfo 8 | } 9 | 10 | fun SignatureInfo(methodInfo: MethodInfo, contract: SignatureContract) = SignatureInfo.Impl(methodInfo, contract) 11 | 12 | fun SignatureInfo(copy: SignatureInfo) = with(copy) { SignatureInfo.Impl(MethodInfo(methodInfo), contract) } -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/contractTransition/ContractTransition.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature.contractTransition; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.List; 6 | import java.util.Set; 7 | 8 | public interface ContractTransition { 9 | /** 10 | * Return literal type set of this transition. This method respects reference transitions 11 | * which types depend on some previous passed values. 12 | * 13 | * @param readTypes previously read literal types. Set represents possible type unions 14 | * @return computed literal type set for this transition 15 | */ 16 | @NotNull 17 | Set getValue(@NotNull List> readTypes); 18 | } 19 | -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/contractTransition/ReferenceContractTransition.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature.contractTransition; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | public class ReferenceContractTransition implements ContractTransition { 10 | 11 | private final int myMask; 12 | 13 | public ReferenceContractTransition(int mask) { 14 | myMask = mask; 15 | } 16 | 17 | @NotNull 18 | @Override 19 | public Set getValue(@NotNull List> readTypes) { 20 | int tmpMask = myMask; 21 | int cnt = 0; 22 | 23 | Set ans = null; 24 | 25 | while (tmpMask > 0) { 26 | if (tmpMask % 2 == 1) { 27 | if (ans == null) { 28 | ans = new HashSet<>(readTypes.get(cnt)); 29 | } else { 30 | ans.retainAll(readTypes.get(cnt)); 31 | } 32 | } 33 | 34 | tmpMask /= 2; 35 | cnt++; 36 | } 37 | 38 | return ans; 39 | } 40 | 41 | public int getMask() { 42 | return myMask; 43 | } 44 | 45 | @Override 46 | public boolean equals(Object o) { 47 | if (this == o) return true; 48 | if (o == null || getClass() != o.getClass()) return false; 49 | 50 | ReferenceContractTransition that = (ReferenceContractTransition) o; 51 | 52 | return myMask == that.myMask; 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | return myMask; 58 | } 59 | } -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/contractTransition/TransitionHelper.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature.contractTransition; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.List; 6 | 7 | public class TransitionHelper { 8 | private TransitionHelper() { 9 | } 10 | 11 | @NotNull 12 | public static ContractTransition calculateTransition(@NotNull List argTypes, int argIndex, @NotNull String type) { 13 | final int mask = getNewMask(argTypes, argIndex, type); 14 | 15 | if (mask > 0) 16 | return new ReferenceContractTransition(mask); 17 | else 18 | return new TypedContractTransition(type); 19 | } 20 | 21 | private static int getNewMask(@NotNull List argsTypes, int argIndex, @NotNull String type) { 22 | int tempMask = 0; 23 | 24 | for (int i = argIndex - 1; i >= 0; i--) { 25 | tempMask <<= 1; 26 | 27 | if (argsTypes.get(i).equals(type)) { 28 | tempMask |= 1; 29 | } 30 | } 31 | 32 | return tempMask; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/contractTransition/TypedContractTransition.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature.contractTransition; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | public class TypedContractTransition implements ContractTransition { 10 | 11 | @NotNull 12 | private final String myType; 13 | 14 | public TypedContractTransition(@NotNull String type) { 15 | this.myType = type; 16 | } 17 | 18 | @NotNull 19 | @Override 20 | public Set getValue(@NotNull List> readTypes) { 21 | return Collections.singleton(myType); 22 | } 23 | 24 | @NotNull 25 | public String getType() { 26 | return myType; 27 | } 28 | 29 | @Override 30 | public boolean equals(Object o) { 31 | if (this == o) return true; 32 | if (o == null || getClass() != o.getClass()) return false; 33 | 34 | TypedContractTransition that = (TypedContractTransition) o; 35 | 36 | return myType.equals(that.myType); 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | return myType.hashCode(); 42 | } 43 | } -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/serialization/RmcDirectory.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature.serialization 2 | 3 | import org.jetbrains.ruby.codeInsight.types.signature.GemInfo 4 | import org.jetbrains.ruby.codeInsight.types.signature.SignatureInfo 5 | import java.io.* 6 | import java.util.zip.GZIPInputStream 7 | import java.util.zip.GZIPOutputStream 8 | 9 | interface RmcDirectory { 10 | fun save(gemInfo: GemInfo, signatures: List) 11 | 12 | fun listGems() : List 13 | 14 | fun load(gemInfo: GemInfo): List 15 | 16 | } 17 | 18 | class RmcDirectoryImpl(private val directory: File) : RmcDirectory { 19 | init { 20 | if (!directory.exists() || !directory.isDirectory) { 21 | throw IOException("Existing directory excepted") 22 | } 23 | } 24 | 25 | override fun load(gemInfo: GemInfo): List { 26 | val inputFile = File(directory, gemInfo2Filename(gemInfo)) 27 | FileInputStream(inputFile).use { 28 | GZIPInputStream(it).use { 29 | DataInputStream(it).use { 30 | return SignatureInfoSerialization.deserialize(it) 31 | } 32 | } 33 | } 34 | } 35 | 36 | override fun save(gemInfo: GemInfo, signatures: List) { 37 | val outputFile = File(directory, gemInfo2Filename(gemInfo)) 38 | FileOutputStream(outputFile).use { 39 | GZIPOutputStream(it).use { 40 | DataOutputStream(it).use { 41 | SignatureInfoSerialization.serialize(signatures, it) 42 | } 43 | } 44 | } 45 | } 46 | 47 | override fun listGems(): List = directory.listFiles().mapNotNull { file2GemInfo(it) } 48 | 49 | private fun gemInfo2Filename(gemInfo: GemInfo) = "${gemInfo.name}-${gemInfo.version}.rmc" 50 | 51 | private fun file2GemInfo(file: File): GemInfo? { 52 | if (file.extension != "rmc") { 53 | return null 54 | } 55 | val name = file.nameWithoutExtension.substringBeforeLast('-') 56 | val version = file.nameWithoutExtension.substringAfterLast('-') 57 | if (name == "" || version == "") { 58 | return null 59 | } 60 | return GemInfo(name, version) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/serialization/SignatureContractSerialization.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature.serialization 2 | 3 | import org.jetbrains.ruby.codeInsight.types.signature.* 4 | import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.ContractTransition 5 | import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.ReferenceContractTransition 6 | import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.TypedContractTransition 7 | import java.io.DataInput 8 | import java.io.DataOutput 9 | import java.util.* 10 | import kotlin.collections.ArrayList 11 | import kotlin.collections.HashMap 12 | 13 | fun ContractTransition.serialize(stream: DataOutput) { 14 | stream.writeBoolean(this is ReferenceContractTransition) 15 | when (this) { 16 | is ReferenceContractTransition -> stream.writeInt(mask) 17 | is TypedContractTransition -> stream.writeUTF(type) 18 | else -> throw IllegalStateException("ContractTransition should be sealed in these classes") 19 | } 20 | } 21 | 22 | fun ContractTransition(stream: DataInput): ContractTransition { 23 | val type = stream.readBoolean() 24 | return when (type) { 25 | true -> ReferenceContractTransition(stream.readInt()) 26 | false -> TypedContractTransition(stream.readUTF()) 27 | } 28 | } 29 | 30 | fun ParameterInfo.serialize(stream: DataOutput) { 31 | stream.writeUTF(name) 32 | stream.writeByte(modifier.ordinal) 33 | } 34 | 35 | fun ParameterInfo(stream: DataInput): ParameterInfo { 36 | return ParameterInfo(stream.readUTF(), ParameterInfo.Type.values()[stream.readByte().toInt()]) 37 | } 38 | 39 | fun SignatureContract.serialize(stream: DataOutput) { 40 | stream.writeInt(argsInfo.size) 41 | argsInfo.forEach { it.serialize(stream) } 42 | 43 | stream.writeInt(nodeCount) 44 | 45 | val visited = HashMap() 46 | val q = ArrayDeque() 47 | 48 | visited[startNode] = 0 49 | q.push(startNode) 50 | 51 | while (q.isNotEmpty()) { 52 | val v = q.poll() 53 | for (it in v.transitions.values) { 54 | if (!visited.containsKey(it)) { 55 | visited[it] = visited.size 56 | q.add(it) 57 | } 58 | } 59 | 60 | stream.writeInt(v.transitions.size) 61 | v.transitions.forEach { transition, u -> 62 | stream.writeInt(visited[u]!!) 63 | transition.serialize(stream) 64 | } 65 | } 66 | } 67 | 68 | fun SignatureContract(stream: DataInput): SignatureContract { 69 | val argsSize = stream.readInt() 70 | val argsInfo = List(argsSize) { ParameterInfo(stream) } 71 | 72 | val nodesSize = stream.readInt() 73 | 74 | val nodes = List(nodesSize) { RSignatureContractNode() } 75 | 76 | val distance = IntArray(nodesSize, { 0 }) 77 | 78 | repeat(nodesSize) { currentNodeIndex -> 79 | val transitionsN = stream.readInt() 80 | 81 | repeat(transitionsN) { 82 | val toIndex = stream.readInt() 83 | distance[toIndex] = distance[currentNodeIndex] + 1 84 | val transition = ContractTransition(stream) 85 | // todo replace with constructor (iterate from the end) 86 | nodes[currentNodeIndex].addLink(transition, nodes[toIndex]) 87 | } 88 | } 89 | 90 | val levels = List(argsSize + 2) { ArrayList() } 91 | nodes.indices.forEach { 92 | levels[distance[it]].add(nodes[it]) 93 | } 94 | 95 | return RSignatureContract(argsInfo, nodes.first(), nodes.last(), levels) 96 | 97 | } 98 | -------------------------------------------------------------------------------- /ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/serialization/TestSerialization.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature.serialization 2 | 3 | import java.io.DataInput 4 | import java.io.DataOutput 5 | import java.util.* 6 | 7 | class StringDataOutput : DataOutput { 8 | val result = StringBuilder() 9 | 10 | private var wasNewline = true 11 | 12 | fun newline() { 13 | result.append("\n") 14 | wasNewline = true 15 | } 16 | 17 | private fun writeSpace() { 18 | if (!wasNewline) { 19 | result.append(' ') 20 | } 21 | wasNewline = false 22 | } 23 | 24 | override fun writeShort(v: Int): Unit = TODO("not implemented") 25 | 26 | override fun writeLong(v: Long): Unit = TODO("not implemented") 27 | 28 | override fun writeDouble(v: Double): Unit = TODO("not implemented") 29 | 30 | override fun writeBytes(s: String?): Unit = TODO("not implemented") 31 | 32 | override fun writeByte(v: Int) { 33 | writeSpace() 34 | result.append(v) 35 | } 36 | 37 | override fun writeFloat(v: Float): Unit = TODO("not implemented") 38 | 39 | override fun write(b: Int): Unit = TODO("not implemented") 40 | 41 | override fun write(b: ByteArray?): Unit = TODO("not implemented") 42 | 43 | override fun write(b: ByteArray?, off: Int, len: Int): Unit = TODO("not implemented") 44 | 45 | override fun writeChars(s: String?): Unit = TODO("not implemented") 46 | 47 | override fun writeChar(v: Int): Unit = TODO("not implemented") 48 | 49 | override fun writeBoolean(v: Boolean) { 50 | writeSpace() 51 | result.append(if (v) '1' else '0') 52 | } 53 | 54 | override fun writeUTF(s: String?) { 55 | writeSpace() 56 | result.append(s) 57 | } 58 | 59 | override fun writeInt(v: Int) { 60 | writeSpace() 61 | result.append(v) 62 | } 63 | } 64 | 65 | class StringDataInput(s: String) : DataInput { 66 | private val scanner = Scanner(s) 67 | 68 | override fun readFully(b: ByteArray?): Unit = TODO("not implemented") 69 | 70 | override fun readFully(b: ByteArray?, off: Int, len: Int): Unit = TODO("not implemented") 71 | 72 | override fun readInt(): Int = scanner.nextInt() 73 | 74 | override fun readUnsignedShort(): Int = TODO("not implemented") 75 | 76 | override fun readUnsignedByte(): Int = TODO("not implemented") 77 | 78 | override fun readUTF(): String = scanner.next() 79 | 80 | override fun readChar(): Char = TODO("not implemented") 81 | 82 | override fun readLine(): String = TODO("not implemented") 83 | 84 | override fun readByte(): Byte = scanner.nextByte() 85 | 86 | override fun readFloat(): Float = TODO("not implemented") 87 | 88 | override fun skipBytes(n: Int): Int = TODO("not implemented") 89 | 90 | override fun readLong(): Long = TODO("not implemented") 91 | 92 | override fun readDouble(): Double = TODO("not implemented") 93 | 94 | override fun readBoolean(): Boolean = (scanner.nextInt() == 1) 95 | 96 | override fun readShort(): Short = TODO("not implemented") 97 | } -------------------------------------------------------------------------------- /ruby-call-signature/src/test/java/org/jetbrains/ruby/codeInsight/types/signature/GemInfoFromPathTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature 2 | 3 | import junit.framework.TestCase 4 | import org.junit.Test 5 | 6 | class GemInfoFromPathTest : TestCase() { 7 | private fun doTest(path: String, gemName: String, gemVersion: String) { 8 | assertEquals(GemInfoOrNull(gemName, gemVersion), gemInfoFromFilePathOrNull(path)) 9 | } 10 | 11 | @Test 12 | fun testToplevel() { 13 | doTest("/home/valich/foo.rb", "", "") 14 | } 15 | 16 | @Test 17 | fun testRubyBundled() { 18 | doTest("/Users/valich/.rvm/rubies/ruby-2.3.3/lib/ruby/2.3.0/mkmf.rb", "ruby", "2.3.3") 19 | } 20 | 21 | @Test 22 | fun testRakeRVM() { 23 | doTest("/Users/valich/.rvm/rubies/ruby-2.3.3/lib/ruby/gems/2.3.0/gems/rake-10.4.2/lib/rake.rb", 24 | "rake", "10.4.2") 25 | } 26 | 27 | @Test 28 | fun testGemNameWithDashes() { 29 | doTest("/Users/valich/.rvm/gems/ruby-2.3.3/gems/debase-ruby_core_source-0.9.9/lib/debase/ruby_core_source.rb", 30 | "debase-ruby_core_source", "0.9.9") 31 | } 32 | 33 | @Test 34 | fun testMacPreinstalledGem() { 35 | doTest("/System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/ruby/gems/2.3.0/gems/sqlite3-1.3.11/lib/sqlite3.rb", 36 | "sqlite3", "1.3.11") 37 | } 38 | 39 | @Test 40 | fun testMacSystemGem() { 41 | doTest("/Users/valich/.gem/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record.rb", 42 | "activerecord", "5.0.1") 43 | } 44 | } -------------------------------------------------------------------------------- /ruby-call-signature/src/test/java/org/jetbrains/ruby/codeInsight/types/signature/SignatureContractMergeTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature 2 | 3 | import org.junit.Test 4 | 5 | class SignatureContractMergeTest : SignatureContractTestBase() { 6 | 7 | @Test 8 | fun testSimpleMerge() { 9 | val contract = generateSimpleContract() 10 | 11 | val testArgs1 = listOf("Int1", "Int2", "Int3") 12 | val testArgs2 = listOf("String1", "Int2", "Int3") 13 | 14 | val testTuple1 = generateRTuple(testArgs1, "String4") 15 | val testTuple2 = generateRTuple(testArgs2, "String4") 16 | 17 | assertTrue(SignatureContract.accept(contract, testTuple1)) 18 | assertFalse(SignatureContract.accept(contract, testTuple2)) 19 | 20 | checkSerialization(contract, MergeTestData.testSimpleMerge) 21 | } 22 | 23 | @Test 24 | fun testComplicatedMerge() { 25 | val testArgs1 = listOf("a1", "b2", "a3", "d4") 26 | val testArgs2 = listOf("a1", "c2", "b3", "d4") 27 | val testTuple1 = generateRTuple(testArgs1, "a5") 28 | val testTuple2 = generateRTuple(testArgs2, "a5") 29 | 30 | val contract = generateComplicatedContract() 31 | 32 | assertFalse(SignatureContract.accept(contract, testTuple1)) 33 | assertTrue(SignatureContract.accept(contract, testTuple2)) 34 | 35 | checkSerialization(contract, MergeTestData.testComplicatedMerge) 36 | } 37 | 38 | @Test 39 | fun testMultipleReturnTypeMerge() { 40 | val contract = generateMultipleReturnTypeContract() 41 | checkSerialization(contract, MergeTestData.testMultipleReturnTypeMerge) 42 | } 43 | 44 | @Test 45 | fun testAdd() { 46 | val testArgs1 = listOf("String1", "Date2", "String3") 47 | val testTuple1 = generateRTuple(testArgs1, "String4") 48 | 49 | val contract = generateAddContract() 50 | assertTrue(SignatureContract.accept(contract, testTuple1)) 51 | 52 | checkSerialization(contract, MergeTestData.testAddResult) 53 | } 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /ruby-call-signature/src/test/java/org/jetbrains/ruby/codeInsight/types/signature/SignatureContractSerializationTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature 2 | 3 | import org.jetbrains.ruby.codeInsight.types.signature.serialization.* 4 | import org.junit.Test 5 | import java.io.ByteArrayInputStream 6 | import java.io.ByteArrayOutputStream 7 | import java.io.DataInputStream 8 | import java.io.DataOutputStream 9 | import java.util.zip.GZIPInputStream 10 | import java.util.zip.GZIPOutputStream 11 | 12 | class SignatureContractSerializationTest : SignatureContractTestBase() { 13 | 14 | private fun checkSignaturesSerialization(signatures: List, 15 | newSignatures: List, contractsTestData: List) { 16 | assertTrue(signatures.size == newSignatures.size) 17 | for (i in 0 until newSignatures.size) { 18 | assertTrue(signatures[i].methodInfo == newSignatures[i].methodInfo) 19 | checkSerialization(signatures[i].contract, contractsTestData[i % 4]) 20 | } 21 | } 22 | 23 | private fun generateSignatures(): Pair, List> { 24 | val gems = listOf( 25 | GemInfo("gem", "1.2.3"), 26 | GemInfo("anothergem", "3.4.5"), 27 | GemInfo("supergem", "0.99") 28 | ) 29 | val classNames = listOf("A::B::C", 30 | "B::C::D", 31 | "D::E::F") 32 | 33 | val classes = gems.map { gem -> classNames.map { ClassInfo(gem, it) } }.flatten() 34 | val methodNames = listOf("foo", "bar", "baz", "foobar") 35 | val methods = classes.map { clazz -> methodNames.map { MethodInfo(clazz, it, RVisibility.PUBLIC) } }.flatten() 36 | val contracts = listOf(generateSimpleContract(), generateComplicatedContract(), 37 | generateMultipleReturnTypeContract(), generateAddContract()) 38 | val contractsTestData = listOf(MergeTestData.testSimpleMerge, MergeTestData.testComplicatedMerge, 39 | MergeTestData.testMultipleReturnTypeMerge, MergeTestData.testAddResult) 40 | assertTrue(contracts.size == contractsTestData.size) 41 | 42 | var idx = 0 43 | val signatures = methods.map { SignatureInfo(it, contracts[idx++ % contracts.size]) } 44 | return Pair(contractsTestData, signatures) 45 | } 46 | 47 | 48 | private fun doTest(contract: String) { 49 | val normalizedInput = contract.trim().replace('\n', ' ') 50 | val signatureContract = SignatureContract(StringDataInput(normalizedInput)) 51 | val serialized = StringDataOutput().let { 52 | signatureContract.serialize(it) 53 | it.result.toString() 54 | } 55 | 56 | assertEquals(normalizedInput, serialized) 57 | } 58 | 59 | fun testSimple() { 60 | doTest(SignatureTestData.simpleContract) 61 | } 62 | 63 | @Test 64 | fun testSerializationList() { 65 | val (contractsTestData, signatures) = generateSignatures() 66 | 67 | val dataOutput = StringDataOutput() 68 | SignatureInfoSerialization.serialize(signatures, dataOutput) 69 | val newSignatures = SignatureInfoSerialization.deserialize(StringDataInput(dataOutput.result.toString())) 70 | 71 | checkSignaturesSerialization(signatures, newSignatures, contractsTestData) 72 | } 73 | 74 | @Test 75 | fun testBinarySerialization() { 76 | val (contractsTestData, signatures) = generateSignatures() 77 | 78 | val outputStream = ByteArrayOutputStream() 79 | GZIPOutputStream(outputStream).use { 80 | DataOutputStream(outputStream).use { 81 | SignatureInfoSerialization.serialize(signatures, it) 82 | } 83 | } 84 | 85 | val inputStream = ByteArrayInputStream(outputStream.toByteArray()) 86 | GZIPInputStream(inputStream).use { 87 | DataInputStream(inputStream).use { 88 | val newSignatures = SignatureInfoSerialization.deserialize(it) 89 | checkSignaturesSerialization(signatures, newSignatures, contractsTestData) 90 | } 91 | } 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /ruby-call-signature/src/test/java/org/jetbrains/ruby/codeInsight/types/signature/SignatureContractTestBase.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature 2 | 3 | import junit.framework.TestCase 4 | import org.jetbrains.ruby.codeInsight.types.signature.serialization.StringDataOutput 5 | import org.jetbrains.ruby.codeInsight.types.signature.serialization.serialize 6 | 7 | abstract class SignatureContractTestBase : TestCase() { 8 | 9 | protected fun checkSerialization(rContract: SignatureContract, testData: String) { 10 | val serialized = StringDataOutput().let { 11 | rContract.serialize(it) 12 | it.result.toString() 13 | } 14 | 15 | val testDataClean = testData.trim().replace('\n', ' ') 16 | 17 | assertEquals(serialized, testDataClean) 18 | } 19 | 20 | protected fun generateComplicatedContract() : RSignatureContract { 21 | val args1 = listOf("a1", "c2", "a3", "a4") 22 | val args2 = listOf("a1", "b2", "a3", "a4") 23 | val args3 = listOf("a1", "c2", "b3", "a4") 24 | val args4 = listOf("a1", "b2", "b3", "a4") 25 | 26 | val args5 = listOf("a1", "c2", "b3", "d4") 27 | val args6 = listOf("a1", "b2", "b3", "d4") 28 | 29 | val tuple1 = generateRTuple(args1, "e5") 30 | val tuple2 = generateRTuple(args2, "e5") 31 | val tuple3 = generateRTuple(args3, "e5") 32 | val tuple4 = generateRTuple(args4, "e5") 33 | 34 | 35 | val tuple5 = generateRTuple(args5, "a5") 36 | val tuple6 = generateRTuple(args6, "a5") 37 | 38 | val contract1 = RSignatureContract(tuple1) 39 | contract1.addRTuple(tuple2) 40 | contract1.addRTuple(tuple3) 41 | contract1.addRTuple(tuple4) 42 | 43 | contract1.minimize() 44 | 45 | val contract2 = RSignatureContract(tuple5) 46 | contract2.addRTuple(tuple6) 47 | contract2.minimize() 48 | 49 | contract1.mergeWith(contract2) 50 | 51 | return contract1 52 | } 53 | 54 | protected fun generateSimpleContract() : RSignatureContract { 55 | val args1 = listOf("String1", "String2", "String3") 56 | val args2 = listOf("Int1", "String2", "String3") 57 | val args3 = listOf("String1", "Int2", "String3") 58 | val args4 = listOf("Int1", "Int2", "String3") 59 | 60 | val args5 = listOf("Int1", "Int2", "Int3") 61 | 62 | 63 | val tuple1 = generateRTuple(args1, "String4") 64 | val tuple2 = generateRTuple(args2, "String4") 65 | val tuple3 = generateRTuple(args3, "String4") 66 | val tuple4 = generateRTuple(args4, "String4") 67 | 68 | val tuple5 = generateRTuple(args5, "String4") 69 | 70 | val contract1 = RSignatureContract(tuple1) 71 | contract1.addRTuple(tuple2) 72 | contract1.addRTuple(tuple3) 73 | contract1.addRTuple(tuple4) 74 | 75 | contract1.minimize() 76 | 77 | val contract2 = RSignatureContract(tuple5) 78 | 79 | contract1.mergeWith(contract2) 80 | 81 | return contract1 82 | } 83 | 84 | 85 | protected fun generateMultipleReturnTypeContract(): RSignatureContract { 86 | val args1 = listOf("a1") 87 | val args2 = listOf("a1") 88 | 89 | val args3 = listOf("a1") 90 | 91 | val tuple1 = generateRTuple(args1, "b2") 92 | val tuple2 = generateRTuple(args2, "c2") 93 | 94 | val tuple3 = generateRTuple(args3, "d2") 95 | 96 | val contract1 = RSignatureContract(tuple1) 97 | contract1.addRTuple(tuple2) 98 | 99 | contract1.minimize() 100 | 101 | val contract2 = RSignatureContract(tuple3) 102 | 103 | contract1.mergeWith(contract2) 104 | return contract1 105 | } 106 | 107 | protected fun generateAddContract(): RSignatureContract { 108 | val testArgs1 = listOf("String1", "Date2", "String3") 109 | val testTuple1 = generateRTuple(testArgs1, "String4") 110 | val args1 = listOf("String1", "String2", "String3") 111 | val args2 = listOf("String1", "Int2", "String3") 112 | 113 | val tuple1 = generateRTuple(args1, "String4") 114 | val tuple2 = generateRTuple(args2, "String4") 115 | 116 | val contract1 = RSignatureContract(tuple1) 117 | contract1.addRTuple(tuple2) 118 | 119 | contract1.minimize() 120 | 121 | val contract2 = RSignatureContract(testTuple1) 122 | 123 | contract1.mergeWith(contract2) 124 | return contract1 125 | } 126 | 127 | protected fun generateRTuple(args: List, returnType: String): RTuple { 128 | val gemInfo = GemInfo.Impl("test_gem", "1.2.3") 129 | val classInfo = ClassInfo.Impl(gemInfo, "TEST1::Fqn") 130 | val location = Location("test1test1", 11) 131 | val methodInfo = MethodInfo.Impl(classInfo, "met1", RVisibility.PUBLIC, location) 132 | 133 | val params = args.indices.map { ParameterInfo("a" + it, ParameterInfo.Type.REQ) } 134 | 135 | return RTuple(methodInfo, params, args, returnType) 136 | } 137 | 138 | object SignatureTestData { 139 | val simpleContract = """ 140 | 1 arg 0 141 | 4 142 | 3 143 | 1 0 a 144 | 2 0 b 145 | 2 0 c 146 | 1 147 | 3 0 d 148 | 1 149 | 3 1 0 150 | 0 151 | """ 152 | 153 | val trivialContract = """ 154 | 0 155 | 2 156 | 1 157 | 1 0 a 158 | 0 159 | """ 160 | 161 | } 162 | 163 | 164 | 165 | object MergeTestData { 166 | val testAddResult = """ 167 | 3 168 | a0 0 169 | a1 0 170 | a2 0 171 | 5 172 | 1 173 | 1 0 String1 174 | 3 175 | 2 0 Int2 176 | 2 0 Date2 177 | 2 0 String2 178 | 1 179 | 3 0 String3 180 | 1 181 | 4 0 String4 182 | 0 183 | """ 184 | val testSimpleMerge = """ 185 | 3 186 | a0 0 187 | a1 0 188 | a2 0 189 | 7 190 | 2 191 | 1 0 Int1 192 | 2 0 String1 193 | 2 194 | 3 0 Int2 195 | 4 0 String2 196 | 2 197 | 4 0 Int2 198 | 4 0 String2 199 | 2 200 | 5 0 Int3 201 | 5 0 String3 202 | 1 203 | 5 0 String3 204 | 1 205 | 6 0 String4 206 | 0 207 | """ 208 | 209 | val testComplicatedMerge = """ 210 | 4 211 | a0 0 212 | a1 0 213 | a2 0 214 | a3 0 215 | 8 216 | 1 217 | 1 0 a1 218 | 2 219 | 2 0 b2 220 | 2 0 c2 221 | 2 222 | 3 0 b3 223 | 4 0 a3 224 | 2 225 | 5 0 d4 226 | 6 0 a4 227 | 1 228 | 6 0 a4 229 | 1 230 | 7 0 a5 231 | 1 232 | 7 0 e5 233 | 0 234 | """ 235 | val testMultipleReturnTypeMerge = """ 236 | 1 237 | a0 0 238 | 3 239 | 1 240 | 1 0 a1 241 | 3 242 | 2 0 b2 243 | 2 0 d2 244 | 2 0 c2 245 | 0 """ 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /screenshots/parameter_type_providing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/ruby-type-inference/df63525a226c4926614a3937546b570b68bc42aa/screenshots/parameter_type_providing.png -------------------------------------------------------------------------------- /screenshots/return_type_providing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/ruby-type-inference/df63525a226c4926614a3937546b570b68bc42aa/screenshots/return_type_providing.png -------------------------------------------------------------------------------- /screenshots/run_with_type_tracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/ruby-type-inference/df63525a226c4926614a3937546b570b68bc42aa/screenshots/run_with_type_tracker.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'ruby-call-signature', 'storage-server-api', 'lambda-update-handler', 'lambda-put-handler' 2 | include 'contract-creator' 3 | include 'ide-plugin' 4 | include 'signature-viewer' 5 | include 'state-tracker' 6 | include 'common' 7 | 8 | -------------------------------------------------------------------------------- /signature-viewer/build.gradle: -------------------------------------------------------------------------------- 1 | version 'unspecified' 2 | 3 | apply plugin: 'java' 4 | 5 | sourceCompatibility = 1.8 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | 12 | dependencies { 13 | compile project(':ruby-call-signature') 14 | compile project(':storage-server-api') 15 | 16 | // compile 'com.h2database:h2:1.4.193' 17 | compile group: 'mysql', name: 'mysql-connector-java', version: '6.0.6' 18 | } 19 | 20 | task runViewer(type: JavaExec) { 21 | classpath sourceSets.main.runtimeClasspath 22 | main = 'org.jetbrains.ruby.runtime.signature.SignatureViewerKt' 23 | } 24 | 25 | task runExport(type: JavaExec) { 26 | if (project.hasProperty("outputDir")) { 27 | args = ["$outputDir"] 28 | } else { 29 | args = ["rmcOutput"] 30 | } 31 | classpath sourceSets.main.runtimeClasspath 32 | main = 'org.jetbrains.ruby.runtime.signature.SignatureExportKt' 33 | } 34 | 35 | task runImport(type: JavaExec) { 36 | if (project.hasProperty("inputDir")) { 37 | args = ["$inputDir"] 38 | } else { 39 | args = ["rmcInput"] 40 | } 41 | classpath sourceSets.main.runtimeClasspath 42 | main = 'org.jetbrains.ruby.runtime.signature.SignatureImportKt' 43 | } 44 | 45 | 46 | sourceSets { 47 | main.java.srcDirs = ['src'] 48 | } -------------------------------------------------------------------------------- /signature-viewer/src/org/jetbrains/ruby/runtime/signature/DBViewer.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.runtime.signature 2 | 3 | import org.jetbrains.exposed.sql.transactions.transaction 4 | import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider 5 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoRow 6 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoTable 7 | 8 | /** 9 | * Just prints content of [CallInfoTable] 10 | */ 11 | fun main(args: Array) { 12 | val dpPath = parseDBViewerCommandLineArgs(args) 13 | DatabaseProvider.connectToDB(dpPath) 14 | 15 | transaction { 16 | val table = CallInfoRow.all().map { it.copy() } 17 | table.forEach { 18 | println("" + 19 | (it.methodInfo.classInfo.gemInfo?.name ?: "No gem") + " " + 20 | (it.methodInfo.classInfo.gemInfo?.version ?: "No version") + " " + 21 | it.methodInfo.location?.path + " " + 22 | it.methodInfo.location?.lineno + " " + 23 | it.methodInfo.visibility + " " + 24 | it.methodInfo.classInfo.classFQN + " " + 25 | it.methodInfo.name + " " + 26 | "args:${it.unnamedArgumentsTypesJoinToRawString()} " + 27 | "return:${it.returnType}") 28 | } 29 | println("Size: ${table.size}") 30 | } 31 | } 32 | 33 | fun parseDBViewerCommandLineArgs(args: Array): String { 34 | if (args.size != 1) { 35 | println("Usage: ") 36 | System.exit(1) 37 | } 38 | return args.single() 39 | } 40 | -------------------------------------------------------------------------------- /signature-viewer/src/org/jetbrains/ruby/runtime/signature/EraseLocation.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.runtime.signature 2 | 3 | import org.jetbrains.exposed.sql.transactions.transaction 4 | import org.jetbrains.exposed.sql.update 5 | import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider 6 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.Location 7 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.MethodInfo 8 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.MethodInfoTable 9 | 10 | /** 11 | * Erases location. This is needed because annotated [MethodInfo]s shouldn't contain any info related to how 12 | * machine of developer who annotated some lib is configured. But [Location] contains home dir, 13 | * .rvm or .rbenv folder, e.t.c so it's needed to be erased for annotated libs. 14 | */ 15 | fun main(args: Array) { 16 | val dpPath = parseDBViewerCommandLineArgs(args) 17 | DatabaseProvider.connectToDB(dpPath) 18 | 19 | transaction { 20 | // This is updateAll 21 | MethodInfoTable.update { 22 | it[MethodInfoTable.locationFile] = null 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /signature-viewer/src/org/jetbrains/ruby/runtime/signature/SignatureExport.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.runtime.signature 2 | 3 | import org.jetbrains.ruby.codeInsight.types.signature.SignatureInfo 4 | import org.jetbrains.ruby.codeInsight.types.signature.serialization.RmcDirectoryImpl 5 | import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider 6 | import org.jetbrains.ruby.codeInsight.types.storage.server.StorageException 7 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.RSignatureProviderImpl 8 | import java.io.File 9 | import java.nio.file.Paths 10 | 11 | const val DB_NAME = "ruby-type-inference-db" + DatabaseProvider.H2_DB_FILE_EXTENSION 12 | 13 | fun main(arg : Array) { 14 | val outputDirPath = parseCommandLine(arg) 15 | DatabaseProvider.connectToDB(Paths.get(outputDirPath, DB_NAME).toString()) 16 | 17 | val outputDir = File(outputDirPath) 18 | 19 | if (!outputDir.exists()) { 20 | outputDir.mkdirs() 21 | } 22 | val rmcDirectory = RmcDirectoryImpl(outputDir) 23 | 24 | 25 | try { 26 | for (gem in RSignatureProviderImpl.registeredGems) { 27 | val signatureInfos = ArrayList() 28 | for (clazz in RSignatureProviderImpl.getRegisteredClasses(gem)) { 29 | RSignatureProviderImpl.getRegisteredMethods(clazz).mapNotNullTo(signatureInfos) { RSignatureProviderImpl.getSignature(it) } 30 | } 31 | rmcDirectory.save(gem, signatureInfos) 32 | } 33 | } catch (e: StorageException) { 34 | e.printStackTrace() 35 | } 36 | 37 | } 38 | 39 | fun parseCommandLine(arg: Array): String { 40 | if (arg.size != 1) { 41 | println("Usage: ") 42 | System.exit(-1) 43 | } 44 | return arg[0] 45 | } 46 | -------------------------------------------------------------------------------- /signature-viewer/src/org/jetbrains/ruby/runtime/signature/SignatureImport.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.runtime.signature 2 | 3 | import org.jetbrains.ruby.codeInsight.types.signature.serialization.RmcDirectoryImpl 4 | import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider 5 | import org.jetbrains.ruby.codeInsight.types.storage.server.StorageException 6 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.RSignatureProviderImpl 7 | import java.io.File 8 | import java.nio.file.Paths 9 | 10 | fun main(arg : Array) { 11 | val outputDirPath = parseCommandLine(arg) 12 | DatabaseProvider.connectToDB(Paths.get(outputDirPath, DB_NAME).toString()) 13 | 14 | val inputDirectory = File(outputDirPath) 15 | 16 | if (!inputDirectory.exists()) { 17 | inputDirectory.mkdirs() 18 | } 19 | val rmcDirectory = RmcDirectoryImpl(inputDirectory) 20 | 21 | try { 22 | rmcDirectory.listGems() 23 | .map { rmcDirectory.load(it) } 24 | .forEach { signatureInfos -> signatureInfos.forEach { RSignatureProviderImpl.putSignature(it) } } 25 | } catch (e: StorageException) { 26 | e.printStackTrace() 27 | } 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /signature-viewer/src/org/jetbrains/ruby/runtime/signature/SignatureViewer.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.runtime.signature 2 | 3 | import org.jetbrains.ruby.codeInsight.types.signature.SignatureContract 4 | import org.jetbrains.ruby.codeInsight.types.signature.SignatureInfo 5 | import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider 6 | import org.jetbrains.ruby.codeInsight.types.storage.server.StorageException 7 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.RSignatureProviderImpl 8 | import java.nio.file.Paths 9 | 10 | fun dumpSignatureInfo(signatureInfo: SignatureInfo) { 11 | val methodInfo = signatureInfo.methodInfo 12 | val classInfo = methodInfo.classInfo 13 | val gemInfo = classInfo.gemInfo!! 14 | println(gemInfo.name + " " + gemInfo.version + " " + classInfo.classFQN) 15 | print(methodInfo.name + " ") 16 | methodInfo.location?.let { print( it.path + " " + it.lineno) } 17 | SignatureContract.getAllReturnTypes(signatureInfo.contract).forEach { print(it + ", ") } 18 | println() 19 | 20 | } 21 | 22 | 23 | fun main(args : Array) { 24 | val outputDirPath = parseCommandLine(args) 25 | DatabaseProvider.connectToDB(Paths.get(outputDirPath, DB_NAME).toString()) 26 | 27 | val map = HashMap>() 28 | 29 | try { 30 | for (gem in RSignatureProviderImpl.registeredGems) { 31 | for (clazz in RSignatureProviderImpl.getRegisteredClasses(gem)) { 32 | for (method in RSignatureProviderImpl.getRegisteredMethods(clazz)) { 33 | val signatureInfo = RSignatureProviderImpl.getSignature(method) 34 | signatureInfo?.let { 35 | var list = map[method.name] 36 | if (list == null) { 37 | list = ArrayList() 38 | list.add(signatureInfo) 39 | map.put(method.name, list) 40 | } else { 41 | list.add(signatureInfo) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | println("All loaded") 48 | 49 | while (true) { 50 | val line = readLine() 51 | map[line]?.let { it.forEach { dumpSignatureInfo(it)} } 52 | } 53 | } catch (e: StorageException) { 54 | e.printStackTrace() 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /signature-viewer/src/org/jetbrains/ruby/runtime/signature/SplitDB.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.runtime.signature 2 | 3 | import org.jetbrains.exposed.sql.Database 4 | import org.jetbrains.exposed.sql.transactions.transaction 5 | import org.jetbrains.ruby.codeInsight.types.signature.GemInfo 6 | import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider 7 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoRow 8 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoTable 9 | import java.nio.file.Paths 10 | 11 | val gemToDBMap = HashMap() 12 | 13 | fun gemToDB(info: GemInfo?, outputDir: String, rubyVersion: String): Database = 14 | gemToDBMap[info] ?: DatabaseProvider.connectToDB(Paths.get(outputDir, 15 | "${info?.name?.plus("-")?.plus(info.version) ?: "no_gem"}-ruby-$rubyVersion" + DatabaseProvider.H2_DB_FILE_EXTENSION).toString()) 16 | .also { gemToDBMap[info] = it } 17 | 18 | fun input(msg: String): String { 19 | println(msg) 20 | return readLine()!! 21 | } 22 | 23 | /** 24 | * This small script splits massive database into small databases. Each 25 | * small database is responsible for particular gem and named accordingly 26 | */ 27 | fun main(args: Array) { 28 | val dpPath = parseDBViewerCommandLineArgs(args) 29 | val input = DatabaseProvider.connectToDB(dpPath) 30 | 31 | val outputDir = input("Enter output dir: ") 32 | val rubyVersion = input("Enter ruby version: ") 33 | 34 | transaction(input) { 35 | CallInfoRow.all().forEach { 36 | val callInfo = it.copy() 37 | transaction(gemToDB(callInfo.methodInfo.classInfo.gemInfo, outputDir, rubyVersion)) { 38 | CallInfoTable.insertInfoIfNotContains(callInfo) 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /state-tracker/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.2.70' 3 | 4 | repositories { 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 9 | } 10 | } 11 | 12 | version 'unspecified' 13 | 14 | apply plugin: 'java' 15 | apply plugin: 'kotlin' 16 | 17 | sourceCompatibility = 1.8 18 | 19 | repositories { 20 | mavenCentral() 21 | } 22 | 23 | dependencies { 24 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 25 | testCompile group: 'junit', name: 'junit', version: '4.12' 26 | } 27 | 28 | compileKotlin { 29 | kotlinOptions.jvmTarget = "1.8" 30 | } 31 | compileTestKotlin { 32 | kotlinOptions.jvmTarget = "1.8" 33 | } 34 | 35 | sourceSets { 36 | main.java.srcDirs = ['src/main/java'] 37 | test.java.srcDirs = ['src/test/java'] 38 | test.resources.srcDirs=['src/test/java/testData'] 39 | } -------------------------------------------------------------------------------- /state-tracker/src/main/java/org/jetbrains/ruby/stateTracker/RubyClassHierarchy.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.stateTracker 2 | 3 | interface RubyClassHierarchy { 4 | val loadPaths: List 5 | 6 | val topLevelConstants: Map 7 | 8 | fun getRubyModule(fqn: String) : RubyModule? 9 | 10 | class Impl(override val loadPaths: List, rubyModules: List, 11 | override val topLevelConstants: Map) : RubyClassHierarchy { 12 | 13 | private val name2modules = rubyModules.associateBy( {it.name} , {it}) 14 | 15 | override fun getRubyModule(fqn: String): RubyModule? { 16 | return name2modules[fqn] 17 | } 18 | } 19 | } 20 | 21 | interface RubyConstant { 22 | val name: String 23 | val type: String 24 | val extended: List 25 | data class Impl(override val name: String, 26 | override val type: String, 27 | override val extended: List) : RubyConstant 28 | } 29 | 30 | interface RubyModule { 31 | val name: String 32 | val classDirectAncestors: List 33 | val instanceDirectAncestors: List 34 | val classMethods: List 35 | val instanceMethods: List 36 | 37 | class Impl(override val name: String, 38 | override val classDirectAncestors: List, 39 | override val instanceDirectAncestors: List, 40 | override val classMethods: List, 41 | override val instanceMethods: List) : RubyModule 42 | } 43 | 44 | interface RubyClass: RubyModule { 45 | val superClass : RubyClass 46 | 47 | class Impl(override val name: String, 48 | override val classDirectAncestors: List, 49 | override val instanceDirectAncestors: List, 50 | override val classMethods: List, 51 | override val instanceMethods: List, 52 | override val superClass: RubyClass) : RubyClass 53 | 54 | companion object : RubyClass { 55 | val EMPTY = this 56 | override val name: String 57 | get() = "" 58 | override val classDirectAncestors: List 59 | get() = emptyList() 60 | override val instanceDirectAncestors: List 61 | get() = emptyList() 62 | override val classMethods: List 63 | get() = emptyList() 64 | override val instanceMethods: List 65 | get() = emptyList() 66 | override val superClass: RubyClass 67 | get() = this 68 | } 69 | } 70 | 71 | interface RubyMethod { 72 | val name: String 73 | val location: Location? 74 | val arguments: List 75 | data class ArgInfo(val kind: ArgumentKind, val name: String) 76 | class Impl(override val name: String, override val location: Location?, 77 | override val arguments: List) : RubyMethod 78 | enum class ArgumentKind { 79 | REQ, 80 | OPT, 81 | REST, 82 | KEY, 83 | KEY_REST, 84 | KEY_REQ, 85 | BLOCK; 86 | 87 | companion object { 88 | fun fromString(name : String): ArgumentKind { 89 | return when (name) { 90 | "req" -> REQ 91 | "opt" -> OPT 92 | "rest" -> REST 93 | "key" -> KEY 94 | "keyrest" -> KEY_REST 95 | "keyreq" -> KEY_REQ 96 | "block" -> BLOCK 97 | else -> throw IllegalArgumentException(name) 98 | } 99 | } 100 | } 101 | } 102 | 103 | } 104 | 105 | interface Location { 106 | val path: String 107 | val lineNo: Int 108 | 109 | data class Impl(override val path: String, override val lineNo: Int) : Location 110 | } 111 | 112 | 113 | -------------------------------------------------------------------------------- /state-tracker/src/test/java/org/jetbrains/ruby/stateTracker/RubyClassHierarchyLoaderNonStandardModuleTypeTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.stateTracker 2 | 3 | import junit.framework.TestCase 4 | import org.junit.Test 5 | 6 | class RubyClassHierarchyLoaderNonStandardModuleTypeTest : TestCase() { 7 | private var classHierarchy : RubyClassHierarchy? = null 8 | 9 | override fun setUp() { 10 | val inputStream = javaClass.classLoader.getResourceAsStream("non-standard-module-type.json") 11 | val inputString = inputStream.bufferedReader().use { it.readText() } 12 | classHierarchy = RubyClassHierarchyLoader.fromJson(inputString) 13 | } 14 | 15 | @Test 16 | fun testHierarchyLoaded() { 17 | assertNotNull(classHierarchy) 18 | classHierarchy?.let { 19 | assertNotNull(it.getRubyModule("AAAA")) 20 | assertNotNull(it.getRubyModule("BBBB")) 21 | } 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /state-tracker/src/test/java/org/jetbrains/ruby/stateTracker/RubyClassHierarchyLoaderTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.stateTracker 2 | 3 | import junit.framework.TestCase 4 | import org.junit.Test 5 | 6 | class RubyClassHierarchyLoaderTest : TestCase() { 7 | 8 | private var classHierarchy : RubyClassHierarchy? = null 9 | 10 | override fun setUp() { 11 | val inputStream = javaClass.classLoader.getResourceAsStream("classes.json") 12 | val inputString = inputStream.bufferedReader().use { it.readText() } 13 | classHierarchy = RubyClassHierarchyLoader.fromJson(inputString) 14 | } 15 | 16 | @Test 17 | fun testHasBasicObject() { 18 | assertNotNull(classHierarchy) 19 | classHierarchy?.let { 20 | assertNotNull(it.getRubyModule("BasicObject")) 21 | } 22 | } 23 | 24 | @Test 25 | fun testIncluded() { 26 | assertNotNull(classHierarchy) 27 | classHierarchy?.let { 28 | val module = it.getRubyModule("Gem::Resolver::Molinillo::Resolver::Resolution") 29 | assertNotNull(module) 30 | assertTrue(module!!.instanceDirectAncestors.any {it.name == "Kernel"}) 31 | assertTrue(module.instanceDirectAncestors.any {it.name == "Gem::Resolver::Molinillo::Delegates::ResolutionState"}) 32 | } 33 | } 34 | 35 | @Test 36 | fun testAllNonDirectAncestorsAreExcluded() { 37 | assertNotNull(classHierarchy) 38 | classHierarchy?.let { 39 | val module = it.getRubyModule("CGI") 40 | assertNotNull(module) 41 | assertTrue(module!!.classDirectAncestors.none {it.name == "Kernel"}) 42 | assertTrue(module.classDirectAncestors.any {it.name == "CGI::Util"}) 43 | } 44 | } 45 | 46 | @Test 47 | fun testSuperClass() { 48 | assertNotNull(classHierarchy) 49 | classHierarchy?.let { 50 | val module = it.getRubyModule("Timeout::Error") as RubyClass 51 | assertTrue(module.superClass.name == "RuntimeError") 52 | } 53 | } 54 | 55 | @Test 56 | fun testHasMethod() { 57 | assertNotNull(classHierarchy) 58 | classHierarchy?.let { 59 | val module = it.getRubyModule("Timeout::Error") as RubyClass 60 | val expectedLocation = Location.Impl("/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/timeout.rb", 61 | 28) 62 | assertTrue(module.instanceMethods.any{it.name == "thread" && it.location == expectedLocation }) 63 | } 64 | } 65 | 66 | @Test 67 | fun testConstants() { 68 | assertNotNull(classHierarchy) 69 | classHierarchy?.let { 70 | val elem = it.topLevelConstants["STDIN"] 71 | assertNotNull(elem) 72 | assertTrue(elem!!.extended.isEmpty()) 73 | assertTrue(elem.name == "STDIN") 74 | assertTrue(elem.type == "IO") 75 | } 76 | } 77 | 78 | @Test 79 | fun testParameters() { 80 | assertNotNull(classHierarchy) 81 | classHierarchy?.let { 82 | val module = it.getRubyModule("Dir::Tmpname")!! 83 | assertTrue(module.classMethods.any { 84 | it.name == "create" && 85 | it.arguments.any { it.kind == RubyMethod.ArgumentKind.KEY_REST } && 86 | it.arguments.any { it.kind == RubyMethod.ArgumentKind.OPT } && 87 | it.arguments.any { it.kind == RubyMethod.ArgumentKind.KEY } && 88 | it.arguments.any { it.kind == RubyMethod.ArgumentKind.REQ } 89 | }) 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /state-tracker/src/test/java/testData/non-standard-module-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "top_level_constants": [ 3 | { 4 | "class_name": "IO", 5 | "extended": [], 6 | "name": "STDIN" 7 | } 8 | ], 9 | "load_path": [ 10 | "/Users/vkkoshelev/.rvm/gems/ruby-2.4.1@global/gems/did_you_mean-1.1.0/li" 11 | ], 12 | 13 | "modules": [ 14 | { 15 | "name": "AAAA", 16 | "type": "BBBB", 17 | "singleton_class_included": [ 18 | ], 19 | "included": [ 20 | ], 21 | "class_methods": [ 22 | ], 23 | "instance_methods": [ 24 | ] 25 | }, 26 | { 27 | "name": "BBBB", 28 | "type": "Module", 29 | "singleton_class_included": [ 30 | ], 31 | "included": [ 32 | ], 33 | "class_methods": [ 34 | ], 35 | "instance_methods": [ 36 | ], 37 | "superclass": "Module" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /storage-server-api/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | 6 | dependencies { 7 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 8 | } 9 | } 10 | 11 | dependencies { 12 | compile project(':common') 13 | compile project(':ruby-call-signature') 14 | 15 | compile "org.jetbrains.exposed:exposed:$exposedVersion" 16 | compile 'com.h2database:h2:1.4.197' 17 | } 18 | 19 | sourceSets { 20 | main.java.srcDirs = ['src/main/java'] 21 | main.kotlin.srcDirs = ['src/main/java'] 22 | 23 | test.kotlin.srcDirs = ['src/test/java'] 24 | } -------------------------------------------------------------------------------- /storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/serialization/BlobSerialization.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.signature.serialization 2 | 3 | import org.jetbrains.exposed.dao.EntityHook 4 | import org.jetbrains.exposed.sql.transactions.TransactionManager 5 | import org.jetbrains.ruby.codeInsight.types.signature.SignatureContract 6 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.SignatureContractRow 7 | import java.io.DataInputStream 8 | import java.io.DataOutputStream 9 | import java.sql.Blob 10 | import java.util.concurrent.CopyOnWriteArrayList 11 | import kotlin.reflect.KProperty 12 | 13 | class BlobDeserializer { 14 | companion object { 15 | private val openBlobs: MutableCollection = CopyOnWriteArrayList() 16 | 17 | init { 18 | EntityHook.subscribe { 19 | openBlobs.forEach { it.free() } 20 | openBlobs.clear() 21 | } 22 | } 23 | } 24 | 25 | @Volatile 26 | private var cachedContract: SignatureContract? = null 27 | 28 | operator fun getValue(signatureContractRow: SignatureContractRow, property: KProperty<*>): SignatureContract { 29 | cachedContract?.let { return it } 30 | 31 | val blob = signatureContractRow.contractRaw 32 | try { 33 | val result = SignatureContract(DataInputStream(blob.binaryStream)) 34 | cachedContract = result 35 | return result 36 | } finally { 37 | blob.free() 38 | } 39 | } 40 | 41 | operator fun setValue(signatureContractRow: SignatureContractRow, property: KProperty<*>, signatureContract: SignatureContract) { 42 | val blob = TransactionManager.current().connection.createBlob() 43 | openBlobs.add(blob) 44 | 45 | BlobSerializer.writeToBlob(signatureContract, blob) 46 | signatureContractRow.contractRaw = blob 47 | cachedContract = signatureContract 48 | } 49 | } 50 | 51 | object BlobSerializer { 52 | fun writeToBlob(signatureContract: SignatureContract, blob: Blob): Blob { 53 | val binaryStream = blob.setBinaryStream(1) 54 | signatureContract.serialize(DataOutputStream(binaryStream)) 55 | binaryStream.close() 56 | return blob 57 | } 58 | } -------------------------------------------------------------------------------- /storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/DatabaseProvider.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.storage.server 2 | 3 | import org.jetbrains.exposed.sql.Database 4 | import org.jetbrains.exposed.sql.SchemaUtils 5 | import org.jetbrains.exposed.sql.Transaction 6 | import org.jetbrains.exposed.sql.selectAll 7 | import org.jetbrains.exposed.sql.transactions.transaction 8 | import org.jetbrains.ruby.codeInsight.Logger 9 | import org.jetbrains.ruby.codeInsight.injector 10 | import org.jetbrains.ruby.codeInsight.types.storage.server.impl.* 11 | 12 | object DatabaseProvider { 13 | var defaultDatabase: Database? = null 14 | private set 15 | /** 16 | * Default database file path with .mv.db suffix included 17 | */ 18 | var defaultDatabaseFilePath: String? = null 19 | private set 20 | private const val IN_MEMORY_URL = "jdbc:h2:mem:test" 21 | private const val H2_DRIVER = "org.h2.Driver" 22 | const val H2_DB_FILE_EXTENSION = ".mv.db" 23 | private val logger: Logger = injector.getLogger(DatabaseProvider::class.java) 24 | 25 | @JvmStatic 26 | fun connectToInMemoryDB(isDefaultDatabase: Boolean = false): Database { 27 | val database = Database.connect(IN_MEMORY_URL, driver = H2_DRIVER) 28 | if (isDefaultDatabase) { 29 | defaultDatabase = database 30 | } 31 | logger.info("Connected to in memory DB") 32 | logDatabaseSize(database) 33 | return database 34 | } 35 | 36 | @JvmStatic 37 | fun connectToDB(filePath: String, isDefaultDatabase: Boolean = false): Database { 38 | check(filePath.endsWith(H2_DB_FILE_EXTENSION)) { 39 | "File path must end with $H2_DB_FILE_EXTENSION suffix" 40 | } 41 | val filePathForUrl = filePath.substring(0, filePath.lastIndexOf(H2_DB_FILE_EXTENSION)) 42 | val database = Database.connect("jdbc:h2:$filePathForUrl", driver = H2_DRIVER) 43 | if (isDefaultDatabase) { 44 | defaultDatabase = database 45 | defaultDatabaseFilePath = filePath 46 | } 47 | logger.info("Connected to DB: $filePath") 48 | createAllDatabases(database) 49 | logDatabaseSize(database) 50 | return database 51 | } 52 | 53 | @JvmStatic 54 | fun defaultDatabaseTransaction(statement: Transaction.() -> T): T { 55 | val defaultDatabaseLocal = defaultDatabase ?: throw IllegalStateException("Assign defaultDatabase firstly") 56 | return transaction(defaultDatabaseLocal, statement) 57 | } 58 | 59 | @JvmOverloads 60 | fun createAllDatabases(db: Database? = null) { 61 | transaction(db ?: defaultDatabase) { 62 | SchemaUtils.create(GemInfoTable, ClassInfoTable, MethodInfoTable, SignatureTable, CallInfoTable) 63 | } 64 | } 65 | 66 | @JvmOverloads 67 | fun dropAllDatabases(db: Database? = null) { 68 | transaction(db ?: defaultDatabase) { 69 | SchemaUtils.drop(GemInfoTable, ClassInfoTable, MethodInfoTable, SignatureTable, CallInfoTable) 70 | } 71 | } 72 | 73 | private fun logDatabaseSize(db: Database) { 74 | transaction(db) { 75 | for (table in listOf(CallInfoTable, MethodInfoTable, ClassInfoTable, SignatureTable, GemInfoTable)) { 76 | logger.info("${table.tableName} table's number of rows ${table.selectAll().count()}") 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/RSignatureProvider.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.storage.server; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.jetbrains.ruby.codeInsight.types.signature.*; 6 | 7 | import java.util.Collection; 8 | 9 | /** 10 | *

An interface that allows for transparent working with the signatures storage.

11 | *

12 | *

The general workflow is the following:

13 | *
    14 | *
  • 1. Determine which gem statistics are to be used. If one wants to receive code insight for some project, 15 | * they must know which gems are available at runtime. 16 | *
  • 2. Since a precalculated information for the particular gem may not be available, one searches for the 17 | * closest gem version with calculated stats via {@link #getClosestRegisteredGem(GemInfo)} 18 | *
  • 3. In order to get the registered classes available upon requiring the given gem one may use 19 | * {@link #getRegisteredClasses(GemInfo)} 20 | *
  • 4. Given a class of a receiver object one may get the registered methods available for sending 21 | * via {@link #getRegisteredMethods(ClassInfo)} 22 | *
  • 5. Given a call, which is represented as a method of a particular class in a particular gem one may 23 | * get Signature contract via {@link #getSignature(MethodInfo)}. It allows for getting params 24 | * information, deducing return type from given input types, etc. 25 | *
26 | */ 27 | public interface RSignatureProvider { 28 | @NotNull 29 | Collection getRegisteredGems() throws StorageException; 30 | 31 | @Nullable 32 | GemInfo getClosestRegisteredGem(@NotNull GemInfo usedGem) throws StorageException; 33 | 34 | @NotNull 35 | Collection getRegisteredClasses(@NotNull GemInfo gem) throws StorageException; 36 | 37 | @NotNull 38 | Collection getAllClassesWithFQN(@NotNull String fqn) throws StorageException; 39 | 40 | @NotNull 41 | Collection getRegisteredMethods(@NotNull ClassInfo containerClass) 42 | throws StorageException; 43 | 44 | /** 45 | * Get registered {@link CallInfo}s by given {@code methodInfo} 46 | */ 47 | @NotNull 48 | Collection getRegisteredCallInfos(@NotNull MethodInfo methodInfo) throws StorageException; 49 | 50 | @Nullable 51 | SignatureInfo getSignature(@NotNull MethodInfo method) throws StorageException; 52 | 53 | void deleteSignature(@NotNull MethodInfo method) throws StorageException; 54 | 55 | void putSignature(@NotNull SignatureInfo signatureInfo) throws StorageException; 56 | 57 | } 58 | -------------------------------------------------------------------------------- /storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/RSignatureStorage.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.storage.server; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.jetbrains.ruby.codeInsight.types.signature.*; 6 | 7 | import java.util.Collection; 8 | 9 | public interface RSignatureStorage extends RSignatureProvider { 10 | 11 | default void readPacket(@NotNull T packet) throws StorageException { 12 | for (final SignatureInfo signatureInfo : packet.getSignatures()) { 13 | final MethodInfo methodInfo = signatureInfo.getMethodInfo(); 14 | final SignatureInfo oldSignature = getSignature(methodInfo); 15 | 16 | RSignatureContract contract; 17 | if (oldSignature != null && 18 | (contract = RSignatureContract.mergeMutably(oldSignature.getContract(), signatureInfo.getContract())) != null) { 19 | putSignature(SignatureInfoKt.SignatureInfo(methodInfo, contract)); 20 | } else { 21 | putSignature(signatureInfo); 22 | } 23 | } 24 | } 25 | 26 | @NotNull 27 | Collection formPackets(@Nullable ExportDescriptor descriptor) throws StorageException; 28 | 29 | class ExportDescriptor { 30 | private final boolean myInclude; 31 | 32 | @NotNull 33 | private final Collection myGemsToIncludeOrExclude; 34 | 35 | public ExportDescriptor(boolean include, @NotNull Collection gemsToIncludeOrExclude) { 36 | myInclude = include; 37 | myGemsToIncludeOrExclude = gemsToIncludeOrExclude; 38 | } 39 | 40 | public boolean isInclude() { 41 | return myInclude; 42 | } 43 | 44 | @NotNull 45 | public Collection getGemsToIncludeOrExclude() { 46 | return myGemsToIncludeOrExclude; 47 | } 48 | } 49 | 50 | interface Packet { 51 | Collection getSignatures(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/StorageException.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.storage.server; 2 | 3 | @SuppressWarnings("unused") 4 | public class StorageException extends Exception { 5 | public StorageException() { 6 | } 7 | 8 | public StorageException(String message) { 9 | super(message); 10 | } 11 | 12 | public StorageException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public StorageException(Throwable cause) { 17 | super(cause); 18 | } 19 | 20 | public StorageException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 21 | super(message, cause, enableSuppression, writableStackTrace); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/impl/RSignatureProviderImpl.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.storage.server.impl 2 | 3 | import org.jetbrains.exposed.sql.and 4 | import org.jetbrains.exposed.sql.deleteWhere 5 | import org.jetbrains.exposed.sql.select 6 | import org.jetbrains.ruby.codeInsight.types.signature.* 7 | import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider 8 | import org.jetbrains.ruby.codeInsight.types.storage.server.RSignatureProvider 9 | 10 | object RSignatureProviderImpl : RSignatureProvider { 11 | override fun getRegisteredGems(): Collection { 12 | return DatabaseProvider.defaultDatabaseTransaction { GemInfoRow.all() }.map { it.copy() } 13 | } 14 | 15 | override fun getClosestRegisteredGem(usedGem: GemInfo): GemInfo? { 16 | val (upperBound, lowerBound) = DatabaseProvider.defaultDatabaseTransaction { 17 | val upperBound = GemInfoTable.select { 18 | GemInfoTable.name.eq(usedGem.name) and GemInfoTable.version.greaterEq(usedGem.version) 19 | } 20 | .orderBy(GemInfoTable.version) 21 | .limit(1) 22 | .firstOrNull() 23 | ?.let { GemInfoRow.wrapRow(it) } 24 | 25 | val lowerBound = GemInfoTable.select { 26 | GemInfoTable.name.eq(usedGem.name) and GemInfoTable.version.lessEq(usedGem.version) 27 | } 28 | .orderBy(GemInfoTable.version, isAsc = false) 29 | .limit(1) 30 | .firstOrNull() 31 | ?.let { GemInfoRow.wrapRow(it) } 32 | return@defaultDatabaseTransaction Pair(upperBound?.copy(), lowerBound?.copy()) 33 | } 34 | 35 | if (lowerBound == null || upperBound == null) { 36 | return lowerBound ?: upperBound 37 | } else { 38 | return if (firstStringCloser(usedGem.version, lowerBound.version, upperBound.version)) lowerBound else upperBound 39 | } 40 | } 41 | 42 | override fun getRegisteredClasses(gem: GemInfo): Collection { 43 | return DatabaseProvider.defaultDatabaseTransaction { 44 | val gemId = GemInfoTable.findRowId(gem) ?: return@defaultDatabaseTransaction emptyList() 45 | 46 | return@defaultDatabaseTransaction ClassInfoRow.find { ClassInfoTable.gemInfo eq gemId }.map { it.copy() } 47 | } 48 | } 49 | 50 | override fun getAllClassesWithFQN(fqn: String): Collection { 51 | return DatabaseProvider.defaultDatabaseTransaction { 52 | ClassInfoRow.find { ClassInfoTable.fqn eq fqn }.map { it.copy() } 53 | } 54 | } 55 | 56 | override fun getRegisteredMethods(containerClass: ClassInfo): Collection { 57 | return DatabaseProvider.defaultDatabaseTransaction { 58 | val classId = ClassInfoTable.findRowId(containerClass) ?: return@defaultDatabaseTransaction emptyList() 59 | 60 | return@defaultDatabaseTransaction MethodInfoRow.find { MethodInfoTable.classInfo eq classId }.map { it.copy() } 61 | } 62 | } 63 | 64 | override fun getSignature(method: MethodInfo): SignatureInfo? { 65 | return DatabaseProvider.defaultDatabaseTransaction { 66 | val methodId = MethodInfoTable.findRowId(method) ?: return@defaultDatabaseTransaction null 67 | 68 | return@defaultDatabaseTransaction SignatureContractRow.find { SignatureTable.methodInfo eq methodId }.firstOrNull()?.copy() 69 | } 70 | } 71 | 72 | override fun deleteSignature(method: MethodInfo) { 73 | return DatabaseProvider.defaultDatabaseTransaction { 74 | val methodId = MethodInfoTable.findRowId(method) ?: return@defaultDatabaseTransaction 75 | 76 | SignatureTable.deleteWhere { SignatureTable.methodInfo eq methodId } 77 | } 78 | } 79 | 80 | override fun putSignature(signatureInfo: SignatureInfo) { 81 | SignatureTable.insertInfoIfNotContains(signatureInfo) 82 | } 83 | 84 | override fun getRegisteredCallInfos(methodInfo: MethodInfo): List { 85 | return DatabaseProvider.defaultDatabaseTransaction { 86 | val methodId = MethodInfoTable.findRowId(methodInfo) ?: return@defaultDatabaseTransaction emptyList() 87 | 88 | return@defaultDatabaseTransaction CallInfoRow.find { CallInfoTable.methodInfoId eq methodId }.map { it.copy() } 89 | } 90 | } 91 | } 92 | 93 | fun firstStringCloser(gemVersion: String, 94 | firstVersion: String, secondVersion: String): Boolean { 95 | val lcpLengthFirst = longestCommonPrefixLength(gemVersion, firstVersion) 96 | val lcpLengthSecond = longestCommonPrefixLength(gemVersion, secondVersion) 97 | return lcpLengthFirst > lcpLengthSecond || lcpLengthFirst > 0 && lcpLengthFirst == lcpLengthSecond && 98 | Math.abs(gemVersion.rawChar(lcpLengthFirst) - firstVersion.rawChar(lcpLengthFirst)) < 99 | Math.abs(gemVersion.rawChar(lcpLengthFirst) - secondVersion.rawChar(lcpLengthSecond)) 100 | } 101 | 102 | private fun String.rawChar(index: Int): Int = if (index < length) this[index].toInt() else 0 103 | 104 | private fun longestCommonPrefixLength(str1: String, str2: String): Int { 105 | val minLength = Math.min(str1.length, str2.length) 106 | return (0 until minLength).firstOrNull { str1[it] != str2[it] } ?: minLength 107 | } 108 | -------------------------------------------------------------------------------- /storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/impl/RowConversions.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.storage.server.impl 2 | 3 | import org.jetbrains.exposed.sql.ResultRow 4 | import org.jetbrains.ruby.codeInsight.types.signature.* 5 | import org.jetbrains.ruby.codeInsight.types.signature.serialization.SignatureContract 6 | import java.io.DataInputStream 7 | 8 | fun GemInfo(row: ResultRow): GemInfo = GemInfo(row[GemInfoTable.name], row[GemInfoTable.version]) 9 | 10 | fun ClassInfo(row: ResultRow): ClassInfo = ClassInfo(GemInfo(row), row[ClassInfoTable.fqn]) 11 | 12 | fun Location(row: ResultRow): Location? { 13 | val locationFile = row[MethodInfoTable.locationFile] 14 | ?: return null 15 | 16 | return Location(locationFile, row[MethodInfoTable.locationLineno]) 17 | } 18 | 19 | fun MethodInfo(row: ResultRow): MethodInfo = MethodInfo.Impl( 20 | ClassInfo(row), 21 | row[MethodInfoTable.name], 22 | row[MethodInfoTable.visibility], 23 | Location(row)) 24 | 25 | fun SignatureInfo(row: ResultRow): SignatureInfo { 26 | val blob = row[SignatureTable.contract] 27 | try { 28 | return SignatureInfo(MethodInfo(row), SignatureContract(DataInputStream(blob.binaryStream))) 29 | } finally { 30 | blob.free() 31 | } 32 | } -------------------------------------------------------------------------------- /storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/testutil/DatabaseTestUtils.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.ruby.codeInsight.types.storage.server.testutil 2 | 3 | import org.jetbrains.exposed.sql.transactions.transaction 4 | import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider 5 | 6 | /** 7 | * This function is used to test database related things. Just creates new clean databases, 8 | * executes [block] and remove created databases. 9 | * 10 | * [TestCase.setUp] and [TestCase.tearDown] functions won't help because [DatabaseProvider.createAllDatabases] 11 | * must be called in the same [transaction] block for in memory database 12 | */ 13 | fun doDBTest(block: () -> Unit) { 14 | transaction { 15 | DatabaseProvider.createAllDatabases() 16 | block() 17 | DatabaseProvider.dropAllDatabases() 18 | } 19 | } 20 | --------------------------------------------------------------------------------