├── .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 | 
7 |
8 | ## Type providing for method parameters
9 |
10 | 
11 |
12 |
13 | ## Type providing for return value
14 |
15 | 
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 [](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [](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 | 
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 [](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 |
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 |
--------------------------------------------------------------------------------