Repository: JetBrains/ruby-type-inference Branch: master Commit: df63525a226c Files: 160 Total size: 406.4 KB Directory structure: gitextract_m6imoy85/ ├── .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/ │ │ │ ├── options.rb │ │ │ ├── require_all.rb │ │ │ ├── starter.rb │ │ │ ├── state_tracker.rb │ │ │ ├── type_tracker.rb │ │ │ ├── version.rb │ │ │ └── workspace.rb │ │ └── arg_scanner.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/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── ide-plugin/ │ ├── CHANGELOG.md │ ├── build.gradle │ ├── resources/ │ │ └── META-INF/ │ │ └── plugin.xml │ └── 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 ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /build/ out/ */build/ .gradle .idea/ **/*.iml **/.rakeTasks arg_scanner/arg_scanner.iml ================================================ FILE: .travis.yml ================================================ language: ruby dist: trusty os: - linux # - osx rvm: - 2.3.3 - 2.4.2 - ruby-head matrix: fast_finish: true allow_failures: - rvm: ruby-head services: - mysql cache: directories: - $HOME/.gradle/caches/ - $HOME/.gradle/wrapper/ before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update ; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install mysql; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then mysql.server start; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then mysql -u root -e "CREATE USER 'travis'@'127.0.0.1' IDENTIFIED BY '';"; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then mysql -u root -e "FLUSH PRIVILEGES;"; fi - mysql -u root -e 'CREATE DATABASE ruby_type_contracts;' - mysql -u root -e 'GRANT ALL ON ruby_type_contracts.* TO 'travis'@'127.0.0.1';' - cd arg_scanner script: - gem install rake - rake test - rake install - cd .. - travis_wait 40 ./gradlew tasks - ./gradlew -Dmysql.user.name=travis -Dmysql.user.password="" test ================================================ FILE: FEATURES.md ================================================ # ruby-type-inference features This doc contains `ruby-type-inference` features which can be useful for you after running your ruby program under type tracker: ![Run with type tracker](screenshots/run_with_type_tracker.png) ## Type providing for method parameters ![Parameter type providing](screenshots/parameter_type_providing.png) ## Type providing for return value ![Return type providing](screenshots/return_type_providing.png) ## Side notes As now RubyMine has more information about types it can provide more reliable code completion, code analysis and other code insight features ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2016-2017 JetBrains s.r.o. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ Automated Type Contracts Generation [![JetBrains incubator project](http://jb.gg/badges/incubator.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![Build Status](https://travis-ci.org/JetBrains/ruby-type-inference.svg?branch=master)](https://travis-ci.org/JetBrains/ruby-type-inference) =================================== `ruby-type-inference` project is a completely new approach to tackle the problems of Ruby dynamic nature and provide more reliable symbol resolution and type inference. It collects some run time data to build type contracts for the methods. Every time a method is being called, some arguments of particular types are being passed to it. Type Tracker collects all such argument combinations and then builds a special contract which satisfies all encountered argument type tuples. The approach has its own pros and cons: * The obtained contracts utilize real-world usages of code of any complexity so it provides true results even if a method utilizes dynamic Ruby features heavily. * The completeness of the contracts obtained for a method highly depends on the coverage of that method, including its callees. That implies the need to merge the data obtained from the different sources (e.g. different projects using the same gem). This implementation addresses the stated coverage problem by providing the possibility to merge any type contracts at any time. ## Usage For simple usage you need to install the [Ruby Dynamic Code Insight](https://plugins.jetbrains.com/plugin/10227-ruby-dynamic-code-insight) plugin for RubyMine. Then this plugin will require the [arg_scanner](https://rubygems.org/gems/arg_scanner) gem to be installed. See [arg_scanner installation instruction](arg_scanner/README.md#installation) if you have problems while installation. After that, you will have the possibility to run your programs under type tracker: ![Run with type tracker](screenshots/run_with_type_tracker.png) Or you can run your programs in terminal via the `rubymine-type-tracker` binary (But you have to keep your project opened in RubyMine). E.g.: ``` rubymine-type-tracker bin/rails server ``` The `rubymine-type-tracker` binary is included into the [arg_scanner](https://rubygems.org/gems/arg_scanner) gem. See [FEATURES.md](FEATURES.md) for understanding what benefits you will have after running your program under type tracker. ## Architecture * **arg_scanner** is a gem with a native extension to attach to ruby processes and trace and intercept all method calls to log type-wise data flow in runtime. See [`arg_scanner`] documentation for details on usage. * The [**type contract processor**](contract-creator) server listens for incoming type data (from `arg_scanner`) and processes it to a compact format. The data stored may be used later for better code analysis and also can be shared with other users. * Code analysis clients (a RubyMine/IJ+Ruby [plugin](ide-plugin)) use the contract data to provide features for the users such as code completion, better resolution, etc. * (_todo_) Signature server receives contracts anonymously from the users and provides a compiled contract collections for popular gems. ## Running project from sources #### Prerequisites The [`arg_scanner`] gem is used for collecting type information. It can be installed manually to the target SDK and requires MRI Ruby at least 2.3. #### Running type tracker There are two possibilities to use the type tracker: _(I)_ using IJ/RubyMine plugin or _(II)_ requiring it from Ruby code. ##### Using RubyMine plugin The easiest way to run the plugin (and the most convenient for its development) is running it with special gradle task against IJ Ultimate snapshot: ``` ./gradlew ide-plugin:runIde ``` The task will compile the plugin, run IJ Ultimate with plugin "installed" in it. There is no need in running anything manually in that case. If you want to try it with existing RubyMine instance, you should: 1. Build it via `./gradlew ide-plugin:buildPlugin` 2. Install plugin in the IDE * Navigate to `File | Settings | Plugins | Install plugin from disk...` * Locate plugin in `ide-plugin/build/distributions` and select. * Restart IDE. Note that due to API changes the plugin may be incompatible with older RM instances. ##### Using command line 1. In order to collect the data for the script needs a contract server to be up and running; it could be run by running ```sh ./gradlew contract-creator:runServer --args path-to-db.mv.db ``` where `path-to-db.mv.db` is path where type contracts will be stored (H2 database file). 1. Run the ruby script to be processed via [`arg-scanner`](arg_scanner/bin/arg-scanner) binary. 1. Use the data collected by the contract server. ## Contributions Any kind of ideas, use cases, contributions and questions are very welcome as the project is just incubating. Please feel free to create issues for any sensible request. [`arg_scanner`]: arg_scanner/README.md ================================================ FILE: arg_scanner/.gitignore ================================================ *.iml .bundle/ .yardoc Gemfile.lock _yardoc/ coverage/ doc/ pkg/ spec/reports/ tmp/ *.bundle *.so *.o *.a mkmf.log ================================================ FILE: arg_scanner/Gemfile ================================================ source 'https://rubygems.org' # Specify your gem's dependencies in arg_scanner.gemspec gemspec group :test do gem 'test-unit' end ================================================ FILE: arg_scanner/LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) 2017 JetBrains Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: arg_scanner/README.md ================================================ # ArgScanner [![Gem Version](https://badge.fury.io/rb/arg_scanner.svg)](https://badge.fury.io/rb/arg_scanner) `arg_scanner` is a gem with the purpose to track all method calls and deliver the following information: * Method signature (arguments, their names and kinds) and declaration place * The types of argument variables given to each method call done This information can be used then to calculate and use type contracts for the analysed methods. `arg_scanner` is meant to be used as a binary to run any other ruby executable manually so including it in the `Gemfile` is not necessary. ## Installation The recommended way to install it is to execute command: ``` gem install arg_scanner ``` **You will possibly need to install [native dependencies](#dependencies)** ## Building from sources If you want to compile the gem from sources, just run the following commands: ``` bundle install bundle exec rake install ``` If you have problems with native extension compilation, make sure you have actual version of [ruby-core-source gem](https://github.com/os97673/debase-ruby_core_source) and have [native dependencies](#dependencies) installed. ## Dependencies ##### [Glib](https://developer.gnome.org/glib/) macOS: `brew install glib` Debian/Ubuntu: `sudo apt install libglib2.0-dev` Arch Linux: `sudo pacman -S glib2` ## Usage `arg_scanner` provides the `arg-scanner` binary which receives any number of arguments and executes the given command in type tracking mode, for example: ``` arg-scanner --type-tracker --pipe-file-path=[pipe_file_path] bundle exec rake spec ``` `pipe_file_path` here is path to pipe file which is printed by server's stdout ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/JetBrains/ruby-type-inference ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). ================================================ FILE: arg_scanner/Rakefile ================================================ require "bundler/gem_tasks" require "rake/extensiontask" require 'rake/testtask' BASE_TEST_FILE_LIST = Dir['test/**/test_*.rb'] task :build => :compile Rake::ExtensionTask.new("arg_scanner") do |ext| ext.lib_dir = "lib/arg_scanner" end desc "Test arg_scanner." Rake::TestTask.new(:test => [:clean, :compile]) do |t| t.libs += %w(./ext ./lib) t.test_files = FileList[BASE_TEST_FILE_LIST] t.verbose = true end task :test => :lib task :default => [:clobber, :compile, :test] ================================================ FILE: arg_scanner/arg_scanner.gemspec ================================================ # coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'arg_scanner/version' Gem::Specification.new do |spec| spec.name = "arg_scanner" spec.version = ArgScanner::VERSION spec.authors = ["Nickolay Viuginov", "Valentin Fondaratov", "Vladimir Koshelev"] spec.email = ["viuginov.nickolay@gmail.com", "fondarat@gmail.com", "vkkoshelev@gmail.com"] spec.summary = %q{Program execution tracker to retrieve data types information} spec.homepage = "https://github.com/jetbrains/ruby-type-inference" spec.license = "MIT" # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' # to allow pushing to a single host or delete this section to allow pushing to any host. # if spec.respond_to?(:metadata) # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'" # else # raise "RubyGems 2.0 or newer is required to protect against " \ # "public gem pushes." # end spec.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features)/}) end spec.bindir = "bin" spec.executables = spec.files.grep(%r{^bin/}) {|f| File.basename(f)} spec.require_paths = ["lib"] spec.extensions = ["ext/arg_scanner/extconf.rb"] spec.add_development_dependency "bundler", ">= 1.13" spec.add_development_dependency "rake", ">= 12.0" spec.add_development_dependency "rake-compiler" spec.add_dependency "debase-ruby_core_source", ">= 0.10.4" spec.add_dependency "native-package-installer", ">= 1.0.0" end ================================================ FILE: arg_scanner/bin/arg-scanner ================================================ #!/usr/bin/env ruby require 'optparse' require 'arg_scanner/options' require 'arg_scanner/version' options = ArgScanner::OPTIONS option_parser = OptionParser.new do |opts| opts.banner = "arg-scanner #{ArgScanner::VERSION}" + <<~EOB Usage: arg-scanner [OPTIONS] arg-scanner is a ruby script mediator supposed to be run from the command line or IDE. The data will be sent to a signature server so it must be running during arg-scanner execution. EOB opts.separator "Options:" opts.on("--type-tracker", "enable type tracker") do options.enable_type_tracker = true end opts.on("--state-tracker", "enable state tracker") do options.enable_state_tracker = true end opts.on("--no-type-tracker", "disable type tracker") do options.enable_type_tracker = false end opts.on("--no-state-tracker", "disable state tracker") do options.enable_state_tracker = false end opts.on("--output-dir=[Dir]", String, "specify output directory (ignored by type tracker)") do |dir| options.output_dir = dir end 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| options.catch_only_every_n_call = n end opts.on("--project-root=[PATH]", String, "Specify project's root directory to catch every call from this directory. "\ "Calls from other directories aren't guaranteed to be caught") do |path| options.project_root = path end opts.on("--pipe-file-path=[PATH]", String, "Specify pipe file path to connect to server") do |path| options.pipe_file_path = path end opts.on("--buffering", "enable buffering between arg-scanner and server. It speeds up arg-scanner but doesn't allow "\ "to use arg-scanner \"interactively\". Disabled by default") do |buffering| options.buffering = buffering end end begin option_parser.parse! ARGV rescue StandardError => e puts option_parser puts puts e.message exit 1 end if ARGV.size < 1 puts option_parser puts puts "Ruby program to trace must be specified." exit 1 end options.set_env old_opts = ENV['RUBYOPT'] || '' starter = "-r #{File.expand_path(File.dirname(__FILE__))}/../lib/arg_scanner/starter" unless old_opts.include? starter ENV['RUBYOPT'] = starter ENV['RUBYOPT'] += " #{old_opts}" if old_opts != '' end $0 = ARGV[0] Kernel.exec *ARGV ================================================ FILE: arg_scanner/bin/console ================================================ #!/usr/bin/env ruby require "bundler/setup" require "arg_scanner" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require "irb" IRB.start ================================================ FILE: arg_scanner/bin/rubymine-type-tracker ================================================ #!/usr/bin/env ruby # This is small script for launching type tracker under RubyMine's provided server. Acts like arg-scanner wrapper require 'optparse' require 'arg_scanner/version' require 'tmpdir' require 'json' option_parser = OptionParser.new do |opts| opts.banner = <<~EOB rubymine-type-tracker #{ArgScanner::VERSION} Usage: rubymine-type-tracker rubymine-type-tracker is a ruby script for easy launching some command under RubyMine's type tracker. The data will be sent to a server run by RubyMine. So before launching this script be sure project is opened in RubyMine with "Ruby Dynamic Code Insight" plugin installed. EOB end begin option_parser.parse! ARGV if ARGV.size == 0 raise StandardError.new("") end rescue StandardError => e puts option_parser exit 1 end dot_ruby_type_inference_dir = File.join(Dir.tmpdir, ".ruby-type-inference") if File.directory?(dot_ruby_type_inference_dir) match_jsons = Dir.foreach(dot_ruby_type_inference_dir).map do |file_name| if file_name == '.' || file_name == '..' next nil end json = JSON.parse(IO.read(File.join(dot_ruby_type_inference_dir, file_name))) if json["projectPath"] != Dir.pwd next nil end next json end.select { |x| x != nil } else match_jsons = [] end if match_jsons.count == 1 json = match_jsons[0] elsif match_jsons.count > 1 STDERR.puts <<~EOB Critical error! You may try to:\n 1. Close RubyMine 2. Clean #{dot_ruby_type_inference_dir} 3. Open RubyMine EOB exit 1 elsif match_jsons.count == 0 STDERR.puts <<~EOB Error! You are possibly... * launching this script under directory different from project opened in RubyMine (please `cd` to dir firstly) * haven't opened project in RubyMine * haven't installed "Ruby Dynamic Code Insight" plugin in RubyMine EOB exit 1 end to_exec = ["arg-scanner", "--type-tracker", "--project-root=#{json["projectPath"]}", "--pipe-file-path=#{json["pipeFilePath"]}", *ARGV] Kernel.exec(*to_exec) ================================================ FILE: arg_scanner/bin/setup ================================================ #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here ================================================ FILE: arg_scanner/ext/arg_scanner/arg_scanner.c ================================================ #include "arg_scanner.h" #include #include #include #include #include #include #include #include //#define DEBUG_ARG_SCANNER 1 #if RUBY_API_VERSION_CODE >= 20500 #if (RUBY_RELEASE_YEAR == 2017 && RUBY_RELEASE_MONTH == 10 && RUBY_RELEASE_DAY == 10) //workaround for 2.5.0-preview1 #define TH_CFP(thread) ((rb_control_frame_t *)(thread)->ec.cfp) #else #define TH_CFP(thread) ((rb_control_frame_t *)(thread)->ec->cfp) #endif #else #define TH_CFP(thread) ((rb_control_frame_t *)(thread)->cfp) #endif #ifdef DEBUG_ARG_SCANNER #define LOG(f, args...) { fprintf(stderr, "DEBUG: '%s'=", #args); fprintf(stderr, f, ##args); fflush(stderr); } #else #define LOG(...) {} #endif #define ruby_current_thread ((rb_thread_t *)RTYPEDDATA_DATA(rb_thread_current())) typedef struct rb_trace_arg_struct rb_trace_arg_t; VALUE mArgScanner = Qnil; int types_ids[20]; static VALUE c_signature; /** * Contains info related to explicitly passed args * For example: * def foo(a, b = 1); end * * `b` passed here implicitly: * foo(1) * * But here explicitly: * foo(1, 10) */ typedef struct { ssize_t call_info_explicit_argc; // Number of arguments that was explicitly passed by user char **call_info_kw_explicit_args; // kw arguments names that was explicitly passed by user (null terminating array) } call_info_t; typedef struct { char *receiver_name; char *method_name; char *args_info; char *path; char *return_type_name; ssize_t explicit_argc; // Number of arguments that was explicitly passed by user int lineno; int is_in_project_root; // Can be 0, 1 or -1 when project_root is not specified } signature_t; void Init_arg_scanner(); static const char *ARG_SCANNER_EXIT_COMMAND = "EXIT"; static const char *EMPTY_VALUE = ""; static const int MAX_NUMBER_OF_MISSED_CALLS = 10; /** * There we keep information about signatures that have already been sent to server in order to not sent them again */ static GTree *sent_to_server_tree; /** * Here we store map with key: signature_t and value: int number (how many times method was called with the same args) * If we got that any method is called with the same args more than MAX_NUMBER_OF_MISSED_CALLS times in a row then * we will ignore it. */ static GTree *number_missed_calls_tree; static GSList *call_stack = NULL; static char *get_args_info(const char *const *explicit_kw_args); static VALUE handle_call(VALUE self, VALUE tp); static VALUE handle_return(VALUE self, VALUE tp); static VALUE destructor(VALUE self); static const char *calc_sane_class_name(VALUE ptr); // returns Qnil if ready; or string containing error message otherwise static VALUE check_if_arg_scanner_ready(VALUE self); // For testing static VALUE get_args_info_rb(VALUE self); static VALUE get_call_info_rb(VALUE self); static call_info_t get_call_info(); static bool is_call_info_needed(); static void call_info_t_free(call_info_t s) { free(s.call_info_kw_explicit_args); } static void signature_t_free(signature_t *s) { free(s->receiver_name); free(s->method_name); free(s->args_info); free(s->path); free(s->return_type_name); free(s); } // Free signature_t partially leaving parts that are used in sent_to_server_tree_comparator // @see_also sent_to_server_tree_comparator static void signature_t_free_partially(signature_t *s) { free(s->receiver_name); s->receiver_name = NULL; free(s->method_name); s->method_name = NULL; } // Comparator for number_missed_calls_tree. static gint number_missed_calls_tree_comparator(gconstpointer x, gconstpointer y, gpointer user_data_ignored) { const signature_t *a = x; const signature_t *b = y; int ret; // Comparison using lineno and path theoretically should guarantees us unique. // And compare lineno firstly because it's faster O(1) than comparing path which is O(path_len) ret = a->lineno - b->lineno; if (ret != 0) return ret; ret = strcmp(a->path, b->path); if (ret != 0) return ret; return 0; } // Comparator for sent_to_server_tree. // If you want to change the way it compare then don't forget to // change signature_t_free_partially accordingly // @see_also signature_t_free_partially static gint sent_to_server_tree_comparator(gconstpointer x, gconstpointer y, gpointer user_data_ignored) { const signature_t *a = x; const signature_t *b = y; int ret; ret = number_missed_calls_tree_comparator(x, y, user_data_ignored); if (ret != 0) return ret; if (a->args_info != NULL && b->args_info != NULL) { ret = strcmp(a->args_info, b->args_info); if (ret != 0) return ret; } ret = strcmp(a->return_type_name, b->return_type_name); if (ret != 0) return ret; return 0; } inline int start_with(const char *str, const char *prefix) { if (str == NULL || prefix == NULL) { return -1; } while (*str != '\0' && *prefix != '\0') { if (*str != *prefix) { return 0; } str++; prefix++; } return 1; } FILE *pipe_file = NULL; static char *project_root = NULL; static int catch_only_every_n_call = 1; static int file_exists(const char *file_path) { return access(file_path, F_OK) != -1; } static VALUE init(VALUE self, VALUE pipe_file_path, VALUE buffering, VALUE project_root_local, VALUE catch_only_every_n_call_local) { if (pipe_file_path != Qnil) { pipe_file_path = rb_file_s_expand_path(1, &pipe_file_path); // https://ruby-doc.org/core-2.2.0/File.html#method-c-expand_path const char *pipe_file_path_c = StringValueCStr(pipe_file_path); if (!file_exists(pipe_file_path_c)) { fprintf(stderr, "Specified pipe file: %s doesn't exists\n", pipe_file_path_c); exit(1); } pipe_file = fopen(pipe_file_path_c, "w"); if (pipe_file == NULL) { fprintf(stderr, "Cannot open pipe file \"%s\" with write access\n", pipe_file_path_c); exit(1); } int buffering_disabled = buffering == Qnil; if (buffering_disabled) { setbuf(pipe_file, NULL); } } if (project_root_local != Qnil) { project_root = strdup(StringValueCStr(project_root_local)); } if (catch_only_every_n_call_local != Qnil) { if (sscanf(StringValueCStr(catch_only_every_n_call_local), "%d", &catch_only_every_n_call) != 1) { fprintf(stderr, "Please specify number in --catch-only-every-N-call arg\n"); exit(1); } srand(time(0)); } return Qnil; } void Init_arg_scanner() { mArgScanner = rb_define_module("ArgScanner"); rb_define_module_function(mArgScanner, "handle_call", handle_call, 1); rb_define_module_function(mArgScanner, "handle_return", handle_return, 1); rb_define_module_function(mArgScanner, "get_args_info", get_args_info_rb, 0); rb_define_module_function(mArgScanner, "get_call_info", get_call_info_rb, 0); rb_define_module_function(mArgScanner, "destructor", destructor, 0); rb_define_module_function(mArgScanner, "check_if_arg_scanner_ready", check_if_arg_scanner_ready, 0); rb_define_module_function(mArgScanner, "init", init, 4); sent_to_server_tree = g_tree_new_full(/*key_compare_func =*/sent_to_server_tree_comparator, /*key_compare_data =*/NULL, /*key_destroy_func =*/(GDestroyNotify)signature_t_free, /*value_destroy_func =*/NULL); // key_destroy_func is NULL because we will use the same keys for number_missed_calls_tree // and sent_to_server_tree. And all memory management is done by sent_to_server_tree number_missed_calls_tree = g_tree_new_full(/*key_compare_func =*/number_missed_calls_tree_comparator, /*key_compare_data =*/NULL, /*key_destroy_func =*/NULL, /*value_destroy_func =*/NULL); } inline void push_to_call_stack(signature_t *signature) { call_stack = g_slist_prepend(call_stack, (gpointer) signature); } inline signature_t *pop_from_call_stack() { if (call_stack == NULL) { return NULL; } signature_t *ret = (signature_t *) call_stack->data; GSList *old_head = call_stack; call_stack = g_slist_remove_link(call_stack, old_head); g_slist_free_1(old_head); return ret; } inline int is_call_stack_empty() { return call_stack == NULL; } /** * Looks at the object at the top of this stack without removing it from the stack. */ inline signature_t *top_of_call_stack() { if (call_stack == NULL) { return NULL; } return (signature_t *) call_stack[0].data; } rb_control_frame_t * my_rb_vm_get_binding_creatable_next_cfp(const rb_thread_t *th, const rb_control_frame_t *cfp) { while (!RUBY_VM_CONTROL_FRAME_STACK_OVERFLOW_P(th, cfp)) { if (cfp->iseq) { return (rb_control_frame_t *)cfp; } cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp); } return 0; } static VALUE exit_from_handle_call_skipping_call() { push_to_call_stack(NULL); return Qnil; } static VALUE handle_call(VALUE self, VALUE tp) { signature_t sign_temp; memset(&sign_temp, 0, sizeof(sign_temp)); sign_temp.lineno = FIX2INT(rb_funcall(tp, rb_intern("lineno"), 0)); // Convert Ruby's Fixnum to C language int VALUE path = rb_funcall(tp, rb_intern("path"), 0); path = rb_file_s_expand_path(1, &path); // https://ruby-doc.org/core-2.2.0/File.html#method-c-expand_path sign_temp.path = StringValueCStr(path); int is_in_project_root = start_with(sign_temp.path, project_root); if (project_root != NULL && !is_in_project_root) { signature_t *peek = top_of_call_stack(); if (!is_call_stack_empty() && (peek == NULL || !(peek->is_in_project_root))) { return exit_from_handle_call_skipping_call(); } } if (project_root == NULL || !is_in_project_root) { int number_of_missed_calls = (int)g_tree_lookup(number_missed_calls_tree, &sign_temp); if (number_of_missed_calls > MAX_NUMBER_OF_MISSED_CALLS) { return exit_from_handle_call_skipping_call(); } } if (catch_only_every_n_call != 1 && rand() % catch_only_every_n_call != 0) { return exit_from_handle_call_skipping_call(); } signature_t *sign = (signature_t *) calloc(1, sizeof(*sign)); sign->is_in_project_root = is_in_project_root; sign->lineno = sign_temp.lineno; sign->path = strdup(sign_temp.path); sign->method_name = strdup(rb_id2name(SYM2ID(rb_funcall(tp, rb_intern("method_id"), 0)))); sign->explicit_argc = -1; #ifdef DEBUG_ARG_SCANNER LOG("Getting args info for %s %s %d \n", sign->method_name, sign->path, sign->lineno); #endif call_info_t info; info.call_info_kw_explicit_args = NULL; if (is_call_info_needed()) { info = get_call_info(); sign->explicit_argc = info.call_info_explicit_argc; } sign->args_info = get_args_info(info.call_info_kw_explicit_args); call_info_t_free(info); if (sign->args_info != NULL && strlen(sign->args_info) >= 1000) { signature_t_free(sign); return exit_from_handle_call_skipping_call(); } push_to_call_stack(sign); return Qnil; } static VALUE handle_return(VALUE self, VALUE tp) { signature_t *sign = pop_from_call_stack(); if (sign == NULL) { return Qnil; } VALUE defined_class = rb_funcall(tp, rb_intern("defined_class"), 0); VALUE receiver_name = rb_mod_name(defined_class); // if defined_class is nil then it means that method is invoked from anonymous module. // Then trying to extract name of it's anonymous module. For more details see // CallStatCompletionTest#testAnonymousModuleMethodCall if (receiver_name == Qnil) { VALUE this = rb_funcall(tp, rb_intern("self"), 0); receiver_name = rb_funcall(this, rb_intern("to_s"), 0); } VALUE return_type_name = rb_funcall(tp, rb_intern("return_value"), 0); sign->receiver_name = strdup(StringValueCStr(receiver_name)); sign->return_type_name = strdup(calc_sane_class_name(return_type_name)); signature_t *sign_in_sent_to_server_tree = g_tree_lookup(sent_to_server_tree, sign); if (sign_in_sent_to_server_tree == NULL) { // Resets number of missed calls to 0 g_tree_insert(number_missed_calls_tree, /*key = */sign, /*value = */0); // GTree will free memory allocated by sign by itself g_tree_insert(sent_to_server_tree, /*key = */sign, /*value = */sign); if (pipe_file != NULL) { fprintf(pipe_file, "{\"method_name\":\"%s\",\"call_info_argc\":\"%d\",\"args_info\":\"%s\",\"visibility\":\"%s\"," "\"path\":\"%s\",\"lineno\":\"%d\",\"receiver_name\":\"%s\",\"return_type_name\":\"%s\"}\n", sign->method_name, sign->explicit_argc, sign->args_info != NULL ? sign->args_info : "", "PUBLIC", sign->path, sign->lineno, sign->receiver_name, sign->return_type_name); } signature_t_free_partially(sign); } else if (project_root == NULL || !sign->is_in_project_root) { signature_t_free(sign); int found = (int) g_tree_lookup(number_missed_calls_tree, sign_in_sent_to_server_tree); g_tree_insert(number_missed_calls_tree, /*key = */sign_in_sent_to_server_tree, /*value = */found + 1); } return Qnil; } static call_info_t get_call_info() { rb_thread_t *thread = ruby_current_thread; rb_control_frame_t *cfp = TH_CFP(thread); call_info_t empty; empty.call_info_kw_explicit_args = NULL; empty.call_info_explicit_argc = -1; cfp += 3; cfp = my_rb_vm_get_binding_creatable_next_cfp(thread, cfp); if(cfp->iseq == NULL || cfp->pc == NULL || cfp->iseq->body == NULL) { return empty; } const rb_iseq_t *iseq = (const rb_iseq_t *) cfp->iseq; ptrdiff_t pc = cfp->pc - cfp->iseq->body->iseq_encoded; const VALUE *iseq_original = rb_iseq_original_iseq(iseq); int indent; for (indent = 1; indent < 6; indent++) { VALUE insn = iseq_original[pc - indent]; int tmp = (int)insn; if(0 < tmp && tmp < 256) { if(indent < 3) { return empty; } call_info_t info; struct rb_call_info *ci = (struct rb_call_info *)iseq_original[pc - indent + 1]; info.call_info_explicit_argc = ci->orig_argc; info.call_info_kw_explicit_args = NULL; if (ci->flag & VM_CALL_KWARG) { struct rb_call_info_kw_arg *kw_args = ((struct rb_call_info_with_kwarg *)ci)->kw_arg; size_t kwArgSize = kw_args->keyword_len; VALUE kw_ary = rb_ary_new_from_values(kw_args->keyword_len, kw_args->keywords); info.call_info_kw_explicit_args = (char **) malloc((kwArgSize + 1)*sizeof(*(info.call_info_kw_explicit_args))); int i; for (i = kwArgSize -1 ; i >= 0; --i) { VALUE kw = rb_ary_pop(kw_ary); const char *kw_name = rb_id2name(SYM2ID(kw)); info.call_info_kw_explicit_args[i] = kw_name; } info.call_info_kw_explicit_args[kwArgSize] = NULL; } else { info.call_info_kw_explicit_args = malloc(sizeof(*info.call_info_kw_explicit_args)); info.call_info_kw_explicit_args[0] = NULL; } return info; } } return empty; } static const char* calc_sane_class_name(VALUE ptr) { VALUE klass = rb_obj_class(ptr); const char* klass_name; // may be false, see `object.c#rb_class_get_superclass` if (klass == Qfalse) { klass_name = ""; } else { klass_name = rb_class2name(klass); } // returned value may be NULL, see `variable.c#rb_class2name` if (klass_name == NULL) { klass_name = ""; } return klass_name; } static char * fast_join_array(char sep, size_t count, const char **strings) { size_t lengths[count + 1]; size_t i; char *result; lengths[0] = 0; for (i = 0; i < count; i++) { const char *str = strings[i]; size_t length; if (!str) length = 0; else length = strlen(str) + (i > 0); // 1 for separator before lengths[i + 1] = lengths[i] + length; } result = (char *)malloc(sizeof(*result) * (1 + lengths[count])); for (i = 0; i < count; i++) { const char *str = strings[i]; if (str) { int start = lengths[i]; if (i > 0) result[start++] = sep; memcpy(result + start, str, sizeof(*result) * (lengths[i + 1] - start)); } } result[lengths[count]] = 0; return result; } static char * fast_join(char sep, size_t count, ...) { char *strings[count]; size_t i; va_list ap; va_start(ap, count); for (i = 0; i < count; i++) { strings[i] = va_arg(ap, char *); } va_end(ap); return fast_join_array(sep, count, strings); } /** * Checks that `container` contains `element` */ static int contains(const char *const *container, const char *element) { if (container == NULL || element == NULL) { return 0; } const char *const *iterator = container; while (*iterator != NULL) { if (strcmp(*iterator, element) == 0) { return 1; } ++iterator; } return 0; } #define JOIN_KW_NAMES_AND_TYPES_BUF_SIZE 2048 static char join_kw_names_and_types_buf[JOIN_KW_NAMES_AND_TYPES_BUF_SIZE]; /** * Null terminating array which contains strings of explicitly passed kw args. * It's used for join_kw_names_and_types */ static const char *const *join_kw_names_and_types_explicit_kw_args = NULL; /** * This function is used for concatenating hash keys and value's types. * Be sure that buf is at least JOIN_KW_NAMES_AND_TYPES_BUF_SIZE bytes. * If join_kw_names_and_types_buf size = JOIN_KW_NAMES_AND_TYPES_BUF_SIZE * isn't enough then this buf will contain invalid information */ static int join_kw_names_and_types(VALUE key, VALUE val, VALUE ignored) { const char *kw_name = rb_id2name(SYM2ID(key)); const char *kw_type = calc_sane_class_name(val); const char *const *explicit_kw_args_iterator = join_kw_names_and_types_explicit_kw_args; // Just such behaviour: when join_kw_names_and_types_explicit_kw_args is // not provided then consider every kw arg as explicitly passed by user int is_explicit = explicit_kw_args_iterator == NULL; if (explicit_kw_args_iterator != NULL) { while(*explicit_kw_args_iterator != NULL) { if (strcmp(*explicit_kw_args_iterator, kw_name) == 0) { is_explicit = 1; break; } ++explicit_kw_args_iterator; } } if (is_explicit) { // Check that buf is not empty if (join_kw_names_and_types_buf[0] != '\0') { strncat(join_kw_names_and_types_buf, ";", JOIN_KW_NAMES_AND_TYPES_BUF_SIZE - 1); } strncat(join_kw_names_and_types_buf, "KEYREST,", JOIN_KW_NAMES_AND_TYPES_BUF_SIZE - 1); strncat(join_kw_names_and_types_buf, kw_type, JOIN_KW_NAMES_AND_TYPES_BUF_SIZE - 1); strncat(join_kw_names_and_types_buf, ",", JOIN_KW_NAMES_AND_TYPES_BUF_SIZE - 1); strncat(join_kw_names_and_types_buf, kw_name, JOIN_KW_NAMES_AND_TYPES_BUF_SIZE - 1); } return ST_CONTINUE; } static char* get_args_info(const char *const *explicit_kw_args) { rb_thread_t *thread; rb_control_frame_t *cfp; thread = ruby_current_thread; cfp = TH_CFP(thread); cfp += 2; VALUE *ep = cfp->ep; ep -= cfp->iseq->body->local_table_size; size_t param_size = cfp->iseq->body->param.size; size_t lead_num = cfp->iseq->body->param.lead_num; size_t opt_num = cfp->iseq->body->param.opt_num; size_t post_num = cfp->iseq->body->param.post_num; unsigned int has_rest = cfp->iseq->body->param.flags.has_rest; unsigned int has_kw = cfp->iseq->body->param.flags.has_kw; unsigned int has_kwrest = cfp->iseq->body->param.flags.has_kwrest; unsigned int has_block = cfp->iseq->body->param.flags.has_block; LOG("%d\n", param_size); LOG("%d\n", lead_num); LOG("%d\n", opt_num); LOG("%d\n", post_num); LOG("%d\n", has_rest); LOG("%d\n", has_kw); LOG("%d\n", has_kwrest); LOG("%d\n", has_block); if (param_size == 0) { return 0; } const char **types = (const char **)malloc(param_size * sizeof(*types)); size_t i, ans_iterator; int types_iterator; ans_iterator = 0; int new_version_flag = strcmp(RUBY_VERSION, "2.4.0") >= 0 ? 1 : 0; LOG("%d\n", new_version_flag); for(i = param_size - 1 - new_version_flag, types_iterator = 0; (size_t)types_iterator < param_size; i--, types_iterator++) { types[types_iterator] = calc_sane_class_name(ep[i - 1]); types_ids[types_iterator] = i - 1; LOG("Type #%d=%s\n", types_iterator, types[types_iterator]) } types_iterator--; if(has_kw) { param_size--; } char **ans = (char **)malloc(param_size * sizeof(*ans)); for(i = 0; i < lead_num; i++, ans_iterator++, types_iterator--) { const char* name = rb_id2name(cfp->iseq->body->local_table[ans_iterator]); ans[ans_iterator] = fast_join(',', 3, "REQ", types[types_iterator], name); } for(i = 0; i < opt_num; i++, ans_iterator++, types_iterator--) { const char* name = rb_id2name(cfp->iseq->body->local_table[ans_iterator]); ans[ans_iterator] = fast_join(',', 3, "OPT", types[types_iterator], name); } for(i = 0; i < has_rest; i++, ans_iterator++, types_iterator--) { const char* name = rb_id2name(cfp->iseq->body->local_table[ans_iterator]); ans[ans_iterator] = fast_join(',', 3, "REST", types[types_iterator], name); } for(i = 0; i < post_num; i++, ans_iterator++, types_iterator--) { const char* name = rb_id2name(cfp->iseq->body->local_table[ans_iterator]); ans[ans_iterator] = fast_join(',', 3, "POST", types[types_iterator], name); } if(cfp->iseq->body->param.keyword != NULL) { const ID *keywords = cfp->iseq->body->param.keyword->table; size_t kw_num = cfp->iseq->body->param.keyword->num; size_t required_num = cfp->iseq->body->param.keyword->required_num; size_t rest_start = cfp->iseq->body->param.keyword->rest_start; LOG("%d %d\n", kw_num, required_num) for(i = 0; i < required_num; i++, ans_iterator++, types_iterator--) { ID key = keywords[i]; ans[ans_iterator] = fast_join(',', 3, "KEYREQ", types[types_iterator], rb_id2name(key)); } for(i = required_num; i < kw_num; i++, types_iterator--) { ID key = keywords[i]; const char *name = rb_id2name(key); if (explicit_kw_args == NULL || contains(explicit_kw_args, name)) { ans[ans_iterator++] = fast_join(',', 3, "KEY", types[types_iterator], name); } } if (param_size - has_block > 1 && has_kwrest && TYPE(ep[types_ids[types_iterator]]) == T_FIXNUM) { types_iterator--; } if (has_kwrest) { char *buf = malloc(JOIN_KW_NAMES_AND_TYPES_BUF_SIZE * sizeof(*buf)); buf[0] = '\0'; join_kw_names_and_types_buf[0] = '\0'; join_kw_names_and_types_explicit_kw_args = explicit_kw_args; // This function call will concatenate info into join_kw_names_and_types_buf rb_hash_foreach(ep[types_ids[types_iterator]], join_kw_names_and_types, Qnil); // Checking that join_kw_names_and_types_buf isn't possibly containing invalid info. // See join_kw_names_and_types documentation to understand why it can be invalid size_t len = strlen(join_kw_names_and_types_buf); if (len > 0 && len < JOIN_KW_NAMES_AND_TYPES_BUF_SIZE - 1) { strncpy(buf, join_kw_names_and_types_buf, JOIN_KW_NAMES_AND_TYPES_BUF_SIZE); ans[ans_iterator++] = buf; } join_kw_names_and_types_explicit_kw_args = NULL; types_iterator--; } } for(i = 0; i < has_block; i++, ans_iterator++, types_iterator--) { const char* name = rb_id2name(cfp->iseq->body->local_table[ans_iterator]); ans[ans_iterator] = fast_join(',', 3, "BLOCK", types[types_iterator], name); } LOG("%d\n", ans_iterator) char *answer = fast_join_array(';', ans_iterator, ans); for(i = 0; i < ans_iterator; i++) { LOG("free2 %d %d =%s= \n", ans[i], strlen(ans[i]), ans[i]); free(ans[i]); } LOG("%d %d %d", ans_iterator, param_size, types_iterator); assert(types_iterator <= 0); free(types); free(ans); return answer; } static VALUE get_args_info_rb(VALUE self) { call_info_t info; info.call_info_kw_explicit_args = NULL; if (is_call_info_needed()) { info = get_call_info(); } char *args_info = get_args_info(info.call_info_kw_explicit_args); call_info_t_free(info); VALUE ret = args_info ? rb_str_new_cstr(args_info) : Qnil; free(args_info); return ret; } static VALUE get_call_info_rb(VALUE self) { if (is_call_info_needed()) { call_info_t info = get_call_info(); VALUE ans; ans = rb_ary_new(); rb_ary_push(ans, LONG2FIX(info.call_info_explicit_argc)); if (info.call_info_kw_explicit_args != NULL) { const char *const *kwarg = info.call_info_kw_explicit_args; int explicit_kw_count = 0; while (*kwarg != NULL) { ++explicit_kw_count; ++kwarg; } char *answer = fast_join_array(',', explicit_kw_count, info.call_info_kw_explicit_args); rb_ary_push(ans, rb_str_new_cstr(answer)); free(answer); } call_info_t_free(info); return ans; } else { return Qnil; } } static bool is_call_info_needed() { rb_thread_t *thread; rb_control_frame_t *cfp; thread = ruby_current_thread; cfp = TH_CFP(thread); cfp += 2; return (cfp->iseq->body->param.flags.has_opt || cfp->iseq->body->param.flags.has_kwrest || cfp->iseq->body->param.flags.has_rest || (cfp->iseq->body->param.keyword != NULL && cfp->iseq->body->param.keyword->required_num == 0)); } static VALUE check_if_arg_scanner_ready(VALUE self) { char error_msg[1024]; if (pipe_file == NULL) { snprintf(error_msg, sizeof(error_msg)/sizeof(*error_msg), "Pipe file is not specified"); return rb_str_new_cstr(error_msg); } return Qnil; } static VALUE destructor(VALUE self) { g_tree_destroy(sent_to_server_tree); g_tree_destroy(number_missed_calls_tree); fprintf(pipe_file, "%s\n", ARG_SCANNER_EXIT_COMMAND); fclose(pipe_file); free(project_root); return Qnil; } ================================================ FILE: arg_scanner/ext/arg_scanner/arg_scanner.h ================================================ #ifndef ARG_SCANNER_H #define ARG_SCANNER_H 1 #include "ruby.h" #include "vm_core.h" #include "version.h" #include "iseq.h" #include "method.h" #endif /* ARG_SCANNER_H */ ================================================ FILE: arg_scanner/ext/arg_scanner/extconf.rb ================================================ require "mkmf" RbConfig::MAKEFILE_CONFIG['CC'] = ENV['CC'] if ENV['CC'] require "debase/ruby_core_source" require "native-package-installer" class NilClass def empty?; true; end end # Just a replacement of have_header because have_header searches not recursively :( def real_have_header(header_name) if (have_header(header_name)) return true end yes_msg = "checking for #{header_name}... yes" no_msg = "checking for #{header_name}... no" include_env = ENV["C_INCLUDE_PATH"] if !include_env.empty? && !Dir.glob("#{include_env}/**/#{header_name}").empty? puts yes_msg return true end if !Dir.glob("/usr/include/**/#{header_name}").empty? puts yes_msg return true end puts no_msg return false end if !real_have_header('glib.h') && !NativePackageInstaller.install(:alt_linux => "glib2-devel", :debian => "libglib2.0-dev", :redhat => "glib2-devel", :arch_linux => "glib2", :homebrew => "glib", :macports => "glib2", :msys2 => "glib2") exit(false) end hdrs = proc { have_header("vm_core.h") and have_header("iseq.h") and have_header("version.h") and have_header("vm_core.h") and have_header("vm_insnhelper.h") and have_header("vm_core.h") and have_header("method.h") } # Allow use customization of compile options. For example, the # following lines could be put in config_options to to turn off # optimization: # $CFLAGS='-fPIC -fno-strict-aliasing -g3 -ggdb -O2 -fPIC' config_file = File.join(File.dirname(__FILE__), 'config_options.rb') load config_file if File.exist?(config_file) if ENV['debase_debug'] $CFLAGS+=' -Wall -Werror -g3' end $CFLAGS += ' `pkg-config --cflags --libs glib-2.0`' $DLDFLAGS += ' `pkg-config --cflags --libs glib-2.0`' dir_config("ruby") if !Debase::RubyCoreSource.create_makefile_with_core(hdrs, "arg_scanner/arg_scanner") STDERR.print("Makefile creation failed\n") STDERR.print("*************************************************************\n\n") STDERR.print(" NOTE: If your headers were not found, try passing\n") STDERR.print(" --with-ruby-include=PATH_TO_HEADERS \n\n") STDERR.print("*************************************************************\n\n") exit(1) end ================================================ FILE: arg_scanner/lib/arg_scanner/options.rb ================================================ require 'ostruct' module ArgScanner OPTIONS = OpenStruct.new( :enable_type_tracker => ENV['ARG_SCANNER_ENABLE_TYPE_TRACKER'], :enable_state_tracker => ENV['ARG_SCANNER_ENABLE_STATE_TRACKER'], :output_directory => ENV['ARG_SCANNER_DIR'], :catch_only_every_n_call => ENV['ARG_SCANNER_CATCH_ONLY_EVERY_N_CALL'] || 1, :project_root => ENV['ARG_SCANNER_PROJECT_ROOT'], :pipe_file_path => ENV['ARG_SCANNER_PIPE_FILE_PATH'] || '', :buffering => ENV['ARG_SCANNER_BUFFERING'] ) def OPTIONS.set_env ENV['ARG_SCANNER_ENABLE_TYPE_TRACKER'] = self.enable_type_tracker ? "1" : nil ENV['ARG_SCANNER_ENABLE_STATE_TRACKER'] = self.enable_state_tracker ? "1" : nil ENV['ARG_SCANNER_DIR'] = self.output_directory ENV['ARG_SCANNER_CATCH_ONLY_EVERY_N_CALL'] = self.catch_only_every_n_call.to_s ENV['ARG_SCANNER_PROJECT_ROOT'] = self.project_root ENV['ARG_SCANNER_PIPE_FILE_PATH'] = self.pipe_file_path ENV['ARG_SCANNER_BUFFERING'] = self.buffering ? "1" : nil end end ================================================ FILE: arg_scanner/lib/arg_scanner/require_all.rb ================================================ # Copyright (c) 2009 Jarmo Pertman # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. module RequireAll # A wonderfully simple way to load your code. # # The easiest way to use require_all is to just point it at a directory # containing a bunch of .rb files. These files can be nested under # subdirectories as well: # # require_all 'lib' # # This will find all the .rb files under the lib directory and load them. # The proper order to load them in will be determined automatically. # # If the dependencies between the matched files are unresolvable, it will # throw the first unresolvable NameError. # # You can also give it a glob, which will enumerate all the matching files: # # require_all 'lib/**/*.rb' # # It will also accept an array of files: # # require_all Dir.glob("blah/**/*.rb").reject { |f| stupid_file(f) } # # Or if you want, just list the files directly as arguments: # # require_all 'lib/a.rb', 'lib/b.rb', 'lib/c.rb', 'lib/d.rb' # def require_all(*args) # Handle passing an array as an argument args.flatten! options = {:method => :require} options.merge!(args.pop) if args.last.is_a?(Hash) if args.empty? puts "no files were loaded due to an empty Array" if $DEBUG return false end if args.size > 1 # Expand files below directories files = args.map do |path| if File.directory? path Dir[File.join(path, '**', '*.rb')] else path end end.flatten else arg = args.first begin # Try assuming we're doing plain ol' require compat stat = File.stat(arg) if stat.file? files = [arg] elsif stat.directory? files = Dir.glob File.join(arg, '**', '*.rb') else raise ArgumentError, "#{arg} isn't a file or directory" end rescue SystemCallError # If the stat failed, maybe we have a glob! files = Dir.glob arg # Maybe it's an .rb file and the .rb was omitted if File.file?(arg + '.rb') file = arg + '.rb' options[:method] != :autoload ? Kernel.send(options[:method], file) : __autoload(file, file, options) return true end # If we ain't got no files, the glob failed raise LoadError, "no such file to load -- #{arg}" if files.empty? end end return if files.empty? if options[:method] == :autoload files.map! { |file_| [file_, File.expand_path(file_)] } files.each do |file_, full_path| __autoload(file_, full_path, options) end return true end files.map! { |file_| File.expand_path file_ } files.sort! begin failed = [] first_name_error = nil # Attempt to load each file, rescuing which ones raise NameError for # undefined constants. Keep trying to successively reload files that # previously caused NameErrors until they've all been loaded or no new # files can be loaded, indicating unresolvable dependencies. files.each do |file_| begin Kernel.send(options[:method], file_) rescue NameError => ex failed << file_ first_name_error ||= ex rescue ArgumentError => ex # Work around ActiveSuport freaking out... *sigh* # # ActiveSupport sometimes throws these exceptions and I really # have no idea why. Code loading will work successfully if these # exceptions are swallowed, although I've run into strange # nondeterministic behaviors with constants mysteriously vanishing. # I've gone spelunking through dependencies.rb looking for what # exactly is going on, but all I ended up doing was making my eyes # bleed. # # FIXME: If you can understand ActiveSupport's dependencies.rb # better than I do I would *love* to find a better solution raise unless ex.message["is not missing constant"] STDERR.puts "Warning: require_all swallowed ActiveSupport 'is not missing constant' error" STDERR.puts ex.backtrace[0..9] end end # If this pass didn't resolve any NameErrors, we've hit an unresolvable # dependency, so raise one of the exceptions we encountered. if failed.size == files.size raise first_name_error else files = failed end end until failed.empty? true end # Works like require_all, but paths are relative to the caller rather than # the current working directory def require_rel(*paths) # Handle passing an array as an argument paths.flatten! return false if paths.empty? source_directory = File.dirname caller.first.sub(/:\d+$/, '') paths.each do |path| require_all File.join(source_directory, path) end end # Loads all files like require_all instead of requiring def load_all(*paths) require_all paths, :method => :load end # Loads all files by using relative paths of the caller rather than # the current working directory def load_rel(*paths) paths.flatten! return false if paths.empty? source_directory = File.dirname caller.first.sub(/:\d+$/, '') paths.each do |path| require_all File.join(source_directory, path), :method => :load end end # Performs Kernel#autoload on all of the files rather than requiring immediately. # # Note that all Ruby files inside of the specified directories should have same module name as # the directory itself and file names should reflect the class/module names. # For example if there is a my_file.rb in directories dir1/dir2/ then # there should be a declaration like this in my_file.rb: # module Dir1 # module Dir2 # class MyFile # ... # end # end # end # # If the filename and namespaces won't match then my_file.rb will be loaded into wrong module! # Better to fix these files. # # Set $DEBUG=true to see how files will be autoloaded if experiencing any problems. # # If trying to perform autoload on some individual file or some inner module, then you'd have # to always specify *:base_dir* option to specify where top-level namespace resides. # Otherwise it's impossible to know the namespace of the loaded files. # # For example loading only my_file.rb from dir1/dir2 with autoload_all: # # autoload_all File.dirname(__FILE__) + '/dir1/dir2/my_file', # :base_dir => File.dirname(__FILE__) + '/dir1' # # WARNING: All modules will be created even if files themselves aren't loaded yet, meaning # that all the code which depends of the modules being loaded or not will not work, like usages # of define? and it's friends. # # Also, normal caveats of using Kernel#autoload apply - you have to remember that before # applying any monkey-patches to code using autoload, you'll have to reference the full constant # to load the code before applying your patch! def autoload_all(*paths) paths.flatten! return false if paths.empty? require "pathname" options = {:method => :autoload} options.merge!(paths.pop) if paths.last.is_a?(Hash) paths.each do |path| require_all path, {:base_dir => path}.merge(options) end end # Performs autoloading relatively from the caller instead of using current working directory def autoload_rel(*paths) paths.flatten! return false if paths.empty? require "pathname" options = {:method => :autoload} options.merge!(paths.pop) if paths.last.is_a?(Hash) source_directory = File.dirname caller.first.sub(/:\d+$/, '') paths.each do |path| file_path = Pathname.new(source_directory).join(path).to_s require_all file_path, {:method => :autoload, :base_dir => source_directory}.merge(options) end end private def __autoload(file, full_path, options) last_module = "Object" # default constant where namespaces are created into begin base_dir = Pathname.new(options[:base_dir]).realpath rescue Errno::ENOENT raise LoadError, ":base_dir doesn't exist at #{options[:base_dir]}" end Pathname.new(file).realpath.descend do |entry| # skip until *entry* is same as desired directory # or anything inside of it avoiding to create modules # from the top-level directories next if (entry <=> base_dir) < 0 # get the module into which a new module is created or # autoload performed mod = Object.class_eval(last_module) without_ext = entry.basename(entry.extname).to_s const = without_ext.split("_").map {|word| word.capitalize}.join if entry.directory? mod.class_eval "module #{const} end" last_module += "::#{const}" else mod.class_eval do puts "autoloading #{mod}::#{const} from #{full_path}" if $DEBUG autoload const, full_path end end end end end ================================================ FILE: arg_scanner/lib/arg_scanner/starter.rb ================================================ # starter.rb is loaded with "ruby -r" option from bin/arg-scanner # or by IDEA also with "ruby -r" option unless ENV["ARG_SCANNER_ENABLE_STATE_TRACKER"].nil? require_relative 'state_tracker' ArgScanner::StateTracker.new end unless ENV["ARG_SCANNER_ENABLE_TYPE_TRACKER"].nil? require_relative 'arg_scanner' require_relative 'type_tracker' # instantiating type tracker will enable calls tracing and sending the data ArgScanner::TypeTracker.instance end ================================================ FILE: arg_scanner/lib/arg_scanner/state_tracker.rb ================================================ require "set" require_relative "require_all" require_relative "workspace" module ArgScanner class StateTracker def initialize @workspace = Workspace.new @workspace.on_process_start at_exit do begin require_extra_libs @workspace.open_output_json("classes") { |file| print_json(file) } ensure @workspace.on_process_exit end end end private def require_extra_libs begin RequireAll.require_all Rails.root.join('lib') rescue Exception => e end begin Rails.application.eager_load! rescue Exception => e end end def print_json(file) result = { :top_level_constants => parse_top_level_constants, :modules => modules_to_json, :load_path => $: } require "json" file.puts(JSON.dump(result)) end def parse_top_level_constants Module.constants.select { |const| Module.const_defined?(const)}.map do |const| begin value = Module.const_get(const) (!value.is_a? Module) ? { :name => const, :class_name => value.class, :extended => get_extra_methods(value)} : nil rescue Exception => e end end.compact end def get_extra_methods(value) value.methods - value.public_methods end def method_to_json(method) ret = { :name => method.name, :parameters => method.parameters } unless method.source_location.nil? ret[:path] = method.source_location[0] ret[:line] = method.source_location[1] end ret rescue Exception => e nil end def module_to_json(mod) ret = { :name => mod.to_s, :type => mod.class.to_s, :singleton_class_ancestors => mod.singleton_class.ancestors.map{|it| it.to_s}, :ancestors => mod.ancestors.map{|it| it.to_s}, # map to_s is needed because for example "Psych" parsed not correctly into JSON format # it's parsed as: "{}\n" check it by launching in rails console: "JSON.generate(Psych)" :class_methods => mod.methods(false).map {|method| method_to_json(mod.method(method))}.compact, :instance_methods => mod.instance_methods(false).map {|method| method_to_json(mod.instance_method(method))}.compact } ret[:superclass] = mod.superclass if mod.is_a? Class ret rescue Exception => e nil end def modules_to_json ObjectSpace.each_object(Module).map {|mod| module_to_json(mod)} end end end ================================================ FILE: arg_scanner/lib/arg_scanner/type_tracker.rb ================================================ require 'set' require 'socket' require 'singleton' require 'thread' require_relative 'options' module ArgScanner class TypeTrackerPerformanceMonitor def initialize @enable_debug = ENV["ARG_SCANNER_DEBUG"] @call_counter = 0 @handled_call_counter = 0 @submitted_call_counter = 0 @old_handled_call_counter = 0 @time = Time.now end def on_call @submitted_call_counter += 1 end def on_return @call_counter += 1 if enable_debug && call_counter % 100000 == 0 $stderr.puts("calls #{call_counter} handled #{handled_call_counter} submitted #{submitted_call_counter}"\ "delta #{handled_call_counter - old_handled_call_counter} time #{Time.now - @time}") @old_handled_call_counter = handled_call_counter @time = Time.now end end def on_handled_return @handled_call_counter += 1 end private attr_accessor :submitted_call_counter attr_accessor :handled_call_counter attr_accessor :old_handled_call_counter attr_accessor :call_counter attr_accessor :enable_debug end class TypeTracker include Singleton def initialize ArgScanner.init(ENV['ARG_SCANNER_PIPE_FILE_PATH'], ENV['ARG_SCANNER_BUFFERING'], ENV['ARG_SCANNER_PROJECT_ROOT'], ENV['ARG_SCANNER_CATCH_ONLY_EVERY_N_CALL']) @enable_debug = ENV["ARG_SCANNER_DEBUG"] @performance_monitor = if @enable_debug then TypeTrackerPerformanceMonitor.new else nil end TracePoint.trace(:call, &ArgScanner.method(:handle_call)) TracePoint.trace(:return, &ArgScanner.method(:handle_return)) error_msg = ArgScanner.check_if_arg_scanner_ready() if error_msg != nil STDERR.puts error_msg Process.exit(1) end ObjectSpace.define_finalizer(self, proc { ArgScanner.destructor() }) end attr_accessor :enable_debug attr_accessor :performance_monitor attr_accessor :prefix end end ================================================ FILE: arg_scanner/lib/arg_scanner/version.rb ================================================ module ArgScanner VERSION = "0.3.3" end ================================================ FILE: arg_scanner/lib/arg_scanner/workspace.rb ================================================ module ArgScanner class Workspace def initialize @dir = ENV["ARG_SCANNER_DIR"] || "." @pid_file = @dir+"/#{Process.pid}.pid" end def on_process_start File.open(@pid_file, "w") {} end def open_output_json(prefix) path = @dir + "/#{prefix}-#{Time.now.strftime('%Y-%m-%d_%H-%M-%S')}-#{Process.pid}.json" path_tmp_name = path + ".temp" File.open(path_tmp_name, "w") { |file| yield file } require 'fileutils' FileUtils.mv(path_tmp_name, path) end def on_process_exit require 'fileutils' FileUtils.rm(@pid_file) end end end ================================================ FILE: arg_scanner/lib/arg_scanner.rb ================================================ require "arg_scanner/version" require "arg_scanner/arg_scanner" require "arg_scanner/type_tracker" require "arg_scanner/state_tracker" module ArgScanner # Your code goes here... end ================================================ FILE: arg_scanner/test/helper.rb ================================================ $LOAD_PATH.unshift(File.dirname(__dir__) + '/../lib') require "test-unit" require "arg_scanner" class TestTypeTracker include Singleton attr_reader :last_args_info attr_reader :last_call_info def initialize @tp = TracePoint.new(:call, :return) do |tp| case tp.event when :call ArgScanner.handle_call(tp) @last_args_info = ArgScanner.get_args_info.split ';' @last_call_info = ArgScanner.get_call_info when :return ArgScanner.handle_return(tp) end end end def enable(*args, &b) @tp.enable *args, &b end def signatures Thread.current[:signatures] ||= Array.new end end ================================================ FILE: arg_scanner/test/test_args_info.rb ================================================ #!/usr/bin/env ruby require File.expand_path("helper", File.dirname(__FILE__)) require 'date' class TestArgsInfoWrapper def foo(a); end def foo2(a, b = 1); end def foo3(**rest); end def foo4(kw: :symbol, **rest1); end def foo5(kw:, **rest); end def foo6(a, *rest, b); end def initialize # @trace = TracePoint.new(:call) do |tp| # case tp.event # when :call # tp.binding.local_variables.each { |v| p tp.binding.eval v.to_s } # ArgScanner.handle_call(tp.lineno, tp.method_id, tp.path) # @args_info = ArgScanner.get_args_info # p @args_info # end # end end end class TestArgsInfo < Test::Unit::TestCase # @!attribute [r] type_tracker # @return [TestTypeTracker] attr_reader :type_tracker def setup @args_info_wrapper = TestArgsInfoWrapper.new @type_tracker = TestTypeTracker.instance end def teardown end def test_simple_kwrest type_tracker.enable do @args_info_wrapper.foo3(a: Date.new, kkw: 'hi') end assert_equal ["KEYREST,Date,a", "KEYREST,String,kkw"], type_tracker.last_args_info end def test_empty_kwrest type_tracker.enable do @args_info_wrapper.foo3() end assert_equal [], type_tracker.last_args_info end def test_req_and_opt_arg type_tracker.enable do @args_info_wrapper.foo2(Date.new) end assert_equal "REQ,Date,a", type_tracker.last_args_info[0] assert type_tracker.last_args_info[1] == "OPT,Fixnum,b" || type_tracker.last_args_info[1] == "OPT,Integer,b" end def test_optkw_and_empty_kwrest type_tracker.enable do @args_info_wrapper.foo4(kw: Date.new) end assert_equal ["KEY,Date,kw"], type_tracker.last_args_info end def test_reqkw_and_empty_kwrest type_tracker.enable do @args_info_wrapper.foo5(kw: Date.new) end assert_equal ["KEYREQ,Date,kw"], type_tracker.last_args_info end def test_reqkw_and_kwrest type_tracker.enable do @args_info_wrapper.foo5(kw: Date.new, aa: true, bb: '1') end assert_equal ["KEYREQ,Date,kw", "KEYREST,TrueClass,aa", "KEYREST,String,bb"], type_tracker.last_args_info end def test_optkw_and_kwrest type_tracker.enable do @args_info_wrapper.foo4(aa: :symbol, bb: '1') end assert_equal ["KEYREST,Symbol,aa", "KEYREST,String,bb"], type_tracker.last_args_info end def test_optkw_passed_and_kwrest type_tracker.enable do @args_info_wrapper.foo4(kw: 'bla-bla', aa: :symbol, bb: '1') end assert_equal ["KEY,String,kw", "KEYREST,Symbol,aa", "KEYREST,String,bb"], type_tracker.last_args_info end def test_rest type_tracker.enable do @args_info_wrapper.foo6(1, 'hi', Date.new, '1') end assert type_tracker.last_args_info[0] == "REQ,Fixnum,a" || type_tracker.last_args_info[0] == "REQ,Integer,a" assert type_tracker.last_args_info[1] == "REST,Array,rest" assert type_tracker.last_args_info[2] == "POST,String,b" end def test_empty_rest type_tracker.enable do @args_info_wrapper.foo6(1, '1') end assert type_tracker.last_args_info[0] == "REQ,Fixnum,a" || type_tracker.last_args_info[0] == "REQ,Integer,a" assert type_tracker.last_args_info[1] == "REST,Array,rest" assert type_tracker.last_args_info[2] == "POST,String,b" end end ================================================ FILE: arg_scanner/test/test_call_info.rb ================================================ #!/usr/bin/env ruby require File.expand_path("helper", File.dirname(__FILE__)) class TestCallInfoWrapper def sqr(z1 = 10, z2 = 11, z3 = 13, z4 = 14, z5, z6, z7, z8, y: '0', x: "40") end def sqr2(z0, z1 = 2, z2 = 10, z3 = 2, z4 = 0, y: 1, x: 30, z: '40') end def foo(a, b, c, *d, e) end def foo2(*args) end def foo3(b: 2, c: '3', **args) end def foo4(b: 2, c:, d: "1", dd: 1, ddd: '111', **args) end def foo5(b) end end class TestCallInfo < Test::Unit::TestCase # @!attribute [r] type_tracker # @return [TestTypeTracker] attr_reader :type_tracker def setup @call_info_wrapper = TestCallInfoWrapper.new @type_tracker = TestTypeTracker.instance end def teardown end def test_simple type_tracker.enable do @call_info_wrapper.sqr2(10, 11) end assert_not_nil type_tracker.last_call_info #assert type_tracker.last_call_info.size == 2 #assert type_tracker.last_call_info[0] == "sqr2" assert_equal 2, type_tracker.last_call_info[0] end def test_simple_req_arg type_tracker.enable do @call_info_wrapper.foo5(10) end assert_nil type_tracker.last_call_info end def test_simple_kw type_tracker.enable do @call_info_wrapper.sqr2(10, 11, x: 10, y: 1) end assert_not_nil type_tracker.last_call_info #assert type_tracker.last_call_info.size == 3 #assert type_tracker.last_call_info[0] == "sqr2" assert_equal 4, type_tracker.last_call_info[0] assert_equal "x,y", type_tracker.last_call_info[1] end def test_rest type_tracker.enable do @call_info_wrapper.foo2(1, 2, 3, 4, 5, 6, 7, 8) end assert_not_nil type_tracker.last_call_info #assert type_tracker.last_call_info.size == 2 #assert type_tracker.last_call_info[0] == "foo2" assert_equal 8, type_tracker.last_call_info[0] end def test_post_and_rest type_tracker.enable do @call_info_wrapper.foo(1, 2, 3, 4, 5, 6, 7, 8) end #coz it is obvious that all the arguments were passed (they are all required) assert_not_nil type_tracker.last_call_info #assert type_tracker.last_call_info.size == 2 #assert type_tracker.last_call_info[0] == "foo" #assert type_tracker.last_call_info[0] == 8 end def test_kwrest type_tracker.enable do @call_info_wrapper.foo3(a: 1, b: 2, c: 3, d: 4) end assert_not_nil type_tracker.last_call_info #assert type_tracker.last_call_info.size == 3 #assert type_tracker.last_call_info[0] == "foo3" assert_equal 4, type_tracker.last_call_info[0] assert_equal "a,b,c,d", type_tracker.last_call_info[1] end def test_rest_and_reqkw_args type_tracker.enable do @call_info_wrapper.foo4(b: "hello", c: 'world', e: 1, f: "not") end assert_not_nil type_tracker.last_call_info #assert type_tracker.last_call_info.size == 3 #assert type_tracker.last_call_info[0] == "foo4" assert_equal 4, type_tracker.last_call_info[0] assert_equal "b,c,e,f", type_tracker.last_call_info[1] end end ================================================ FILE: arg_scanner/test/test_state_tracker.rb ================================================ require 'test/unit' require 'tempfile' require 'fileutils' require 'json' class StateTrackerTest < Test::Unit::TestCase class << self #Runs only once at start def startup file = Tempfile.new("StateTracker") dirname = file.path FileUtils.rm(dirname) file.close begin FileUtils.makedirs(dirname) 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") files = Dir["#{dirname}/*.json"] @@json = JSON.parse(File.read(files[0])) ensure FileUtils.rm_rf(dirname) end end end def test_has_struct assert_not_nil(get_class_with_name("Struct")) end def test_symbol_is_fine symbol = get_class_with_name("Symbol") assert_not_nil(symbol) assert_equal(symbol["type"], "Class") assert_equal(symbol["superclass"], "Object") assert_not_nil(symbol["singleton_class_ancestors"].find_index("Kernel")) assert_not_nil(symbol["ancestors"].find_index("Comparable")) assert_not_nil(get_class_method(symbol, "all_symbols")) assert_not_nil(get_instance_method(symbol, "match")) parameters = get_instance_method(symbol, "match")['parameters'] assert_not_nil(parameters) assert_equal(parameters[0][0], (RUBY_VERSION < "2.4.0") ? "req" : "rest") end def test_loaded_path_is_fine assert_not_nil(@@json["load_path"]) assert_not_nil(@@json["load_path"][0]) end def test_constant_is_fine assert_not_nil(@@json["top_level_constants"]) assert_not_nil(@@json["top_level_constants"][0]) assert_not_nil(@@json["top_level_constants"][0]["name"]) assert_not_nil(@@json["top_level_constants"][0]["class_name"]) assert_not_nil(@@json["top_level_constants"][0]["extended"]) end private def get_class_method(symbol, name) get_named_entity(symbol, "class_methods", name) end def get_instance_method(symbol, name) get_named_entity(symbol, "instance_methods", name) end def get_class_with_name(name) get_named_entity(@@json, "modules", name) end def get_named_entity(obj, index, name) obj[index].find {|entity| entity["name"] == name} end end ================================================ FILE: arg_scanner/util/state_filter.rb ================================================ #!/usr/bin/env ruby require 'json' require 'set' if ARGV.length < 3 puts("state_filter.rb [ output_modules, :load_path => json["load_path"]})) ================================================ FILE: build.gradle ================================================ buildscript { repositories { jcenter() mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7" classpath 'org.apache.httpcomponents:httpclient:4.5.2' } } allprojects { repositories { mavenCentral() maven { url 'https://dl.bintray.com/kotlin/exposed' } } apply plugin: 'java' apply plugin: 'kotlin' def project = it dependencies { if (project.name != 'ide-plugin') { compile 'org.jetbrains:annotations:15.0' compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" } testCompile 'junit:junit:4.12' testCompile 'com.h2database:h2:1.4.193' } compileKotlin { kotlinOptions.jvmTarget = "1.8" } test { systemProperties System.properties testLogging { exceptionFormat = 'full' } } } task wrapper(type: Wrapper) { gradleVersion = '4.10.2' } subprojects { if (it.name in ['storage-server-api', 'lambda-update-handler', 'lambda-put-handler', 'contract-creator', 'state-tracker', 'ide-plugin']) { dependencies { compile 'com.google.code.gson:gson:2.8.0' } } } ================================================ FILE: common/build.gradle ================================================ buildscript { repositories { jcenter() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } dependencies { } sourceSets { main.java.srcDirs = ['src/main/java'] main.kotlin.srcDirs = ['src/main/java'] test.kotlin.srcDirs = ['src/test/java'] } ================================================ FILE: common/src/main/java/org/jetbrains/ruby/codeInsight/Injector.kt ================================================ package org.jetbrains.ruby.codeInsight /** * Dependency injection mechanism */ interface Injector { fun getLogger(cl: Class): Logger } @Volatile private var _injector: Injector? = null val injector: Injector get() { return _injector ?: throw IllegalStateException("Injector must be initialized before any usage") } // Because the we don't know anything about injector initializators we assume that it can be // potentially multi threaded but necessity of injector initialization thread safety isn't really investigated @Synchronized fun initInjector(injector: Injector) { check(_injector == null) { "Injector must be initialized only once" } _injector = injector } ================================================ FILE: common/src/main/java/org/jetbrains/ruby/codeInsight/Logger.kt ================================================ package org.jetbrains.ruby.codeInsight interface Logger { fun info(msg: String) } ================================================ FILE: common/src/main/java/org/jetbrains/ruby/codeInsight/PrintToStdoutLogger.kt ================================================ package org.jetbrains.ruby.codeInsight import java.text.SimpleDateFormat import java.util.* private val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") /** * Most basic [Logger] implementation */ class PrintToStdoutLogger(private val category: String) : Logger { constructor(cl : Class<*>) : this(cl.name) override fun info(msg: String) { println("${format.format(Calendar.getInstance())} [$category] $msg") } } ================================================ FILE: contract-creator/build.gradle ================================================ sourceSets { main.java.srcDirs = ['src'] } dependencies { compile project(':common') compile project(':ruby-call-signature') compile project(':storage-server-api') // compile 'com.h2database:h2:1.4.193' } task runServer(type: JavaExec) { classpath sourceSets.main.runtimeClasspath main = 'org.jetbrains.ruby.runtime.signature.server.SignatureServerKt' } ================================================ FILE: contract-creator/src/org/jetbrains/ruby/runtime/signature/server/SignatureServer.kt ================================================ package org.jetbrains.ruby.runtime.signature.server import com.google.gson.Gson import com.google.gson.JsonParseException import com.google.gson.JsonSyntaxException import org.jetbrains.ruby.codeInsight.initInjector import org.jetbrains.ruby.codeInsight.types.signature.CallInfo import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoTable import org.jetbrains.ruby.runtime.signature.server.serialisation.ServerResponseBean import org.jetbrains.ruby.runtime.signature.server.serialisation.toCallInfo import java.io.File import java.io.FileInputStream import java.io.IOException import java.nio.file.Paths import java.util.* import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicLong import java.util.logging.Logger import kotlin.concurrent.thread private const val EXIT_COMMAND = "EXIT"; fun main(args: Array) { initInjector(SignatureServerInjector) parseArgs(args).let { DatabaseProvider.connectToDB(it.dbFilePath, isDefaultDatabase = true) } val pipeFileName = SignatureServer().runServerAsync(isDaemon = false) println("Pass this to arg-scanner via --pipe-file-path: $pipeFileName") // Intercept Ctrl+C Runtime.getRuntime().addShutdownHook(thread(start = false) { File(pipeFileName).delete() }) } private data class ParsedArgs(val dbFilePath: String) private fun parseArgs(args: Array): ParsedArgs { if (args.size != 1) { System.err.println(""" One argument required: path-to-h2-db-file Or if you run it via gradle: ./gradlew contract-creator:runServer --args path-to-db """.trimIndent()) System.exit(1) } return ParsedArgs(args.single()) } class SignatureServer { companion object { private const val LOCAL_STORAGE_SIZE_LIMIT = 128 @Suppress("ObjectPropertyName") private val _runningServers: MutableList = Collections.synchronizedList(mutableListOf()) val runningServers: List get() = _runningServers } private val LOGGER = Logger.getLogger("SignatureServer") private val callInfoContainer = LinkedList() private val gson = Gson() private val queue = ArrayBlockingQueue(10024) private val isReady = AtomicBoolean(true) private var previousPollEndedWithFlush = false val readTime = AtomicLong(0) val jsonTime = AtomicLong(0) val addTime = AtomicLong(0) private val signatureHandler = SignatureHandler() private val pollJsonThread = PollJsonThread() fun isProcessingRequests() = !isReady.get() private fun generateTempFilePath(prefix: String = ""): String { val dirForTempFiles = System.getProperty("java.io.tmpdir") return Paths.get(dirForTempFiles, prefix + UUID.randomUUID()).toString() } /** * @return pipe filename path which should be passed to arg-scanner */ fun runServerAsync(isDaemon: Boolean): String { isReady.set(false) _runningServers.add(this) LOGGER.info("Starting server") val pipeFileName = generateTempFilePath(prefix = "ruby-type-inference-pipe-") val proc: Process = Runtime.getRuntime().exec("mkfifo $pipeFileName") if (proc.waitFor() != 0) { throw RuntimeException("Cannot create pipe file") } signatureHandler.pipeFilePath = pipeFileName signatureHandler.isDaemon = isDaemon signatureHandler.start() pollJsonThread.isDaemon = isDaemon pollJsonThread.start() return pipeFileName } var afterFlushListener: (() -> Unit)? = null var afterExitListener: (() -> Unit)? = null /** * @return true when client won't send data anymore */ private fun pollJson(): Boolean { val jsonString by lazy { if (previousPollEndedWithFlush) queue.take() else queue.poll() } if (callInfoContainer.size > LOCAL_STORAGE_SIZE_LIMIT || jsonString == null || jsonString == EXIT_COMMAND) { flushNewTuplesToMainStorage() previousPollEndedWithFlush = true return jsonString == EXIT_COMMAND } previousPollEndedWithFlush = false parseJson(jsonString) return false } private fun parseJson(jsonString: String) { val currCallInfo = ben(jsonTime) { try { return@ben gson.fromJson(jsonString, ServerResponseBean::class.java)?.toCallInfo() } catch (ex: Throwable) { when (ex) { is JsonSyntaxException, is JsonParseException -> { // Sometimes it's possible that some json fields contain quotation mark and we got JsonSyntaxException LOGGER.severe("Cannot parse: $jsonString") } is IllegalStateException -> { LOGGER.severe(ex.message) } else -> throw ex } return@ben null } } // filter, for example, such things # if (currCallInfo?.methodInfo?.classInfo?.classFQN?.startsWith("#<") == true) { return } if (currCallInfo != null) { ben(addTime) { callInfoContainer.add(currCallInfo) } } } private fun flushNewTuplesToMainStorage() { DatabaseProvider.defaultDatabaseTransaction { for (callInfo in callInfoContainer) { CallInfoTable.insertInfoIfNotContains(callInfo) } } callInfoContainer.clear() afterFlushListener?.invoke() } private inner class SignatureHandler internal constructor() : Thread() { var pipeFilePath: String = "" override fun run() { try { var missed = 0 var br = FileInputStream(pipeFilePath).bufferedReader() var currString: String? = "" do { // continue when EOF is reached because EOF doesn't mean that program // traced by arg-scanner is died. Program could simply call `Kernel.exec` // See CallStatCompletionTest.testRubyExecWithBuffering and // CallStatCompletionTest.testRubyExecWithoutBuffering currString = ben(readTime) { br.readLine() } if (currString != null) { queue.put(currString) } else { missed++ br.close() // If don't reassign reader then `readLine` will always return `null` br = FileInputStream(pipeFilePath).bufferedReader() } // 1000 is just threshold for safety } while (currString != EXIT_COMMAND && missed < 1000) } catch (e: IOException) { LOGGER.severe("Error in SignatureHandler") } finally { File(pipeFilePath).delete() } } } private inner class PollJsonThread : Thread() { override fun run() { while (true) { if (pollJson()) { isReady.set(true) afterExitListener?.invoke() _runningServers.remove(this@SignatureServer) break } } } } } fun ben(x: AtomicLong, F: ()->T): T { val start = System.nanoTime() try { return F.invoke() } finally { x.addAndGet(System.nanoTime() - start) } } ================================================ FILE: contract-creator/src/org/jetbrains/ruby/runtime/signature/server/SignatureServerInjector.kt ================================================ package org.jetbrains.ruby.runtime.signature.server import org.jetbrains.ruby.codeInsight.Injector import org.jetbrains.ruby.codeInsight.Logger import org.jetbrains.ruby.codeInsight.PrintToStdoutLogger object SignatureServerInjector : Injector { override fun getLogger(cl: Class): Logger { return PrintToStdoutLogger(cl) } } ================================================ FILE: contract-creator/src/org/jetbrains/ruby/runtime/signature/server/serialisation/ServerResponseBean.kt ================================================ package org.jetbrains.ruby.runtime.signature.server.serialisation import org.jetbrains.ruby.codeInsight.types.signature.* data class ServerResponseBean( val method_name: String, /** * Number of unnamedArguments passed by user explicitly * * For example for method: * def foo(a, b = 1); end * * This method invocation have only one explicit argument * foo(4) * * But this method invocation have two explicit unnamedArguments * foo(4, 5) */ val call_info_argc: Int, val args_info: String, val visibility: String, val path: String, val lineno: Int, val receiver_name: String, val return_type_name: String) // explicit here means that this unnamedArguments was explicitly provided by user // for example: // def foo(a, b = 1); end // foo(1) # here only `a` is explicitly provided // foo(1, 5) # here `a` and `b` are both explicitly provided private data class Arg(val paramInfo: ParameterInfo, val type: String, var explicit: Boolean) private const val PARAMETER_MODIFIER_INDEX_IN_ATTRIBUTES = 0 private const val PARAMETER_TYPE_INDEX_IN_ATTRIBUTES = 1 private const val PARAMETER_NAME_INDEX_IN_ATTRIBUTES = 2 private const val NUMBER_OF_ATTRIBUTES_FOR_PARAMETER = 3 /** * @throws IllegalStateException if [ServerResponseBean] is not correctly formed */ fun ServerResponseBean.toCallInfo(): CallInfo { var argc = this.call_info_argc val args = this.args_info.takeIf { it != "" }?.split(";")?.map { val parts: List = it.split(",") val modifier = parts[PARAMETER_MODIFIER_INDEX_IN_ATTRIBUTES] val type = parts[PARAMETER_TYPE_INDEX_IN_ATTRIBUTES] val name = if (parts.size == NUMBER_OF_ATTRIBUTES_FOR_PARAMETER) { // It's possible that parameter in ruby doesn't have name, for example: // def foo(*); end parts[PARAMETER_NAME_INDEX_IN_ATTRIBUTES] } else { "" } // If argc == -1 then all args are explicitly passed return@map Arg(ParameterInfo(name, ParameterInfo.Type.valueOf(modifier)), type, explicit = argc == -1) } ?: emptyList() if (argc != -1) { for (arg in args) { if (arg.paramInfo.isNamedParameter || arg.paramInfo.modifier == ParameterInfo.Type.REQ || arg.paramInfo.modifier == ParameterInfo.Type.POST) { arg.explicit = true argc-- } } for (arg in args) { if (argc <= 0) { break } if (arg.paramInfo.modifier == ParameterInfo.Type.OPT) { arg.explicit = true argc-- } } for (arg in args) { if (argc <= 0) { break } if (arg.paramInfo.modifier == ParameterInfo.Type.REST) { arg.explicit = true argc-- } } check(argc == 0 || args.any { it.paramInfo.modifier == ParameterInfo.Type.BLOCK } && argc == 1) { "Failed to parse this bean: ${this.toString()}" } } val namedArgumentsNamesToTypes = args.asSequence().filter { it.paramInfo.isNamedParameter } .map { ArgumentNameAndType(it.paramInfo.name, it.type) }.toList() val unnamedArgumentsTypes = args.asSequence().filter { !it.paramInfo.isNamedParameter } .map { arg -> ArgumentNameAndType(arg.paramInfo.name, arg.type.takeIf { arg.explicit } ?: ArgumentNameAndType.IMPLICITLY_PASSED_ARGUMENT_TYPE) }.toList() val methodInfo = MethodInfo.Impl( ClassInfo.Impl(gemInfoFromFilePathOrNull(this.path), this.receiver_name), this.method_name, RVisibility.valueOf(this.visibility), Location(this.path, this.lineno)) return CallInfoImpl(methodInfo, namedArgumentsNamesToTypes, unnamedArgumentsTypes, this.return_type_name) } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Wed Nov 07 19:25:40 MSK 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip ================================================ FILE: gradle.properties ================================================ # Available idea versions: # https://www.jetbrains.com/intellij-repository/releases # https://www.jetbrains.com/intellij-repository/snapshots # ruby plugin versions can be found here: # https://plugins.jetbrains.com/plugin/1293-ruby/versions kotlin_version=1.2.70 ideaVersion=IU-193.5233.102 rubyPluginVersion=193.5233.57 exposedVersion=0.17.3 ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: ide-plugin/CHANGELOG.md ================================================ ## 0.1.1 (15 Dec 2017) * (#17) Fix "find usages" action for dynamic symbols which resolve to text-based definitions. ## 0.1 (29 Nov 2017) Initial plugin version * Collect State action Adds on_exit hook which dumps class/module includes structure and contained methods which can be used for resolution/completion later. * Collect Type action Enables call tracing (with a considerable slowdown) and dumps return types which can be used for better type inference. * Symbol/Type provider to improve resolution and type inference based on the collected data. ================================================ FILE: ide-plugin/build.gradle ================================================ buildscript { repositories { maven { url 'https://dl.bintray.com/jetbrains/intellij-plugin-service' } } } plugins { id "org.jetbrains.intellij" version "0.3.11" } dependencies { def withoutKotlinAndMySql = { exclude group: 'org.jetbrains.kotlin' exclude group: 'mysql' } def withoutSlfAndKotlinAndMySql = { exclude group: 'org.slf4j'//, module: 'slf4j-api' exclude group: 'org.jetbrains.kotlin' exclude group: 'mysql' } compile project(':common') compile project(':ruby-call-signature'), withoutKotlinAndMySql compile project(':storage-server-api'), withoutSlfAndKotlinAndMySql compile project(':contract-creator'), withoutSlfAndKotlinAndMySql compile project(':state-tracker'), withoutSlfAndKotlinAndMySql // https://mvnrepository.com/artifact/com.h2database/h2 compile group: 'com.h2database', name: 'h2', version: '1.4.199' } sourceSets { main.java.srcDirs = ['src'] main.resources.srcDirs = ['resources'] test.java.srcDirs = ['src/test/java'] test.resources.srcDirs=['src/test/testData'] } intellij { version ideaVersion pluginName 'ruby-runtime-stats' plugins = ["yaml", "org.jetbrains.plugins.ruby:$rubyPluginVersion"] } patchPluginXml { sinceBuild '193.5233.102' untilBuild '193.*' version '0.3.3' } prepareSandbox.doLast { def destDir = "$it.destinationDir/$intellij.pluginName" copy { from sourceSets.main.resources.include("**/*.rb", "**/*.db") into destDir } } test { testLogging.showStandardStreams = true } ================================================ FILE: ide-plugin/resources/META-INF/plugin.xml ================================================ org.jetbrains.ruby-runtime-stats Ruby Dynamic Code Insight JetBrains com.intellij.modules.ruby This plugin provides additional Code Insight intelligence to improve resolution, find usages and refactoring capabilities.

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

]]>
Changelog ]]> org.jetbrains.plugins.ruby.ruby.intentions.AddContractAnnotationIntention org.jetbrains.plugins.ruby.ruby.intentions.RemoveCollectedInfoIntention
================================================ FILE: ide-plugin/src/com/intellij/execution/executors/CollectStateExecutor.kt ================================================ package com.intellij.execution.executors import com.intellij.execution.Executor import com.intellij.icons.AllIcons import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.wm.ToolWindowId import javax.swing.Icon class CollectStateExecutor : Executor() { private val myIcon = AllIcons.General.GearPlain override fun getToolWindowId(): String { return ToolWindowId.RUN } override fun getToolWindowIcon(): Icon { return myIcon } override fun getIcon(): Icon { return myIcon } override fun getDisabledIcon(): Icon? { return null } override fun getDescription(): String { return "Run selected configuration with collecting state" } override fun getActionName(): String { return "CollectState" } override fun getId(): String { return EXECUTOR_ID } override fun getStartActionText(): String { return "Run with Collecting State" } override fun getContextActionId(): String { return "RunCollectState" } override fun getHelpId(): String? { return null } override fun getStartActionText(configurationName: String): String { val name = escapeMnemonicsInConfigurationName( StringUtil.first(configurationName, 30, true)) return "Run" + (if (StringUtil.isEmpty(name)) "" else " '$name'") + " with Collecting State" } private fun escapeMnemonicsInConfigurationName(configurationName: String): String { return configurationName.replace("_", "__") } companion object { val EXECUTOR_ID = "CollectState" } } ================================================ FILE: ide-plugin/src/com/intellij/execution/executors/RunWithTypeTrackerExecutor.java ================================================ package com.intellij.execution.executors; import com.intellij.execution.Executor; import com.intellij.icons.AllIcons; import com.intellij.openapi.util.IconLoader; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.wm.ToolWindowId; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.net.URL; public class RunWithTypeTrackerExecutor extends Executor { @NotNull public static final String EXECUTOR_ID = "RunWithTypeTracker"; @NotNull private final Icon myIcon; public RunWithTypeTrackerExecutor() { final URL iconURL = RunWithTypeTrackerExecutor.class.getClassLoader().getResource( UIUtil.isUnderDarcula() ? "runWithTypeTracker_dark.svg" : "runWithTypeTracker.svg"); final Icon icon = IconLoader.findIcon(iconURL); myIcon = icon != null ? icon : AllIcons.General.Error; } @Override public String getToolWindowId() { return ToolWindowId.RUN; } @Override public Icon getToolWindowIcon() { return myIcon; } @NotNull @Override public Icon getIcon() { return myIcon; } @Nullable @Override public Icon getDisabledIcon() { return null; } @NotNull @Override public String getDescription() { return "Run selected configuration with Type Tracker"; } @NotNull @Override public String getActionName() { return "Run with Type Tracker"; } @NotNull @Override public String getId() { return EXECUTOR_ID; } @NotNull public String getStartActionText() { return "Run with Type Tracker"; } @NotNull @Override public String getContextActionId() { return "Run with Type Tracker"; } @Nullable @Override public String getHelpId() { return null; } @NotNull @Override public String getStartActionText(@NotNull final String configurationName) { final String name = escapeMnemonicsInConfigurationName( StringUtil.first(configurationName, 30, true)); return "Run" + (StringUtil.isEmpty(name) ? "" : " '" + name + "'") + " with Type Tracker"; } @NotNull private static String escapeMnemonicsInConfigurationName(@NotNull final String configurationName) { return configurationName.replace("_", "__"); } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/IdePluginLogger.kt ================================================ package org.jetbrains.plugins.ruby import org.jetbrains.ruby.codeInsight.Logger class IdePluginLogger(private val intellijPlatformLogger: com.intellij.openapi.diagnostic.Logger) : Logger { override fun info(msg: String) { intellijPlatformLogger.info(msg) } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/PluginResourceUtil.java ================================================ package org.jetbrains.plugins.ruby; import com.intellij.ide.plugins.IdeaPluginDescriptor; import com.intellij.ide.plugins.PluginManager; import com.intellij.openapi.extensions.PluginId; import org.jetbrains.annotations.NotNull; import java.io.File; public final class PluginResourceUtil { private static final String PLUGIN_ID = "org.jetbrains.ruby-runtime-stats"; private PluginResourceUtil() { } @NotNull public static String getPluginResourcesPath() { final IdeaPluginDescriptor plugin = PluginManager.getPlugin(PluginId.getId(PLUGIN_ID)); if (plugin == null) { throw new AssertionError("Nonsense: this plugin is not registered"); } final File pluginHome = plugin.getPath(); if (pluginHome == null) { throw new AssertionError("Corrupted plugin: could not find home"); } return pluginHome.getPath() + "/"; } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/RubyDynamicCodeInsightPluginInjector.kt ================================================ package org.jetbrains.plugins.ruby import org.jetbrains.ruby.codeInsight.Injector import org.jetbrains.ruby.codeInsight.Logger object RubyDynamicCodeInsightPluginInjector : Injector { override fun getLogger(cl: Class): Logger { return IdePluginLogger(com.intellij.openapi.diagnostic.Logger.getInstance(cl)) } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ancestorsextractor/AncestorsExtractor.kt ================================================ package org.jetbrains.plugins.ruby.ancestorsextractor import com.intellij.openapi.application.ReadAction import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.ThrowableComputable import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.structure.SymbolUtil import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.structure.util.SymbolHierarchy import java.io.FileWriter import java.io.PrintWriter /** * Keeps Ruby module [name] and it's [ancestors] */ data class RubyModule(val name: String, val ancestors: List) /** * Ancestors extractor for Ruby's project's modules */ interface AncestorsExtractorBase { /** * Extract ancestors for every Ruby's Module in [project] */ fun extractAncestors(project: Project, sdk: Sdk): List /** * Set [RailsConsoleRunner.Listener] for [RailsConsoleRunner] * @see RailsConsoleRunner.Listener */ var listener: RailsConsoleRunner.Listener? } /** * Extract ancestors for Ruby's modules the way how RubyMine sees them. * If you don't provide [allModulesNames] this implementation works only for Ruby on Rails project as in case when * you don't provide [allModulesNames] all modules names would be taken from Ruby on Rails console ("bin/rails console") */ class AncestorsExtractorByRubyMine(private val allModulesNames: List? = null, override var listener: RailsConsoleRunner.Listener? = null) : AncestorsExtractorBase { /** * Implementation of [AncestorsExtractorBase.extractAncestors] based on how RubyMine sees ancestors */ override fun extractAncestors(project: Project, sdk: Sdk): List { val allModulesNamesLocal: List = allModulesNames ?: extractAllModulesNames(project, sdk) // I don't know why but seems that SymbolScopeUtil#getAncestorsCaching needs to be called // inside ReadAction otherwise sometimes I get Exception return ReadAction.compute(ThrowableComputable, Exception> { allModulesNamesLocal.map { RubyModule(it, extractAncestorsFor(it, project)) } }) } /** * Extract all modules names from Ruby on Rails project. */ private fun extractAllModulesNames(project: Project, sdk: Sdk): List { val tempFile = createTempFile(prefix = "modules", suffix = ".json") try { val rubyCode = """ require 'json' open("${tempFile.path}", "w") do |f| f.puts JSON.generate(ObjectSpace.each_object(Module).to_a.map {|from| from.to_s}) end """.trimIndent() return RailsConsoleRunner(listener).extractFromRubyOnRailsConsole(project, sdk, Array::class.java, tempFile.path, rubyCode, eagerLoad = true).toList() } finally { tempFile.delete() } } private fun extractAncestorsFor(rubyModuleName: String, project: Project): List { return SymbolUtil.findConstantByFQN(project, rubyModuleName)?.let { SymbolHierarchy.getAncestorsCaching(it, null) }?.map { it.symbol.fqnWithNesting.toString() } ?: listOf() } } /** * Extract ancestors the way Ruby's Module.ancestors method does this. * This implementation works only for Ruby on Rails project. * @see Ruby's Module.ancestors */ class AncestorsExtractorByObjectSpace(override var listener: RailsConsoleRunner.Listener? = null) : AncestorsExtractorBase { /** * Implementation of [AncestorsExtractorBase.extractAncestors] based on Ruby's Module.ancestors method * @see Ruby's Module.ancestors */ override fun extractAncestors(project: Project, sdk: Sdk): List { val tempFile = createTempFile(prefix = "module-ancestors-pair", suffix = ".json") try { val rubyCode = """ objects = ObjectSpace.each_object(Module).to_a; nil # nil is to prevent irb to print big objects output objects = objects.map {|mod| {:name => mod.to_s, :ancestors => mod.ancestors.map {|from| from.to_s}}}; nil require 'json' open("${tempFile.path}", "w") do |f| f.puts JSON.generate(objects) end """.trimIndent() return RailsConsoleRunner(listener).extractFromRubyOnRailsConsole(project, sdk, Array::class.java, tempFile.path, rubyCode, eagerLoad = true).toList() } finally { tempFile.delete() } } /** * Extract information about where ruby includes was performed * @return Map where key is [String] with the following format: "**ancestor**#**includer**" and value is [String] * containing ruby file path and line number where **ancestor** was included by **includer** */ fun extractIncludes(project: Project, sdk: Sdk): Map { val tempWhereIncluded = createTempFile(prefix = "where-included", suffix = ".json") val tempRubyCodeFile = createTempFile(prefix = "preload-temp-script", suffix = ".rb") try { PrintWriter(FileWriter(tempRubyCodeFile)).use { it.println(""" END { require 'json' open("${tempWhereIncluded.path}", "w") { |f| f.puts JSON.generate(RubyDetectIncludeUniqueModuleName.get) } } module RubyDetectIncludeUniqueModuleName @@map = {} def self.get return @@map end def append_features(mod) # self included by mod @@map["#{self.to_s}##{mod.to_s}"] = caller_locations()[1].to_s super end end Module.prepend RubyDetectIncludeUniqueModuleName """.trimIndent()) } @Suppress("UNCHECKED_CAST") return RailsConsoleRunner(listener).extractFromRubyOnRailsConsole(project, sdk, Map::class.java, tempWhereIncluded.path, rubyCode = "", eagerLoad = true, rubyConsoleArguments = arrayOf("-r", tempRubyCodeFile.path)) as Map } finally { tempRubyCodeFile.delete() tempWhereIncluded.delete() } } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ancestorsextractor/RailsConsoleRunner.kt ================================================ package org.jetbrains.plugins.ruby.ancestorsextractor import com.google.gson.Gson import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionModes import com.intellij.execution.process.ProcessEvent import com.intellij.execution.process.ProcessListener import com.intellij.openapi.application.ReadAction import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.Key import com.intellij.openapi.util.ThrowableComputable import org.jetbrains.plugins.ruby.ancestorsextractor.RailsConsoleRunner.Listener import org.jetbrains.plugins.ruby.rails.Rails3Constants import org.jetbrains.plugins.ruby.rails.Rails4Constants import org.jetbrains.plugins.ruby.ruby.RubyUtil import org.jetbrains.plugins.ruby.ruby.run.context.RubyScriptExecutionContext import java.io.File import java.io.IOException import java.io.PrintWriter import java.nio.file.Paths /** * Runs some Ruby code on Ruby on Rails console ("bin/rails console") */ class RailsConsoleRunner( /** * Set [Listener]. There is only one possible [Listener]. Feel free to change it to * addListener to have multiple listeners if you want */ private var listener: RailsConsoleRunner.Listener?) { data class RailsConsoleExecutionResult(val stdout: String, val stderr: String) /** * Extract information left by [rubyCode] in [tempJSONFilPath] * @param clazz which kind of information [rubyCode] left in [tempJSONFilPath]. * Note: Do not use [List] here because [Gson] won't parse it, use [Array] instead * @param tempJSONFilPath path to temp file where [rubyCode] left information which * can be converted from JSON to [clazz] * @param rubyCode Your ruby code which should leave some JSON information in [tempJSONFilPath] * @param eagerLoad Works like you set `eager_load` variable inside config/environments/LOADED_ENVIRONMENT.rb * @param rubyConsoleArguments additional arguments to pass to ruby interpreter * @throws ExecutionException when error occurred either while executing [rubyCode] either * while trying to read data from JSON left by Ruby * @throws IllegalStateException when getter [Project.getBasePath] of [project] returns `null` */ @Throws(ExecutionException::class, IllegalStateException::class) fun extractFromRubyOnRailsConsole(project: Project, sdk: Sdk, clazz: Class, tempJSONFilPath: String, rubyCode: String, eagerLoad: Boolean, rubyConsoleArguments: Array = arrayOf()): T { val projectDirPath = project.basePath ?: throw IllegalStateException("Seems that project is default. " + "Quote from com.intellij.openapi.project.Project#getBasePath JavaDoc") val rubyCodeToExec = if (eagerLoad) { """ Rails.application.eager_load!; nil # nil is to prevent irb to print big output """.trimIndent() + rubyCode } else { rubyCode } val railsConsoleExecutionResult = runRailsConsole(projectDirPath, sdk, rubyCodeToExec, rubyConsoleArguments, railsConsoleArguments = arrayOf("--environment=development")) val ret = ReadAction.compute(ThrowableComputable { val file = File(tempJSONFilPath) return@ThrowableComputable try { file.inputStream().bufferedReader().use { Gson().fromJson(it.readLine(), clazz) } } catch (ex: IOException) { null } }) listener?.informationWasExtractedFromIRB() return ret ?: throw ExecutionException(""" |Error occurred either in the following Ruby code (ruby was launched with these arguments: ${rubyConsoleArguments.contentToString()}): |$rubyCodeToExec |stdout of this Ruby code execution: |${railsConsoleExecutionResult.stdout} |stderr of this Ruby code execution: |${railsConsoleExecutionResult.stderr} |either while trying to read data from JSON left by Ruby """.trimMargin()) } /** * Run [toExec] in ruby on rails console ("bin/rails console"). You can use it for example for generating * some temp json files to later parse them in Kotlin/Java * @param projectDirPath Path to project dir * @param toExec Newline separated [String] to execute in ruby on rails console * @param rubyConsoleArguments additional arguments to pass to ruby interpreter * @param railsConsoleArguments additional arguments to pass to "bin/rails console" * @throws ExecutionException when error occurred while launching rails console */ @Throws(ExecutionException::class) fun runRailsConsole(projectDirPath: String, sdk: Sdk, toExec: String, rubyConsoleArguments: Array = arrayOf(), railsConsoleArguments: Array = arrayOf()): RailsConsoleExecutionResult { val executionMode = ExecutionModes.SameThreadMode(false) executionMode.addProcessListener(object : ProcessListener { override fun processTerminated(event: ProcessEvent) { } override fun processWillTerminate(event: ProcessEvent, willBeDestroyed: Boolean) { } override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { } override fun startNotified(event: ProcessEvent) { PrintWriter(event.processHandler.processInput, true).use { it.println(toExec); it.println("quit") } } }) val processOutput = RubyScriptExecutionContext.create(Paths.get(projectDirPath, Rails4Constants.CONSOLE4_SCRIPT).toString(), sdk) // .withInterpreterOptions(*rubyConsoleArguments) todo API doesn't exist anymore :( // todo replace with .withInterpreterOptions when API becomes available .withAdditionalEnvs(mapOf(RubyUtil.RUBYOPT to rubyConsoleArguments.joinToString(separator = " "))) .withArguments(Rails3Constants.CONSOLE, *railsConsoleArguments) .withExecutionMode(executionMode) .withWorkingDirPath(projectDirPath).executeScript() ?: throw ExecutionException("Error occurred while launching rails console") listener?.irbConsoleExecuted() return RailsConsoleRunner.RailsConsoleExecutionResult( processOutput.stdout, processOutput.stderr ) } /** * [Listener] of particular events in [RailsConsoleRunner]. */ interface Listener { /** * It would be called first */ fun irbConsoleExecuted() /** * It would be called second */ fun informationWasExtractedFromIRB() } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/actions/ExportAncestorsActions.kt ================================================ package org.jetbrains.plugins.ruby.ruby.actions import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import org.jetbrains.plugins.ruby.ancestorsextractor.AncestorsExtractorBase import org.jetbrains.plugins.ruby.ancestorsextractor.AncestorsExtractorByObjectSpace import org.jetbrains.plugins.ruby.ancestorsextractor.AncestorsExtractorByRubyMine import org.jetbrains.plugins.ruby.ancestorsextractor.RubyModule import java.io.PrintWriter /** * Base class representation for exporting Ruby on Rails project's modules' ancestors */ abstract class ExportAncestorsActionBase( whatToExport: String, generateFilename: (Project) -> String, private val extractor: AncestorsExtractorBase ) : ExportFileActionBase(whatToExport, generateFilename, extensions = arrayOf("txt"), numberOfProgressBarFractions = 5) { override fun backgroundProcess(absoluteFilePath: String, module: Module?, sdk: Sdk?, project: Project) { moveProgressBarForward() extractor.listener = ProgressListener() val ancestors: List = try { extractor.extractAncestors(project, sdk ?: throw IllegalStateException("Ruby SDK is not set")) } catch(ex: Throwable) { PrintWriter(absoluteFilePath).use { it.println(ex.message) } return } moveProgressBarForward() PrintWriter(absoluteFilePath).use { printWriter -> ancestors.forEach { printWriter.println("Module: ${it.name}") printWriter.print("Ancestors: ") if (it.ancestors.isEmpty()) printWriter.print("Nothing found") it.ancestors.forEach { printWriter.print("$it ") } printWriter.print("\n\n") } } moveProgressBarForward() } } class ExportAncestorsByObjectSpaceAction : ExportAncestorsActionBase( whatToExport = "ancestors by ObjectSpace", generateFilename = { project -> "ancestors-by-objectspace-${project.name}" }, extractor = AncestorsExtractorByObjectSpace() ) class ExportAncestorsByRubymineAction : ExportAncestorsActionBase( whatToExport = "ancestors by RubyMine", generateFilename = { project -> "ancestors-by-rubymine-${project.name}" }, extractor = AncestorsExtractorByRubyMine() ) ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/actions/ExportAncesttorsDiffAction.kt ================================================ package org.jetbrains.plugins.ruby.ruby.actions import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import org.jetbrains.plugins.ruby.ancestorsextractor.AncestorsExtractorByObjectSpace import org.jetbrains.plugins.ruby.ancestorsextractor.AncestorsExtractorByRubyMine import org.jetbrains.plugins.ruby.ancestorsextractor.RubyModule import java.io.PrintWriter class ExportAncestorsDiffAction : ExportFileActionBase(whatToExport = "ancestors diff", generateFilename = { project: Project -> "ancestors-diff-${project.name}" }, extensions = arrayOf("txt"), numberOfProgressBarFractions = 9) { override fun backgroundProcess(absoluteFilePath: String, module: Module?, sdk: Sdk?, project: Project) { moveProgressBarForward() val byObjectSpace: List val byRubyMine: List val ancestorHashSymbolIncluderToWhereIncluded: Map try { val listener = ProgressListener() val ancestorsExtractorByObjectSpace = AncestorsExtractorByObjectSpace(listener) // Here all listener methods would be called byObjectSpace = ancestorsExtractorByObjectSpace.extractAncestors(project, sdk ?: throw IllegalStateException("Ruby SDK is not set")) // The second place where all listener methods would be called ancestorHashSymbolIncluderToWhereIncluded = ancestorsExtractorByObjectSpace.extractIncludes(project, sdk) // Provide all modulesNames same as in byObjectSpace for easy ancestors comparison val allModulesNames: List = byObjectSpace.map { it.name } // The third place where all listener methods would be called byRubyMine = AncestorsExtractorByRubyMine(allModulesNames, listener) .extractAncestors(project, sdk) } catch (ex: Throwable) { PrintWriter(absoluteFilePath).use { it.println(ex.message) } return } moveProgressBarForward() PrintWriter(absoluteFilePath).use { printWriter -> val objectSpaceIterator = byObjectSpace.iterator() val rubymineIterator = byRubyMine.iterator() while (objectSpaceIterator.hasNext() && rubymineIterator.hasNext()) { val a = objectSpaceIterator.next() val b = rubymineIterator.next() assert(a.name == b.name) printWriter.println("Module: ${a.name}") var same = true a.ancestors.filter { !b.ancestors.contains(it) }.let { if (!it.isEmpty()) { same = false printWriter.print("Ancestors in ObjectSpace only: ") it.forEach { val whereIncluded = ancestorHashSymbolIncluderToWhereIncluded[it + "#" + a.name] var toPrint = it if (whereIncluded != null) { toPrint += "($whereIncluded)" } printWriter.print("$toPrint ") } printWriter.println() } } b.ancestors.filter { !a.ancestors.contains(it) }.let { if (!it.isEmpty()) { same = false printWriter.print("Ancestors in RubyMine only: ") it.forEach { printWriter.print("$it ") } printWriter.println() } } if (same) { printWriter.println("No difference in ancestors list") } printWriter.println() } } moveProgressBarForward() } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/actions/ExportFileActionBase.kt ================================================ package org.jetbrains.plugins.ruby.ruby.actions import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.fileChooser.FileChooserDescriptor import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.fileChooser.FileSaverDescriptor import com.intellij.openapi.fileChooser.ex.FileChooserDialogImpl import com.intellij.openapi.fileChooser.ex.FileSaverDialogImpl import com.intellij.openapi.module.Module import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.util.ProgressWindow import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.ThrowableComputable import org.jetbrains.plugins.ruby.ancestorsextractor.AncestorsExtractorBase import org.jetbrains.plugins.ruby.ancestorsextractor.RailsConsoleRunner import org.jetbrains.plugins.ruby.ruby.RModuleUtil /** * Base class representing file export action with "save to" dialog * @param whatToExport Will be shown in "save to" dialog * @param generateFilename Generate filename for "save to" dialog * @param extensions Array of available extensions for exported file * @param description Description in "save to" dialog */ abstract class ExportFileActionBase( private val whatToExport: String, private val generateFilename: (Project) -> String, private val extensions: Array, private val description: String = "", private val numberOfProgressBarFractions: Int? = null ) : DumbAwareAction() { final override fun actionPerformed(e: AnActionEvent) { val project = e.project ?: return val dialog = FileSaverDialogImpl(FileSaverDescriptor( "Export $whatToExport", description, *extensions), project) val fileWrapper = dialog.save(null, generateFilename(project)) ?: return val module: Module? = RModuleUtil.getInstance().getModule(e.dataContext) val sdk: Sdk? = RModuleUtil.getInstance().findRubySdkForModule(module) try { ProgressManager.getInstance().runProcessWithProgressSynchronously(ThrowableComputable { return@ThrowableComputable backgroundProcess(fileWrapper.file.absolutePath, module, sdk, project) }, "Exporting $whatToExport", false, project) } catch (ex: Exception) { Messages.showErrorDialog(ex.message, "Error while exporting $whatToExport") } } /** * In this method implementation you can do you job needed for file export and then file exporting itself. * * @param absoluteFilePath absolute file path which user have chosen to save file to. * @param module module from the context of action it invoked * @param sdk sdk from the context of action it invoked * @param project project from the context of action it invoked */ protected abstract fun backgroundProcess(absoluteFilePath: String, module: Module?, sdk: Sdk?, project: Project) @Throws(IllegalStateException::class) protected fun moveProgressBarForward() { if (numberOfProgressBarFractions == null) throw IllegalStateException("You cannot call moveProgressBarForward() " + "method when progressBarFractions property is null") val progressIndicator = ProgressManager.getInstance().progressIndicator if (progressIndicator is ProgressWindow) { progressIndicator.fraction = minOf(1.0, progressIndicator.fraction + 1.0/numberOfProgressBarFractions) } } /** * You can use to set as [AncestorsExtractorBase.listener] because every [ProgressListener] * method call just calls [moveProgressBarForward] */ protected inner class ProgressListener : RailsConsoleRunner.Listener { override fun irbConsoleExecuted() { moveProgressBarForward() } override fun informationWasExtractedFromIRB() { moveProgressBarForward() } } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/actions/ImportExportContractsAction.kt ================================================ package org.jetbrains.plugins.ruby.ruby.actions import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.fileChooser.FileChooserDescriptor import com.intellij.openapi.fileChooser.ex.FileChooserDialogImpl import com.intellij.openapi.module.Module import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.ThrowableComputable import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.plugins.ruby.ruby.codeInsight.types.resetAllRubyTypeProviderAndIDEACaches import org.jetbrains.ruby.codeInsight.types.signature.CallInfo import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoRow import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoTable import java.io.File const val CHUNK_SIZE = 1500 fun Database.copyTo(destination: Database, moveProgressBar: Boolean) { var progressIndicator: ProgressIndicator? = null var count: Int? = null if (moveProgressBar) { progressIndicator = ProgressManager.getInstance().progressIndicator count = transaction(this) { CallInfoTable.selectAll().count() } } var offset = 0 while (true) { val info: List = transaction(this) { CallInfoRow.wrapRows(CallInfoTable.selectAll().limit(CHUNK_SIZE, offset)).map { it.copy() } } if (info.isEmpty()) { break } transaction(destination) { info.forEach { CallInfoTable.insertInfoIfNotContains(it) } } offset += CHUNK_SIZE if (moveProgressBar) { progressIndicator!!.fraction = offset.toDouble() / count!! } } } class ExportContractsAction : ExportFileActionBase( whatToExport = "Type contracts", generateFilename = { project: Project -> "${project.name}-type-contracts" }, extensions = arrayOf("mv.db") ) { override fun backgroundProcess(absoluteFilePath: String, module: Module?, sdk: Sdk?, project: Project) { exportContractsToFile(absoluteFilePath, moveProgressBar = true) } companion object { fun exportContractsToFile(pathToExport: String, moveProgressBar: Boolean) { check(pathToExport.endsWith(DatabaseProvider.H2_DB_FILE_EXTENSION)) { "Path to export must end with .mv.db" } File(pathToExport).delete() val databaseToExportTo = DatabaseProvider.connectToDB(pathToExport) DatabaseProvider.defaultDatabase!!.copyTo(databaseToExportTo, moveProgressBar) } } } class ImportContractsAction : DumbAwareAction() { override fun actionPerformed(e: AnActionEvent) { val project = e.project val files = FileChooserDialogImpl( FileChooserDescriptor(true, false, false, false, false, false), project).choose(project) if (files.isEmpty()) { return } try { ProgressManager.getInstance().runProcessWithProgressSynchronously(ThrowableComputable { files.forEach { importContractsFromFile(it.path, moveProgressBar = true) } return@ThrowableComputable }, "Importing type contracts", false, project) resetAllRubyTypeProviderAndIDEACaches(project) } catch (ex: Exception) { Messages.showErrorDialog(ex.message, "Error while importing type contracts") } } companion object { fun importContractsFromFile(pathToImportFrom: String, moveProgressBar: Boolean) { check(pathToImportFrom.endsWith(DatabaseProvider.H2_DB_FILE_EXTENSION)) { "Path to import from must end with .mv.db" } val dbToImportFrom = DatabaseProvider.connectToDB(pathToImportFrom) dbToImportFrom.copyTo(DatabaseProvider.defaultDatabase!!, moveProgressBar) } } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/ProjectLifecycleListenerImpl.kt ================================================ package org.jetbrains.plugins.ruby.ruby.codeInsight import com.google.gson.Gson import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManagerListener import org.jetbrains.plugins.ruby.ruby.persistent.TypeInferenceDirectory import org.jetbrains.plugins.ruby.util.runServerAsyncInIDEACompatibleMode import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.runtime.signature.server.SignatureServer import java.io.File import java.io.PrintWriter import java.nio.file.Paths /** * Short [Project] description for `rubymine-type-tracer` */ data class ProjectDescription(val projectName: String, val projectPath: String, val pipeFilePath: String) { /** * @param project default projects are not allowed! */ constructor(project: Project, pipeFilePath: String) : this(project.name, project.basePath!!, pipeFilePath) } /** * This directory is needed for `rubymine-type-tracker` script * * In this directory we keep files named the same as currently opened projects in RubyMine. * Each file contains projectPath of pipe file required for arg-scanner. */ private val openedProjectsDir = File(System.getProperty("java.io.tmpdir")!!).resolve(".ruby-type-inference") .also { it.mkdirs() } /** * This registered in `plugin.xml` and it's constructor called every time RubyMine starts */ class ProjectLifecycleListenerImpl : ProjectManagerListener { private val gson = Gson() private companion object { @Volatile private var initialized: Boolean = false } override fun projectOpened(project: Project) { if (!project.isDefault) { connectToDB(project.name) // This server is used for `rubymine-type-tracker` script startNewBackgroundInfinityServer(project) } } override fun projectClosed(project: Project) { if (!project.isDefault) { val projectDescription = readProjectDescription(project, deleteJsonAfterRead = true) File(projectDescription.pipeFilePath).delete() } } private fun connectToDB(projectName: String) { val filePath = Paths.get( TypeInferenceDirectory.RUBY_TYPE_INFERENCE_DIRECTORY.toString(), projectName).toString() + DatabaseProvider.H2_DB_FILE_EXTENSION DatabaseProvider.connectToDB(filePath, isDefaultDatabase = true) } /** * Starts server for `rubymine-type-tracker` script */ private fun startNewBackgroundInfinityServer(project: Project): Boolean { if (project.isDefault) { return false } val server = SignatureServer() val pipeFilePath: String = server.runServerAsyncInIDEACompatibleMode(project) writeProjectDescription(ProjectDescription(project, pipeFilePath)) server.afterExitListener = { startNewBackgroundInfinityServer(project) } return true } private fun writeProjectDescription(description: ProjectDescription) { val jsonFile: File = openedProjectsDir.resolve(description.projectName) PrintWriter(jsonFile).use { it.println(gson.toJson(description)) } } private fun readProjectDescription(project: Project, deleteJsonAfterRead: Boolean = false): ProjectDescription { val jsonFile: File = openedProjectsDir.resolve(project.name) val json: String = jsonFile.bufferedReader().use { it.readText() } val description = gson.fromJson(json, ProjectDescription::class.java)!! if (deleteJsonAfterRead) { jsonFile.delete() } return description } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/RubyDynamicCodeInsightPluginAppLifecyctlListener.kt ================================================ package org.jetbrains.plugins.ruby.ruby.codeInsight import com.intellij.ide.AppLifecycleListener import com.intellij.openapi.project.Project import org.jetbrains.plugins.ruby.RubyDynamicCodeInsightPluginInjector import org.jetbrains.ruby.codeInsight.initInjector class RubyDynamicCodeInsightPluginAppLifecyctlListener : AppLifecycleListener { override fun appStarting(projectFromCommandLine: Project?) { initInjector(RubyDynamicCodeInsightPluginInjector) } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/TrackerDataLoader.kt ================================================ package org.jetbrains.plugins.ruby.ruby.codeInsight import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.startup.StartupActivity import org.jetbrains.plugins.ruby.ruby.codeInsight.stateTracker.RubyClassHierarchyWithCaching class TrackerDataLoader : StartupActivity, DumbAware { override fun runActivity(project: Project) { ModuleManager.getInstance(project).modules.forEach { RubyClassHierarchyWithCaching.loadFromSystemDirectory(it) } } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/stateTracker/ClassHierarchySymbolProvider.kt ================================================ package org.jetbrains.plugins.ruby.ruby.codeInsight.stateTracker import com.intellij.openapi.module.ModuleUtilCore import com.intellij.psi.PsiElement import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.RubySymbolProviderBase import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.fqn.FQN import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.structure.Symbol import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.v2.SymbolPsiProcessor import org.jetbrains.plugins.ruby.ruby.lang.psi.RPsiElement class ClassHierarchySymbolProvider : RubySymbolProviderBase() { override fun processDynamicSymbols(symbol: Symbol, element: RPsiElement?, fqn: FQN, processor: SymbolPsiProcessor, invocationPoint: PsiElement?): Boolean { if (element == null) { return true } val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return true val hierarchy = RubyClassHierarchyWithCaching.getInstance(module)?: return true hierarchy.getMembersWithCaching(fqn.fullPath, symbol.rootSymbol).forEach { if (!processor.process(it)) { return false } } return true } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/stateTracker/RubyClassHierarchyWithCaching.kt ================================================ package org.jetbrains.plugins.ruby.ruby.codeInsight.stateTracker import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.module.Module import com.intellij.openapi.util.Key import com.intellij.util.containers.ContainerUtil import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.Type import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.Types import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.structure.RMethodSyntheticSymbol import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.structure.Symbol import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.structure.SymbolUtil import org.jetbrains.plugins.ruby.ruby.persistent.TypeInferenceDirectory import org.jetbrains.plugins.ruby.settings.RubyTypeContractsSettings import org.jetbrains.ruby.stateTracker.* import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream class RubyClassHierarchyWithCaching private constructor(private val rubyClassHierachy: RubyClassHierarchy) { private val lookupCache = ContainerUtil.createConcurrentWeakMap, RubyMethod>() private val membersCache = ContainerUtil.createConcurrentWeakMap>() fun getTypeForConstant(constant: String): RubyConstant? { return rubyClassHierachy.topLevelConstants[constant] } fun getMembersWithCaching(moduleName: String, topLevel: Symbol) : Set { val module = rubyClassHierachy.getRubyModule(moduleName) ?: return emptySet() return getMembersWithCaching(module, topLevel) } private fun lookupInstanceMethodWithCaching(module: RubyModule, methodName: String) : RubyMethod? { return lookupCache.computeIfAbsent(Pair(module.name, methodName)) { lookupInstanceMethod(module, methodName) } } private fun lookupInstanceMethod(module: RubyModule, methodName: String): RubyMethod? { val ownResult = module.instanceMethods.firstOrNull {it.name == methodName} if (ownResult != null) { return ownResult } module.instanceDirectAncestors.forEach { val result = lookupInstanceMethodWithCaching(it, methodName) if (result != null) { return result } } if (module is RubyClass && module.superClass != RubyClass.EMPTY) { val result = lookupInstanceMethodWithCaching(module.superClass, methodName) if (result != null) { return result } } return null } private fun getMembersWithCaching(module: RubyModule, topLevel: Symbol) : Set { return membersCache.computeIfAbsent(module.name) { getMembers(module, topLevel) } } private fun getMembers(module: RubyModule, topLevel: Symbol) : Set { val set = HashSet() val symbol = SymbolUtil.findSymbolInHierarchy(topLevel, module.name, Types.MODULE_OR_CLASS, topLevel.psiElement) set.addAll(module.instanceMethods.map { RMethodSyntheticSymbol(topLevel.project, Type.INSTANCE_METHOD, it, symbol) }) set.addAll(module.classMethods.map { RMethodSyntheticSymbol(topLevel.project, Type.CLASS_METHOD, it, symbol) }) module.instanceDirectAncestors.forEach { set.addAll(getMembersWithCaching(it, topLevel))} module.classDirectAncestors.forEach{ set.addAll(getMembersWithCaching(it, topLevel)) } if (module is RubyClass && module.superClass != RubyClass.EMPTY) { set.addAll(getMembersWithCaching(module.superClass, topLevel)) } return set } companion object { private val CLASS_HIERARCHY_KEY = Key("org.jetbrains.plugins.ruby.ruby.codeInsight.stateTracker.ClassHierarchy") private val CLASS_HIERARCHY_FILENAME = "-class-hierarchy.json.gz" fun loadFromSystemDirectory(module: Module): RubyClassHierarchyWithCaching? { val file = File(TypeInferenceDirectory.RUBY_TYPE_INFERENCE_DIRECTORY.toFile(), module.project.name + "-" + module.name + CLASS_HIERARCHY_FILENAME) if (!file.exists()) { return null } FileInputStream(file).use { GZIPInputStream(it).use { val json = it.reader(Charsets.UTF_8).use{ it.readText() } return createClassHierarchyFromJson(json, module) } } } @Synchronized fun updateAndSaveToSystemDirectory(jsons: List, module: Module) { val json = RubyClassHierarchyLoader.mergeJsons(jsons) createClassHierarchyFromJson(json, module) FileOutputStream(File(TypeInferenceDirectory.RUBY_TYPE_INFERENCE_DIRECTORY.toFile(), module.project.name + "-" + module.name + CLASS_HIERARCHY_FILENAME)).use { GZIPOutputStream(it).use { it.writer(Charsets.UTF_8).use { it.write(json) } } } } private fun createClassHierarchyFromJson(json: String, module: Module) : RubyClassHierarchyWithCaching { val rubyClassHierarchy = RubyClassHierarchyWithCaching(RubyClassHierarchyLoader.fromJson(json)) module.putUserData(RubyClassHierarchyWithCaching.CLASS_HIERARCHY_KEY, rubyClassHierarchy) return rubyClassHierarchy } fun getInstance(module: Module): RubyClassHierarchyWithCaching? { if (!ServiceManager.getService(module.project, RubyTypeContractsSettings::class.java).stateTrackerEnabled) { return null } val ret = module.getUserData(CLASS_HIERARCHY_KEY) if (ret != null) { return ret } return null } } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/symbols/structure/RMethodSyntheticSymbol.java ================================================ package org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.structure; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.editor.Document; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.util.CachedValueProvider; import com.intellij.psi.util.CachedValuesManager; import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.ruby.rdoc.yard.psi.RangeInDocumentFakePsiElement; import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.Type; import org.jetbrains.plugins.ruby.ruby.codeInsight.types.RType; import org.jetbrains.plugins.ruby.ruby.lang.psi.RPsiElement; import org.jetbrains.plugins.ruby.ruby.lang.psi.controlStructures.methods.ArgumentInfo; import org.jetbrains.plugins.ruby.ruby.lang.psi.controlStructures.methods.Visibility; import org.jetbrains.plugins.ruby.ruby.lang.psi.impl.controlStructures.methods.RCommandArgumentListImpl; import org.jetbrains.plugins.ruby.ruby.lang.psi.methodCall.RCall; import org.jetbrains.ruby.stateTracker.Location; import org.jetbrains.ruby.stateTracker.RubyMethod; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; public class RMethodSyntheticSymbol extends SymbolImpl implements RMethodSymbol { @NotNull private final Visibility myVisibility; @NotNull private final List myArgsInfo; @Nullable private final String myPath; private final int myLineno; public RMethodSyntheticSymbol(@NotNull final Project project, @NotNull final Type type, @NotNull final RubyMethod rubyMethod, @Nullable final Symbol parent) { super(project, rubyMethod.getName(), type, parent); myVisibility = Visibility.PUBLIC; myArgsInfo = toArgsInfo(rubyMethod.getArguments()); final Location location = rubyMethod.getLocation(); if (location != null) { myPath = location.getPath(); myLineno = location.getLineNo(); } else { myPath = ""; myLineno = 0; } } private List toArgsInfo(List arguments) { return arguments.stream().map(RMethodSyntheticSymbol::toArgumentInfo).collect(Collectors.toList()); } private static ArgumentInfo toArgumentInfo(final @NotNull RubyMethod.ArgInfo argInfo) { ArgumentInfo.Type type; switch (argInfo.getKind()) { case REQ: type = ArgumentInfo.Type.SIMPLE; break; case OPT: type = ArgumentInfo.Type.PREDEFINED; break; case REST: type = ArgumentInfo.Type.ARRAY; break; case KEY: type = ArgumentInfo.Type.NAMED; break; case KEY_REST: type = ArgumentInfo.Type.HASH; break; case KEY_REQ: type = ArgumentInfo.Type.KEYREQ; break; case BLOCK: type = ArgumentInfo.Type.BLOCK; break; default: throw new IllegalArgumentException(argInfo.getKind().toString()); } return new ArgumentInfo(argInfo.getName(), type); } @NotNull @Override public String getName() { //noinspection ConstantConditions return super.getName(); } @Override public @Nullable List getArgumentInfos() { return myArgsInfo; } @Override @Nullable public List getArgumentInfos(boolean includeDefaultArgs) { return null; } @Nullable @Override public RType getCallType(@Nullable final RCall call) { return null; } @NotNull @Override public String getArgsPresentation() { if (myArgsInfo.isEmpty()) { return ""; } else { return "(" + RCommandArgumentListImpl.getPresentableName(myArgsInfo) + ")"; } } @Override public boolean isSynthetic() { return false; } @NotNull @Override public Visibility getVisibility() { return myVisibility; } @Override public PsiElement getPsiElement() { if (myPath == null) { return null; } final VirtualFile virtualFile = VirtualFileManager.getInstance().findFileByUrl(VfsUtilCore.pathToUrl(myPath)); if (virtualFile == null) { return null; } final PsiFile file = PsiManager.getInstance(getProject()).findFile(virtualFile); if (file == null) { return null; } return CachedValuesManager.getCachedValue(file, () -> CachedValueProvider.Result.create(calcElement(file), file)); } @NotNull @Override public Collection getAllDeclarations(PsiElement invocationPoint) { final PsiElement psiElement = getPsiElement(); return psiElement == null ? Collections.emptyList() : Collections.singletonList(psiElement); } @Nullable private PsiElement calcElement(@NotNull PsiFile file) { final Document document = PsiDocumentManager.getInstance(getProject()).getDocument(file); if (document == null) { return null; } return ReadAction.compute(() -> { int offset = document.getLineStartOffset(myLineno); int nextLineOffset = document.getLineEndOffset(myLineno); int curOffset = offset; PsiElement psiElement; do { psiElement = file.findElementAt(curOffset); if (psiElement == null) { return null; } curOffset = psiElement.getTextRange().getEndOffset(); } while (!(psiElement instanceof RPsiElement) && curOffset < nextLineOffset); if (psiElement instanceof RPsiElement) { return psiElement; } psiElement = file.findElementAt(offset); if (psiElement == null) { return null; } final int startElementOffset = psiElement.getTextRange().getStartOffset(); final int endElementOffset = psiElement.getTextRange().getEndOffset(); int start = offset - startElementOffset; int end = Math.min(nextLineOffset - startElementOffset, endElementOffset - startElementOffset); return new MyFakeElement(psiElement, new TextRange(start, end), getName()); }); } private static class MyFakeElement extends RangeInDocumentFakePsiElement { @NotNull private final String myName; MyFakeElement(@NotNull PsiElement parent, @NotNull TextRange rangeInParent, @NotNull String name) { super(parent, rangeInParent); myName = name; } @NotNull @Override public String getName() { return myName; } @Override public PsiElement setName(@NotNull String name) throws IncorrectOperationException { throw new IncorrectOperationException("not supported"); } } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/types/RubyCollectStateRunner.kt ================================================ package org.jetbrains.plugins.ruby.ruby.codeInsight.types import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.RunProfile import com.intellij.execution.configurations.RunProfileState import com.intellij.execution.executors.CollectStateExecutor import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.ui.RunContentDescriptor import com.intellij.openapi.util.io.FileUtil import org.jetbrains.plugins.ruby.ruby.run.configuration.AbstractRubyRunConfiguration import org.jetbrains.plugins.ruby.ruby.run.configuration.CollectExecSettings import org.jetbrains.plugins.ruby.ruby.run.configuration.RubyAbstractCommandLineState import org.jetbrains.plugins.ruby.ruby.run.configuration.RubyProgramRunner import java.io.IOException class RubyCollectStateRunner : RubyProgramRunner() { override fun canRun(executorId: String, profile: RunProfile): Boolean { return executorId == CollectStateExecutor.EXECUTOR_ID && profile is AbstractRubyRunConfiguration<*> } @Throws(ExecutionException::class) override fun doExecute(state: RunProfileState, environment: ExecutionEnvironment): RunContentDescriptor? { if (state is RubyAbstractCommandLineState) { val newConfig = state.config.clone() val pathToState = tryGenerateTmpDirPath() CollectExecSettings.putTo(newConfig, CollectExecSettings.createSettings(true, false, true, pathToState )) val newState = newConfig.getState(environment.executor, environment) if (newState != null) { return super.doExecute(newState, environment) } } return null } private fun tryGenerateTmpDirPath(): String? { try { val tmpDir = FileUtil.createTempDirectory("state-tracker", "") return tmpDir.absolutePath } catch (ignored: IOException) { return null } } override fun getRunnerId(): String { return RUBY_COLLECT_STATE_RUNNER_ID } companion object { private val RUBY_COLLECT_STATE_RUNNER_ID = "RubyCollectState" } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/types/RubyRunWithTypeTrackerRunner.kt ================================================ package org.jetbrains.plugins.ruby.ruby.codeInsight.types import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.RunProfile import com.intellij.execution.configurations.RunProfileState import com.intellij.execution.executors.RunWithTypeTrackerExecutor import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.ui.RunContentDescriptor import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.util.io.FileUtil import org.jetbrains.plugins.ruby.ruby.run.configuration.AbstractRubyRunConfiguration import org.jetbrains.plugins.ruby.ruby.run.configuration.CollectExecSettings import org.jetbrains.plugins.ruby.ruby.run.configuration.RubyAbstractCommandLineState import org.jetbrains.plugins.ruby.ruby.run.configuration.RubyProgramRunner import org.jetbrains.plugins.ruby.settings.RubyTypeContractsSettings import java.io.IOException class RubyRunWithTypeTrackerRunner : RubyProgramRunner() { @Throws(ExecutionException::class) override fun doExecute(state: RunProfileState, environment: ExecutionEnvironment): RunContentDescriptor? { if (state is RubyAbstractCommandLineState) { val (_, _, typeTrackerEnabled) = ServiceManager.getService(environment.project, RubyTypeContractsSettings::class.java) val newConfig = state.config.clone() val pathToState = tryGenerateTmpDirPath() CollectExecSettings.putTo(newConfig, CollectExecSettings.createSettings(true, typeTrackerEnabled, false, pathToState )) val newState = newConfig.getState(environment.executor, environment) if (newState != null) { return super.doExecute(newState, environment) } } return null } override fun preloaderAllowed(): Boolean = false private fun tryGenerateTmpDirPath(): String? = try { val tmpDir = FileUtil.createTempDirectory("type-tracker", "") tmpDir.absolutePath } catch (ignored: IOException) { null } override fun canRun(executorId: String, profile: RunProfile): Boolean { return executorId == RunWithTypeTrackerExecutor.EXECUTOR_ID && profile is AbstractRubyRunConfiguration<*> } override fun getRunnerId(): String { return RUBY_COLLECT_TYPE_RUNNER_ID } companion object { private val RUBY_COLLECT_TYPE_RUNNER_ID = "RubyCollectType" } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/codeInsight/types/RubyTypeProvider.kt ================================================ package org.jetbrains.plugins.ruby.ruby.codeInsight.types import com.intellij.openapi.application.ReadAction import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.module.ModuleUtilCore import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project import com.intellij.util.containers.ContainerUtil import org.jetbrains.plugins.ruby.ruby.codeInsight.AbstractRubyTypeProvider import org.jetbrains.plugins.ruby.ruby.codeInsight.IncomingType import org.jetbrains.plugins.ruby.ruby.codeInsight.resolve.ResolveUtil import org.jetbrains.plugins.ruby.ruby.codeInsight.stateTracker.RubyClassHierarchyWithCaching import org.jetbrains.plugins.ruby.ruby.codeInsight.symbolicExecution.SymbolicExecutionContext import org.jetbrains.plugins.ruby.ruby.codeInsight.symbolicExecution.SymbolicExpressionProvider import org.jetbrains.plugins.ruby.ruby.codeInsight.symbolicExecution.SymbolicTypeInferenceProvider import org.jetbrains.plugins.ruby.ruby.codeInsight.symbolicExecution.TypeInferenceComponent import org.jetbrains.plugins.ruby.ruby.codeInsight.symbolicExecution.instance.TypeInferenceInstance import org.jetbrains.plugins.ruby.ruby.codeInsight.symbolicExecution.symbolicExpression.SymbolicCall import org.jetbrains.plugins.ruby.ruby.codeInsight.symbolicExecution.symbolicExpression.SymbolicExpression import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.Type import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.structure.Symbol import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.structure.util.SymbolHierarchy import org.jetbrains.plugins.ruby.ruby.codeInsight.types.impl.REmptyType import org.jetbrains.plugins.ruby.ruby.lang.psi.RPsiElement import org.jetbrains.plugins.ruby.ruby.lang.psi.RubyPsiUtil import org.jetbrains.plugins.ruby.ruby.lang.psi.expressions.RExpression import org.jetbrains.plugins.ruby.ruby.lang.psi.variables.RIdentifier import org.jetbrains.ruby.codeInsight.types.signature.* import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoTable import org.jetbrains.ruby.codeInsight.types.storage.server.impl.RSignatureProviderImpl import java.util.concurrent.Executors import java.util.concurrent.Future import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException /** * Cache where we store last accessed [CallInfo]s. Thread safe */ private val registeredCallInfosCache: MutableMap> = ContainerUtil.createConcurrentSoftKeySoftValueMap>() fun resetAllRubyTypeProviderAndIDEACaches(project: Project?) { registeredCallInfosCache.clear() // Clears IDEAs caches about inferred types ServiceManager.getService(project ?: return, TypeInferenceContext::class.java)?.clear() } fun getCachedOrComputedRegisteredCallInfo(methodInfo: MethodInfo): List { return registeredCallInfosCache.getOrPut(methodInfo) { RSignatureProviderImpl.getRegisteredCallInfos(methodInfo) } } class RubyParameterTypeProvider : AbstractRubyTypeProvider() { override fun createTypeBySymbol(symbol: Symbol): RType? { return null } override fun createTypeByRExpression(expr: RExpression): RType? { val symbol = ResolveUtil.resolveToSymbolWithCaching(expr.reference, false) if (symbol?.type == Type.CONSTANT) { val module = ModuleUtilCore.findModuleForPsiElement(expr) ?: return null val classHierarchyWithCaching = RubyClassHierarchyWithCaching.getInstance(module) ?: return null val path = symbol?.fqnWithNesting?.fullPath ?: return null val constant = classHierarchyWithCaching.getTypeForConstant(path) ?: return null val originType = RTypeFactory.createTypeByFQN(expr.project, constant.type) val mixins = constant.extended.map { RTypeFactory.createTypeByFQN(expr.project, it) } if (mixins.isNotEmpty()) { return RTypeUtil.union(originType, mixins.reduce { acc, it -> RTypeUtil.union(acc, it) }) } return originType } if (expr is RIdentifier && expr.isParameter) { val method = RubyPsiUtil.getContainingRMethod(expr) ?: return null val rubyModuleName = RubyPsiUtil.getContainingRClassOrModule(method)?.fqn?.fullPath ?: "Object" val info = MethodInfo.Impl(ClassInfo(rubyModuleName), method.fqn.shortName, RVisibility.PUBLIC) val callInfos: List = getCachedOrComputedRegisteredCallInfo(info) val returnType = callInfos.map { callInfo -> val typeName = expr.name?.let { callInfo.getTypeNameByArgumentName(it) } ?: return@map REmptyType.INSTANCE return@map RTypeFactory.createTypeClassName(typeName, expr) }.unionTypesSmart() return if (returnType == REmptyType.INSTANCE) { // If we don't have any information about type then return null // in order to allow to RubyMine try to determine type itself null } else { returnType } } return null } } /** * Provides types for Ruby method return values. Type providing based on information collection into [CallInfoTable] */ class ReturnTypeSymbolicTypeInferenceProvider : SymbolicTypeInferenceProvider { companion object { private val pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1) } override fun evaluateSymbolicCall(symbolicCall: SymbolicCall, context: SymbolicExecutionContext, callContext: TypeInferenceInstance.CallContext, provider: SymbolicExpressionProvider, component: TypeInferenceComponent): SymbolicExpression? { ProgressManager.checkCanceled() val job: () -> SymbolicExpression? = { evaluateSymbolicCallPoolJob(symbolicCall, context, callContext, component) } val future: Future = pool.submit(job) return try { // This method is run under read action and we cannot afford to spend a lot of time determining time. // Otherwise we got glitches see: https://youtrack.jetbrains.com/issue/RUBY-25433 future.get(100, TimeUnit.MILLISECONDS) } catch (ex: TimeoutException) { null } } private fun evaluateSymbolicCallPoolJob(symbolicCall: SymbolicCall, context: SymbolicExecutionContext, callContext: TypeInferenceInstance.CallContext, component: TypeInferenceComponent): SymbolicExpression? { var unnamedArgsTypes: List = emptyList() var namedArgsTypes: List = emptyList() var receiverTypesConsideringAncestors: List = emptyList() ReadAction.run { ProgressManager.checkCanceled() val exactReceiverType: RType = SymbolicTypeInferenceProvider.getReceiverType(symbolicCall, component, callContext) // reversed because getAncestorsCaching gives us list of ancestors ordered from parent to end children // This list already include exactReceiverType receiverTypesConsideringAncestors = RTypeUtil.getBirthTypeSymbol(exactReceiverType) ?.let { SymbolHierarchy.getAncestorsCaching(it, callContext.invocationPoint) } ?.map { it.symbol.fqnWithNesting.toString() }?.reversed() ?: emptyList() val typeInferenceComponent = context.getComponent(TypeInferenceComponent::class.java) unnamedArgsTypes = symbolicCall.arguments.asSequence().filter { it.type != IncomingType.ASSOC } .map { typeInferenceComponent.getTypeForSymbolicExpression(it.expression).name }.toList() namedArgsTypes = symbolicCall.arguments.asSequence().filter { it.type == IncomingType.ASSOC } .map { val type = typeInferenceComponent.getTypeForSymbolicExpression(it.expression).name ?: return@map null return@map ArgumentNameAndType(it.keyName, type) }.toList() } for (receiverTypeName in receiverTypesConsideringAncestors) { val methodInfo = MethodInfo.Impl(ClassInfo.Impl(null, receiverTypeName), symbolicCall.name) val registeredCallInfos = getCachedOrComputedRegisteredCallInfo(methodInfo) val registeredReturnTypes: List = registeredCallInfos .asSequence() .filter { argumentsMatch(it, unnamedArgsTypes, namedArgsTypes) } .map { it.returnType } .toList() .takeIf { !it.isEmpty() } ?: registeredCallInfos.map { it.returnType } val returnType = registeredReturnTypes .mapNotNull { RTypeFactory.createTypeClassName(it, callContext.invocationPoint as? RPsiElement ?: return@mapNotNull null) } .unionTypesSmart() if (returnType != REmptyType.INSTANCE) { component.updateSymbolicExpressionType(symbolicCall, returnType) return symbolicCall } } // If we don't have any information about type then return null // in order to allow to RubyMine try to determine type itself return null } private fun argumentsMatch(oneOfExpected: CallInfo, actualUnnamedArgs: List, actualNamedArgs: List): Boolean { if (oneOfExpected.unnamedArguments.size != actualUnnamedArgs.size || oneOfExpected.namedArguments.size != actualNamedArgs.size) { return false } if (oneOfExpected.unnamedArguments.zip(actualUnnamedArgs).any { it.first.type != ArgumentNameAndType.IMPLICITLY_PASSED_ARGUMENT_TYPE && it.second != null && it.first.type != it.second }) { return false } if (oneOfExpected.namedArguments.zip(actualNamedArgs).any { it.second != null && it.first.type != ArgumentNameAndType.IMPLICITLY_PASSED_ARGUMENT_TYPE && it.second!!.type != ArgumentNameAndType.IMPLICITLY_PASSED_ARGUMENT_TYPE && it.first.type != it.second!!.type }) { return false } return true } } /** * The same as [unionTypes] but also get rid of [REmptyType] and duplicates */ private fun List.unionTypesSmart(): RType = filter { it != REmptyType.INSTANCE }.distinct().unionTypes() private fun List.unionTypes(): RType { if (size == 0) { return REmptyType.INSTANCE } if (size == 1) { return first() } val m = size / 2 return RTypeUtil.union(subList(0, m).unionTypes(), subList(m, size).unionTypes()) } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/intentions/AddContractAnnotationIntention.java ================================================ package org.jetbrains.plugins.ruby.ruby.intentions; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.exposed.dao.EntityID; import org.jetbrains.plugins.ruby.ruby.codeInsight.symbols.fqn.FQN; import org.jetbrains.plugins.ruby.ruby.lang.psi.RFile; import org.jetbrains.plugins.ruby.ruby.lang.psi.RubyElementFactory; import org.jetbrains.plugins.ruby.ruby.lang.psi.RubyPsiUtil; import org.jetbrains.plugins.ruby.ruby.lang.psi.controlStructures.methods.RMethod; import org.jetbrains.plugins.ruby.ruby.lang.psi.variables.RFName; import org.jetbrains.ruby.codeInsight.types.signature.*; import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.ContractTransition; import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.ReferenceContractTransition; import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.TypedContractTransition; import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider; import org.jetbrains.ruby.codeInsight.types.storage.server.impl.MethodInfoTable; import org.jetbrains.ruby.codeInsight.types.storage.server.impl.RSignatureProviderImpl; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class AddContractAnnotationIntention extends BaseRubyMethodIntentionAction { @Nls(capitalization = Nls.Capitalization.Sentence) @NotNull @Override public String getFamilyName() { return getText(); } public boolean isAvailable(@NotNull final Project project, final @NotNull Editor editor, @NotNull final PsiFile file) { if (!super.isAvailable(project, editor, file)) { return false; } if (!file.isWritable()) { return false; } RFName rfName = getRFName(editor, file); if (rfName == null) { return false; } RMethod method = RubyPsiUtil.getContainingRMethod(rfName); if (method == null) { return false; } MethodInfo methodInfo = createMethodInfo(method); EntityID found = methodInfo != null ? DatabaseProvider.defaultDatabaseTransaction( transaction -> MethodInfoTable.INSTANCE.findRowId(methodInfo)) : null; return found != null; } public void invoke(@NotNull final Project project, final Editor editor, final PsiFile file) throws IncorrectOperationException { PsiDocumentManager.getInstance(project).commitAllDocuments(); final PsiElement element = file.findElementAt(editor.getCaretModel().getOffset()); final RMethod method = PsiTreeUtil.getParentOfType(element, RMethod.class); assert method != null : "Method cannot be null here"; final MethodInfo methodInfo = createMethodInfo(method); assert methodInfo != null : "MethodInfo mustn't be null: it was checked in isAvailable"; List registeredCallInfos = RSignatureProviderImpl.INSTANCE.getRegisteredCallInfos(methodInfo); StringBuilder builder = new StringBuilder(); builder.append("# @contract"); for (CallInfo callInfo : registeredCallInfos) { builder.append("\n# ("); builder.append(String.join(", ", callInfo.getNamedArguments().stream().map(ArgumentNameAndType::getType).toArray(String[]::new))); builder.append(") -> "); builder.append(callInfo.getReturnType()); } final RFile fileWithComments = RubyElementFactory.createRubyFile(project, builder.toString()); method.getParent().addRangeBefore(fileWithComments.getFirstChild(), fileWithComments.getLastChild(), method); } @Nullable private static MethodInfo createMethodInfo(@NotNull RMethod method) { final FQN fqnWithNesting = method.getFQNWithNesting(); final String methodName = fqnWithNesting.getShortName(); final String receiverName = fqnWithNesting.getCallerFQN().getFullPath(); final Module module = ModuleUtilCore.findModuleForPsiElement(method); if (module == null) { return null; } return new MethodInfo.Impl(new ClassInfo.Impl(null, receiverName), methodName, RVisibility.PUBLIC, null); } private void dfs(@NotNull SignatureNode v, @NotNull StringBuilder currentLine, @NotNull List result) { if (result.size() > 3) { return; } if (v.getTransitions().isEmpty()) { result.add(currentLine.toString()); return; } if (v.getTransitions().size() == 1) { v.getTransitions().forEach((edge, node) -> dfs(node, addEdge(currentLine, edgeToStr(edge)), result)); return; } Map> transitions = new HashMap<>(); v.getTransitions().forEach((edge, node) -> transitions.computeIfAbsent(node, (a) -> new ArrayList<>()).add(edgeToStr(edge))); transitions.forEach((node, types) -> dfs(node, addEdge(new StringBuilder(currentLine), StringUtil.join(types, " | ")), result)); } private StringBuilder addEdge(@NotNull StringBuilder currentLine, String types) { return currentLine.append(currentLine.length() == 0 ? "" : ", ").append(types); } private String edgeToStr(ContractTransition edge) { if (edge instanceof ReferenceContractTransition) { return "T" + (((ReferenceContractTransition) edge).getMask() + 1); } else { return ((TypedContractTransition) edge).getType(); } } @NotNull @Override protected String getTextByRubyFunctionNamePsiElement(@Nullable RFName element) { return "Add type contract"; } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/intentions/BaseRubyMethodIntentionAction.kt ================================================ package org.jetbrains.plugins.ruby.ruby.intentions import com.intellij.codeInsight.intention.impl.BaseIntentionAction import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile import com.intellij.psi.util.PsiTreeUtil import org.jetbrains.plugins.ruby.ruby.lang.psi.variables.RFName abstract class BaseRubyMethodIntentionAction : BaseIntentionAction() { private var _text: String? = null final override fun getText(): String = _text ?: getTextByRubyFunctionNamePsiElement(null) protected abstract fun getTextByRubyFunctionNamePsiElement(element: RFName?): String protected fun getRFName(editor: Editor, file: PsiFile): RFName? { val offset = editor.caretModel.offset val element = file.findElementAt(offset) return PsiTreeUtil.getParentOfType(element, RFName::class.java) } override fun isAvailable(project: Project, editor: Editor, file: PsiFile): Boolean { val methodNameElement: RFName = getRFName(editor, file) ?: return false _text = getTextByRubyFunctionNamePsiElement(methodNameElement) return true } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/intentions/RemoveCollectedInfoIntention.kt ================================================ package org.jetbrains.plugins.ruby.ruby.intentions import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile import org.jetbrains.plugins.ruby.ruby.codeInsight.types.resetAllRubyTypeProviderAndIDEACaches import org.jetbrains.plugins.ruby.ruby.lang.psi.RubyPsiUtil import org.jetbrains.plugins.ruby.ruby.lang.psi.variables.RFName import org.jetbrains.ruby.codeInsight.types.signature.ClassInfo import org.jetbrains.ruby.codeInsight.types.signature.MethodInfo import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoTable class RemoveCollectedInfoIntention : BaseRubyMethodIntentionAction() { override fun getFamilyName(): String = getText() override fun getTextByRubyFunctionNamePsiElement(element: RFName?): String { return "Remove collected info about ${element?.name ?: "this"} method" } override fun invoke(project: Project, editor: Editor, file: PsiFile) { val method = getRFName(editor, file)?.let { RubyPsiUtil.getContainingRMethod(it) } ?: return val rubyModuleName = RubyPsiUtil.getContainingRClassOrModule(method)?.fqn?.fullPath ?: "Object" val info = MethodInfo.Impl(ClassInfo.Impl(null, rubyModuleName), method.fqn.shortName) DatabaseProvider.defaultDatabaseTransaction { CallInfoTable.deleteAllInfoRelatedTo(info) } resetAllRubyTypeProviderAndIDEACaches(project) } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/persistent/TypeInferenceDirectory.kt ================================================ package org.jetbrains.plugins.ruby.ruby.persistent import com.intellij.openapi.application.PathManager import com.intellij.openapi.util.io.FileUtil import java.nio.file.Paths object TypeInferenceDirectory { val RUBY_TYPE_INFERENCE_DIRECTORY by lazy { val path = Paths.get(PathManager.getSystemPath(), "ruby-type-inference")!! FileUtil.createDirectory(path.toFile()) path } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/run/configuration/CollectExecSettings.java ================================================ package org.jetbrains.plugins.ruby.ruby.run.configuration; import com.intellij.openapi.util.Key; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class CollectExecSettings { @NotNull private static final Key COLLECT_TYPE_EXEC_SETTINGS = new Key<>("CollectTypeExecSettings"); private boolean myArgScannerEnabled; private boolean myTypeTrackerEnabled; private boolean myStateTrackerEnabled; @Nullable private String myOutputDirectory; public boolean isArgScannerEnabled() { return myArgScannerEnabled; } public boolean isStateTrackerEnabled() { return myStateTrackerEnabled; } public void setStateTrackerEnabled(boolean myStateTrackerEnabled) { this.myStateTrackerEnabled = myStateTrackerEnabled; } public void setArgScannerEnabled(boolean myArgScannerEnabled) { this.myArgScannerEnabled = myArgScannerEnabled; } public boolean isTypeTrackerEnabled() { return myTypeTrackerEnabled; } public void setTypeTrackerEnabled(boolean myTypeTrackerEnabled) { this.myTypeTrackerEnabled = myTypeTrackerEnabled; } @Nullable public String getOutputDirectory() { return myOutputDirectory; } public void setReturnTypeTrackerPath(final @Nullable String path) { myOutputDirectory = path; } @NotNull public static CollectExecSettings getFrom(@NotNull final AbstractRubyRunConfiguration configuration) { final CollectExecSettings data = configuration.getCopyableUserData(COLLECT_TYPE_EXEC_SETTINGS); return data != null ? data : createSettings(false, false, false, null); } public static void putTo(@NotNull final AbstractRubyRunConfiguration configuration, @NotNull final CollectExecSettings settings) { configuration.putCopyableUserData(COLLECT_TYPE_EXEC_SETTINGS, settings); } public static CollectExecSettings createSettings(final boolean argScannerEnabled, final boolean typeTrackerEnabled, final boolean stateTrackerEnabled, final String tempDirectoryPath ) { final CollectExecSettings settings = new CollectExecSettings(); settings.setArgScannerEnabled(argScannerEnabled); settings.setTypeTrackerEnabled(typeTrackerEnabled); settings.setReturnTypeTrackerPath(tempDirectoryPath); settings.setStateTrackerEnabled(stateTrackerEnabled); return settings; } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/ruby/run/configuration/RunWithTypeTrackerRunConfigurationExtension.java ================================================ package org.jetbrains.plugins.ruby.ruby.run.configuration; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.configurations.RunnerSettings; import com.intellij.execution.process.ProcessAdapter; import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.process.ProcessHandler; import com.intellij.openapi.Disposable; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.InvalidDataException; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.Alarm; import com.intellij.util.AlarmFactory; import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.SystemIndependent; import org.jetbrains.plugins.ruby.gem.GemDependency; import org.jetbrains.plugins.ruby.gem.GemInfo; import org.jetbrains.plugins.ruby.gem.GemInstallUtil; import org.jetbrains.plugins.ruby.gem.util.GemSearchUtil; import org.jetbrains.plugins.ruby.ruby.RubyUtil; import org.jetbrains.plugins.ruby.ruby.codeInsight.stateTracker.RubyClassHierarchyWithCaching; import org.jetbrains.plugins.ruby.util.SignatureServerUtilKt; import org.jetbrains.ruby.runtime.signature.server.SignatureServer; import java.io.File; import java.io.IOException; import java.util.*; import java.util.stream.Collectors; public class RunWithTypeTrackerRunConfigurationExtension extends RubyRunConfigurationExtension { private static final Logger LOG = Logger.getInstance(RunWithTypeTrackerRunConfigurationExtension.class); private static final String ARG_SCANNER_GEM_NAME = "arg_scanner"; private static final String ARG_SCANNER_REQUIRE_SCRIPT = "arg_scanner/starter"; private static final String ENABLE_TYPE_TRACKER_KEY = "ARG_SCANNER_ENABLE_TYPE_TRACKER"; private static final String ENABLE_STATE_TRACKER_KEY = "ARG_SCANNER_ENABLE_STATE_TRACKER"; private static final String PROJECT_ROOT_KEY = "ARG_SCANNER_PROJECT_ROOT"; private static final String ARG_SCANNER_PIPE_FILE_PATH_KEY = "ARG_SCANNER_PIPE_FILE_PATH"; private static final String OUTPUT_DIRECTORY = "ARG_SCANNER_DIR"; private static final int MAX_RETRY_NO = 15; private static final int RETRY_TIMEOUT = 2000; @Override protected void readExternal(@NotNull final AbstractRubyRunConfiguration runConfiguration, @NotNull final Element element) throws InvalidDataException { } @Nullable @Override protected String getEditorTitle() { return null; } @Override public boolean isApplicableFor(@NotNull AbstractRubyRunConfiguration configuration) { return true; } @Override public boolean isEnabledFor(@NotNull AbstractRubyRunConfiguration applicableConfiguration, @Nullable RunnerSettings runnerSettings) { final CollectExecSettings config = CollectExecSettings.getFrom(applicableConfiguration); return config.isArgScannerEnabled(); } @Override protected void patchCommandLine(@NotNull final AbstractRubyRunConfiguration configuration, @Nullable final RunnerSettings runnerSettings, @NotNull final GeneralCommandLine cmdLine, @NotNull final String runnerId) { final Module module = configuration.getModule(); if (module == null) { return; } String includeKey = getRequireKeyForGem(module, configuration.getSdk(), ARG_SCANNER_GEM_NAME); if (includeKey == null) { return; } String pipeFileName = SignatureServerUtilKt.runServerAsyncInIDEACompatibleMode(new SignatureServer(), configuration.getProject()); final Map env = cmdLine.getEnvironment(); final String rubyOpt = StringUtil.notNullize(env.get(RubyUtil.RUBYOPT)); final CollectExecSettings collectTypeSettings = CollectExecSettings.getFrom(configuration); if (collectTypeSettings.isTypeTrackerEnabled()) { env.put(ENABLE_TYPE_TRACKER_KEY, "1"); } if (collectTypeSettings.isStateTrackerEnabled()) { env.put(ENABLE_STATE_TRACKER_KEY, "1"); } if (collectTypeSettings.getOutputDirectory() != null) { env.put(OUTPUT_DIRECTORY, collectTypeSettings.getOutputDirectory()); } @SystemIndependent String basePath = configuration.getProject().getBasePath(); if (basePath != null) { env.put(PROJECT_ROOT_KEY, basePath); } env.put(ARG_SCANNER_PIPE_FILE_PATH_KEY, pipeFileName); final String newRubyOpt = rubyOpt + includeKey + " -r" + ARG_SCANNER_REQUIRE_SCRIPT; cmdLine.withEnvironment(RubyUtil.RUBYOPT, newRubyOpt); } @Override protected void validateConfiguration(@NotNull AbstractRubyRunConfiguration configuration, boolean isExecution) throws Exception { RunConfigurationUtil.inspectSDK(configuration, isExecution); final Module module = configuration.getModule(); final Sdk sdk = configuration.getSdk(); if (module == null || sdk == null) { RunConfigurationUtil.throwExecutionOrRuntimeException("Cannot execute outside of module context", isExecution); } GemInfo argScannerGem = GemSearchUtil.findGem(module, sdk, ARG_SCANNER_GEM_NAME); if (argScannerGem == null) { if (isExecution) { final int result = Messages.showYesNoDialog(configuration.getProject(), "'arg_scanner' gem is required to collect the data. Do you want to install it?", "Gem Not Found", null); if (result == Messages.YES) { final HashMap errors = new HashMap<>(); GemInstallUtil.installGemsDependencies(sdk, module, Collections.singleton(GemDependency.create(ARG_SCANNER_GEM_NAME, ">= 0.3.3")), true, false, errors); if (errors.isEmpty()) { // means success return; } } } RunConfigurationUtil.throwExecutionOrRuntimeException("Cannot find required " + ARG_SCANNER_GEM_NAME + " gem in the current SDK", false); } } @Nullable private String getRequireKeyForGem(@NotNull Module module, @Nullable Sdk sdk, @NotNull String gemName) { final GemInfo gemInfo = GemSearchUtil.findGem(module, sdk, gemName); if (gemInfo == null) { return null; } final VirtualFile libFolder = gemInfo.getLibFolder(); if (libFolder == null) { return null; } return " -I" + libFolder.getPath(); } @Override protected void attachToProcess(@NotNull AbstractRubyRunConfiguration configuration, @NotNull ProcessHandler handler, @Nullable RunnerSettings runnerSettings) { final CollectExecSettings settings = CollectExecSettings.getFrom(configuration); if (settings.isStateTrackerEnabled()) { handler.addProcessListener( new ProcessAdapter() { @Override public void processTerminated(@NotNull ProcessEvent event) { processStateTrackerResult(settings, configuration); } } ); } } private boolean checkForPidFiles(final @NotNull File directory) { File[] listOfFiles = directory.listFiles(); return listOfFiles != null && Arrays.stream(listOfFiles).anyMatch((it) -> it.getName().endsWith(".pid")); } private void waitAllProcess(final @NotNull File directory, final @NotNull Runnable task, final @NotNull Disposable parent, int tryNo) { if (!checkForPidFiles(directory) || tryNo > MAX_RETRY_NO) { task.run(); } else { AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, parent).addRequest( () -> waitAllProcess(directory, task, parent,tryNo + 1), RETRY_TIMEOUT); } } private void processStateTrackerResult(final @NotNull CollectExecSettings settings, final @NotNull AbstractRubyRunConfiguration configuration) { String directoryPath = settings.getOutputDirectory(); assert directoryPath != null; File directory = new File(directoryPath); final Module module = configuration.getModule(); if (module == null) { return; } waitAllProcess(directory, () -> { try { File[] listOfFiles = directory.listFiles(); if (listOfFiles == null) { return; } final List jsons = Arrays.stream(listOfFiles).filter((it) -> it.getName().endsWith("json")).map((it) -> { try { return FileUtil.loadFile(it); } catch (IOException e) { LOG.warn(e); return null; } }).filter(Objects::nonNull).collect(Collectors.toList()); if (jsons.isEmpty()) { return; } if (settings.isStateTrackerEnabled()) { RubyClassHierarchyWithCaching.Companion.updateAndSaveToSystemDirectory(jsons, module); } } finally { FileUtil.delete(directory); } }, module, 0); } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/settings/RubyTypeContractsConfigurable.kt ================================================ package org.jetbrains.plugins.ruby.settings import com.intellij.openapi.options.ConfigurableBase class RubyTypeContractsConfigurable(private val settings: RubyTypeContractsSettings) : ConfigurableBase(RubyTypeContractsConfigurable::class.java.name, "Ruby Type Contracts", null) { override fun getSettings(): RubyTypeContractsSettings { return settings } override fun createUi(): RubyTypeContractsConfigurableUI { return RubyTypeContractsConfigurableUI(settings) } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/settings/RubyTypeContractsConfigurableUI.kt ================================================ package org.jetbrains.plugins.ruby.settings import com.intellij.openapi.options.ConfigurableUi import com.intellij.openapi.ui.VerticalFlowLayout import com.intellij.ui.BooleanTableCellEditor import com.intellij.ui.BooleanTableCellRenderer import com.intellij.ui.ToolbarDecorator import com.intellij.ui.components.JBPanel import com.intellij.ui.table.TableView import com.intellij.util.text.VersionComparatorUtil import com.intellij.util.ui.CheckBox import com.intellij.util.ui.ColumnInfo import com.intellij.util.ui.JBUI import com.intellij.util.ui.ListTableModel import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.ruby.codeInsight.types.signature.GemInfo import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.impl.GemInfoTable import org.jetbrains.ruby.codeInsight.types.storage.server.impl.RSignatureProviderImpl import java.util.* import javax.swing.JComponent import javax.swing.JTable import javax.swing.ListSelectionModel import kotlin.collections.ArrayList import kotlin.collections.HashMap class RubyTypeContractsConfigurableUI(settings: RubyTypeContractsSettings) : ConfigurableUi { private val toBeRemovedGems = ArrayList() private val registeredGems = ArrayList(RSignatureProviderImpl.registeredGems) private val perGemSettingsMap = HashMap(settings.perGemSettingsMap) private var typeTrackerEnabled = settings.typeTrackerEnabled private var stateTrackerEnabled = settings.stateTrackerEnabled private val tableModel = ListTableModel( object : ColumnInfo("Gem Name") { override fun valueOf(item: GemInfo?) = item?.name }, object : ColumnInfo("Version") { override fun valueOf(item: GemInfo?) = item?.version override fun getComparator() = Comparator { gemInfo1, gemInfo2 -> VersionComparatorUtil.COMPARATOR.compare(gemInfo1.version, gemInfo2.version) } }, object : ColumnInfo("Share") { private val renderer = BooleanTableCellRenderer() private val editor = BooleanTableCellEditor() private val width = Math.max( renderer.preferredSize.width, renderer.getFontMetrics(renderer.font).stringWidth(name) + 10) override fun getWidth(table: JTable?): Int = width override fun valueOf(item: GemInfo?) = perGemSettingsMap[item]?.share != false && item?.name != LOCAL_SOURCE_GEM_NAME override fun isCellEditable(item: GemInfo?) = item?.name != LOCAL_SOURCE_GEM_NAME override fun getRenderer(item: GemInfo?) = renderer override fun getEditor(item: GemInfo?) = editor override fun setValue(item: GemInfo, value: Boolean) { if (value) { perGemSettingsMap.remove(item) } else { perGemSettingsMap.put(GemInfoBean(item.name, item.version), PerGemSettings(false)) } } } ) private val tableView = TableView(tableModel) init { tableView.intercellSpacing = JBUI.emptySize() tableView.isStriped = true tableView.cellSelectionEnabled = false tableView.rowSelectionAllowed = true tableView.showHorizontalLines = false tableView.showVerticalLines = false tableView.setSelectionMode(ListSelectionModel.SINGLE_SELECTION) refill() } override fun reset(settings: RubyTypeContractsSettings) { perGemSettingsMap.clear() perGemSettingsMap.putAll(settings.perGemSettingsMap) typeTrackerEnabled = settings.typeTrackerEnabled stateTrackerEnabled = settings.stateTrackerEnabled toBeRemovedGems.clear() refill() } override fun isModified(settings: RubyTypeContractsSettings): Boolean { return perGemSettingsMap != settings.perGemSettingsMap || toBeRemovedGems.isNotEmpty() || settings.stateTrackerEnabled != stateTrackerEnabled || settings.typeTrackerEnabled != typeTrackerEnabled } override fun apply(settings: RubyTypeContractsSettings) { if (toBeRemovedGems.isNotEmpty()) { DatabaseProvider.defaultDatabaseTransaction { toBeRemovedGems.forEach { GemInfoTable.deleteWhere { GemInfoTable.name eq it.name and (GemInfoTable.version eq it.version) } } perGemSettingsMap.keys.removeAll(toBeRemovedGems) registeredGems.removeAll(toBeRemovedGems) } } settings.stateTrackerEnabled = stateTrackerEnabled settings.typeTrackerEnabled = typeTrackerEnabled settings.perGemSettingsMap = HashMap(perGemSettingsMap) refill() } override fun getComponent(): JComponent { val panel = JBPanel>(VerticalFlowLayout()) panel.add(ToolbarDecorator.createDecorator(tableView.component) .setRemoveAction { _ -> val selectedObject = tableView.selectedObject ?: return@setRemoveAction val selectedRowNumber = tableView.selectedRow toBeRemovedGems.add(selectedObject) tableModel.removeRow(tableView.selectedRow) tableView.selectionModel.setSelectionInterval(-239, Math.min(selectedRowNumber, tableModel.rowCount - 1)) } .disableAddAction() .disableUpDownActions().createPanel()) panel.add(CheckBox("Use state tracker results for completion", this, "stateTrackerEnabled")) panel.add(CheckBox("Use type tracker results for completion", this, "typeTrackerEnabled")) return panel } private fun refill() { tableModel.items = registeredGems.map { GemInfoBean(it.name, it.version) } } companion object { private val LOCAL_SOURCE_GEM_NAME = "LOCAL" } } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/settings/RubyTypeContractsSettings.kt ================================================ package org.jetbrains.plugins.ruby.settings import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.util.xmlb.XmlSerializerUtil import com.intellij.util.xmlb.annotations.Attribute import com.intellij.util.xmlb.annotations.MapAnnotation import org.jetbrains.ruby.codeInsight.types.signature.GemInfo @State( name = "RubyTypeContractsSettings", storages = arrayOf(Storage("ruby_type_inference.xml")) ) data class RubyTypeContractsSettings @JvmOverloads constructor( @Attribute var localSourcesTrackingPolicy: LocalSourcesTrackingPolicy = LocalSourcesTrackingPolicy.ACCUMULATE, @MapAnnotation var perGemSettingsMap: MutableMap = HashMap(), @Attribute("typeTrackerEnabled") var typeTrackerEnabled: Boolean = true, @Attribute("stateTrackerEnabled") var stateTrackerEnabled: Boolean = true) : PersistentStateComponent { override fun loadState(state: RubyTypeContractsSettings) { XmlSerializerUtil.copyBean(state, this) } override fun getState(): RubyTypeContractsSettings? = this } enum class LocalSourcesTrackingPolicy { IGNORE, CLEAR_ON_CHANGES, ACCUMULATE } data class GemInfoBean(@Attribute("name") override val name: String = "", @Attribute("version") override val version: String = "") : GemInfo data class PerGemSettings(@Attribute("share") val share: Boolean) { @Suppress("unused") private constructor() : this(true) } ================================================ FILE: ide-plugin/src/org/jetbrains/plugins/ruby/util/SignatureServerUtil.kt ================================================ package org.jetbrains.plugins.ruby.util import com.intellij.openapi.project.Project import org.jetbrains.plugins.ruby.ruby.codeInsight.types.resetAllRubyTypeProviderAndIDEACaches import org.jetbrains.ruby.runtime.signature.server.SignatureServer /** * Runs [SignatureServer] in IDEA compatible mode (for example IDEAs caches will be cleaned after server * flushes data to DB. Server is launched in daemon mode and so on) * * @return pipe filename path which should be passed to arg-scanner. */ fun SignatureServer.runServerAsyncInIDEACompatibleMode(project: Project): String { this.afterFlushListener = { resetAllRubyTypeProviderAndIDEACaches(project) } return this.runServerAsync(isDaemon = true) } ================================================ FILE: ide-plugin/src/test/java/CallStatCompletionTest.kt ================================================ import com.intellij.execution.ExecutionException import com.intellij.openapi.diagnostic.Logger import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase import junit.framework.Assert import org.jetbrains.plugins.ruby.ruby.run.RubyCommandLine import org.jetbrains.plugins.ruby.ruby.run.RubyLocalRunner import org.jetbrains.ruby.codeInsight.types.signature.CallInfo import org.jetbrains.ruby.codeInsight.types.signature.ClassInfo import org.jetbrains.ruby.codeInsight.types.signature.MethodInfo import org.jetbrains.ruby.codeInsight.types.signature.RVisibility import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.impl.RSignatureProviderImpl import org.jetbrains.ruby.runtime.signature.server.SignatureServer import java.io.IOException import java.util.concurrent.TimeUnit class CallStatCompletionTest : LightPlatformCodeInsightFixtureTestCase() { private var lastServer: SignatureServer? = null override fun getTestDataPath(): String { return "src/test/testData" } override fun setUp() { super.setUp() DatabaseProvider.connectToInMemoryDB(isDefaultDatabase = true) DatabaseProvider.dropAllDatabases() DatabaseProvider.createAllDatabases() } override fun tearDown() { try { DatabaseProvider.dropAllDatabases() } finally { super.tearDown() } } fun testSimpleCallInfoCollection() { val callInfos = runAndGetCallInfos("simple_call_info_collection_test.rb", createMethodInfo("AClass", "foo")) assertEquals(1, callInfos.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(callInfos, 1)) assertCallInfosContainsUnique(callInfos, listOf("String"), "Symbol") } fun testSimpleCallInfosCollectionMultipleFunctions() { executeScript("simple_call_info_collection_test_multiple_functions_test.rb") waitForServer() val fooCallInfos = RSignatureProviderImpl.getRegisteredCallInfos(createMethodInfo("A", "foo")) val barCallInfos = RSignatureProviderImpl.getRegisteredCallInfos(createMethodInfo("A", "bar")) assertEquals(1, fooCallInfos.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(fooCallInfos, 2)) assertCallInfosContainsUnique(fooCallInfos, listOf("String", "Class"), "String") assertEquals(3, barCallInfos.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(barCallInfos, 1)) assertCallInfosContainsUnique(barCallInfos, listOf("TrueClass"), "A") assertCallInfosContainsUnique(barCallInfos, listOf("FalseClass"), "FalseClass") assertCallInfosContainsUnique(barCallInfos, listOf("Symbol"), "A") } fun testSimpleCallInfosCollectionWithMultipleArguments() { val callInfos = runAndGetCallInfos("simple_call_info_collection_with_multiple_arguments_test.rb", createMethodInfo("AClass", "foo")) assertEquals(1, callInfos.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(callInfos, 2)) assertCallInfosContainsUnique(callInfos, listOf("String", "TrueClass"), "Regexp") } fun testSaveTypesBetweenLaunches() { var callInfos = runAndGetCallInfos("save_types_between_launches_test_part_1.rb", createMethodInfo("A", "foo")) assertEquals(2, callInfos.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(callInfos, 1)) assertCallInfosContainsUnique(callInfos, listOf("String"), "Symbol") assertCallInfosContainsUnique(callInfos, listOf("Class"), "A") callInfos = runAndGetCallInfos("save_types_between_launches_test_part_2.rb", createMethodInfo("A", "foo")) assertEquals(4, callInfos.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(callInfos, 1)) assertCallInfosContainsUnique(callInfos, listOf("String"), "Symbol") assertCallInfosContainsUnique(callInfos, listOf("Class"), "A") assertCallInfosContainsUnique(callInfos, listOf("TrueClass"), "FalseClass") assertCallInfosContainsUnique(callInfos, listOf("String"), "Regexp") } fun testForgetCallInfoWhenArgumentsNumberChanged() { var callInfos = runAndGetCallInfos("forget_call_info_when_arguments_number_changed_test_part_1.rb", createMethodInfo("A", "foo")) assertEquals(1, callInfos.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(callInfos, 1)) assertCallInfosContainsUnique(callInfos, listOf("String"), "Symbol") callInfos = runAndGetCallInfos("forget_call_info_when_arguments_number_changed_test_part_2.rb", createMethodInfo("A", "foo")) assertEquals(1, callInfos.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(callInfos, 2)) assertCallInfosContainsUnique(callInfos, listOf("TrueClass", "FalseClass"), "FalseClass") } fun testCallInfoOfNestedClass() { val callInfos = runAndGetCallInfos("call_info_of_nested_class_test.rb", createMethodInfo("M::A", "foo")) assertEquals(1, callInfos.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(callInfos, 1)) assertCallInfosContainsUnique(callInfos, listOf("M::A"), "M::A") } fun testTopLevelMethodsCallInfoCollection() { val callInfos = runAndGetCallInfos("top_level_methods_call_info_collection_test.rb", createMethodInfo("Object", "foo")) assertEquals(4, callInfos.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(callInfos, 2)) assertCallInfosContainsUnique(callInfos, listOf("TrueClass", "FalseClass"), "TrueClass") assertCallInfosContainsUnique(callInfos, listOf("FalseClass", "Symbol"), "Symbol") assertCallInfosContainsUnique(callInfos, listOf("String", "TrueClass"), "Regexp") assertCallInfosContainsUnique(callInfos, listOf("String", "TrueClass"), "String") } fun testDuplicatesInCallInfoTable() { val callInfos = runAndGetCallInfos("duplicates_in_callinfo_table_test.rb", createMethodInfo("Object", "foo")) assertEquals(3, callInfos.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(callInfos, 1)) assertCallInfosContainsUnique(callInfos, listOf("String"), "String") assertCallInfosContainsUnique(callInfos, listOf("String"), "FalseClass") assertCallInfosContainsUnique(callInfos, listOf("FalseClass"), "FalseClass") } fun testMethodWithoutParameters() { val callInfos = runAndGetCallInfos("method_without_parameters_test.rb", createMethodInfo("Object", "foo")) assertEquals(1, callInfos.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(callInfos, 0)) assertCallInfosContainsUnique(callInfos, emptyList(), "String") } fun testAnonymousModuleMethodCall() { val callInfos = runAndGetCallInfos("anonymous_module_method_call_test.rb", createMethodInfo("A", "foo")) assertEquals(1, callInfos.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(callInfos, 2)) assertCallInfosContainsUnique(callInfos, listOf("String", "Symbol"), "TrueClass") } fun testRubyExecWithBuffering() { executeScript("ruby_exec_test.rb", additionalArgScannerArgs = arrayOf("--buffering")) waitForServer() val foo: List = RSignatureProviderImpl.getRegisteredCallInfos(createMethodInfo("Object", "foo")) val bar: List = RSignatureProviderImpl.getRegisteredCallInfos(createMethodInfo("Object", "bar")) assertEquals(0, foo.size) assertEquals(1, bar.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(bar, 1)) assertCallInfosContainsUnique(bar, listOf("TrueClass"), "NilClass") } fun testRubyExecWithoutBuffering() { executeScript("ruby_exec_test.rb") waitForServer() val foo: List = RSignatureProviderImpl.getRegisteredCallInfos(createMethodInfo("Object", "foo")) val bar: List = RSignatureProviderImpl.getRegisteredCallInfos(createMethodInfo("Object", "bar")) assertEquals(1, foo.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(foo, 1)) assertCallInfosContainsUnique(foo, listOf("String"), "NilClass") assertEquals(1, bar.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(bar, 1)) assertCallInfosContainsUnique(bar, listOf("TrueClass"), "NilClass") } fun testGemFunctionsCatchingWithProjectRootSpecified() { val runnableScriptName = "in_project_root_test/in_project_root_test.rb" val projectRoot: String = javaClass.classLoader.getResource(runnableScriptName).path executeScript(runnableScriptName, additionalArgScannerArgs = arrayOf("--project-root=$projectRoot")) waitForServer() val catch = RSignatureProviderImpl.getRegisteredCallInfos(createMethodInfo("Object", "catch")) assertEquals(1, catch.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(catch, 1)) assertCallInfosContainsUnique(catch, listOf("String"), "NilClass") val catch_2 = RSignatureProviderImpl.getRegisteredCallInfos(createMethodInfo("Object", "catch_2")) assertEquals(1, catch_2.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(catch_2, 1)) assertCallInfosContainsUnique(catch_2, listOf("String"), "NilClass") val dont_catch_2 = RSignatureProviderImpl.getRegisteredCallInfos(createMethodInfo("Object", "dont_catch_2")) assertEquals(0, dont_catch_2.size) val catch_3 = RSignatureProviderImpl.getRegisteredCallInfos(createMethodInfo("Object", "catch_3")) assertEquals(1, catch_3.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(catch_3, 1)) assertCallInfosContainsUnique(catch_3, listOf("Proc"), "NilClass") val dont_catch_3 = RSignatureProviderImpl.getRegisteredCallInfos(createMethodInfo("Object", "dont_catch_3")) assertEquals(0, dont_catch_3.size) val foo = RSignatureProviderImpl.getRegisteredCallInfos(createMethodInfo("Object", "foo")) assertEquals(1, foo.size) assertTrue(allCallInfosHaveNumberOfUnnamedArguments(foo, 1)) assertCallInfosContainsUnique(foo, listOf("Proc"), "NilClass") } private fun executeScript(runnableScriptName: String, additionalArgScannerArgs: Array = emptyArray()) { val url = javaClass.classLoader.getResource(runnableScriptName) if (url == null) { val e = RuntimeException("Cannot find script: $runnableScriptName") LOGGER.error(e) throw e } val scriptPath = url.path val module = myFixture.module try { LOGGER.warn(getProcessOutput(RubyCommandLine(RubyLocalRunner.getRunner(module), false) .withWorkDirectory("../arg_scanner") .withExePath("rake") .withParameters("install") .createProcess())) lastServer = SignatureServer() val pipeFileName = lastServer!!.runServerAsync(true) assertEquals("", getProcessOutput(RubyCommandLine(RubyLocalRunner.getRunner(module), false) .withExePath("arg-scanner") .withParameters("--pipe-file-path=$pipeFileName", "--type-tracker", *additionalArgScannerArgs, "ruby", scriptPath) .createProcess())) } catch (e: ExecutionException) { LOGGER.error(e.message) throw RuntimeException(e) } catch (e: InterruptedException) { LOGGER.error(e.message) throw RuntimeException(e) } } private fun waitForServer() { try { Thread.sleep(100) } catch (ignored: InterruptedException) { } var cnt = 0 while (lastServer!!.isProcessingRequests() && cnt < 100) { try { Thread.sleep(1000) cnt++ } catch (e: InterruptedException) { throw RuntimeException(e) } } } private fun runAndGetCallInfos(executableScriptName: String, methodInfo: MethodInfo): List { executeScript(executableScriptName) waitForServer() return RSignatureProviderImpl.getRegisteredCallInfos(methodInfo) } companion object { private val LOGGER = Logger.getInstance("CallStatCompletionTest") @Throws(InterruptedException::class) private fun getProcessOutput(process: Process): String { // final InputStream inputStream = process.getInputStream(); val errorStream = process.errorStream process.waitFor(30, TimeUnit.SECONDS) try { return errorStream.bufferedReader().use { it.readText() } } catch (e: IOException) { throw RuntimeException(e) } } private fun createMethodInfo(className: String, methodName: String): MethodInfo { return MethodInfo(ClassInfo(className), methodName, RVisibility.PUBLIC) } private fun assertCallInfosContainsUnique(callInfos: List, arguments: List, returnType: String) { val toAssert = callInfos.filter { callInfo -> callInfo.unnamedArguments.map { it.type } == arguments && callInfo.returnType == returnType }.count() == 1 Assert.assertTrue("" + "Expected \n" + "${callInfos.joinToString(separator = ",\n")}\n" + "contains unique ${arguments.joinToString(prefix = "(", postfix = ")")} -> $returnType", toAssert) } private fun allCallInfosHaveNumberOfUnnamedArguments(callInfos: List, numberOfArguments: Int): Boolean { return callInfos.all { it.unnamedArguments.size == numberOfArguments } } } } ================================================ FILE: ide-plugin/src/test/java/org/jetbrains/plugins/ruby/ruby/actions/ImportExportTests.kt ================================================ package org.jetbrains.plugins.ruby.ruby.actions import junit.framework.Assert import junit.framework.TestCase import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.ruby.codeInsight.types.signature.* import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoRow import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoTable import java.nio.file.Paths import java.util.* class ImportExportTests : TestCase() { fun testSimpleExport() { val data = (0 until 2 * CHUNK_SIZE + 1).map { createCallInfo("A$it", "foo", listOf("String", "Symbol"), "Integer") } DatabaseProvider.connectToDB(generateTempDBFilePath(), isDefaultDatabase = true) DatabaseProvider.defaultDatabaseTransaction { data.forEach { CallInfoTable.insertInfoIfNotContains(it) } } val exportedDB = generateTempDBFilePath().let { pathToExport: String -> ExportContractsAction.exportContractsToFile(pathToExport, moveProgressBar = false) return@let DatabaseProvider.connectToDB(pathToExport) } Assert.assertEquals(DatabaseProvider.defaultDatabase!!.allCallInfos, exportedDB.allCallInfos) } fun testSimpleImport() { val data = (0 until 2 * CHUNK_SIZE + 1).map { createCallInfo("A$it", "foo", listOf("String", "Symbol"), "Integer") } DatabaseProvider.connectToDB(generateTempDBFilePath(), isDefaultDatabase = true) val dbToImport = generateTempDBFilePath().let { pathToImport: String -> val db = DatabaseProvider.connectToDB(pathToImport) transaction(db) { data.forEach { CallInfoTable.insertInfoIfNotContains(it) } } ImportContractsAction.importContractsFromFile(pathToImport, moveProgressBar = false) return@let db } Assert.assertEquals(dbToImport.allCallInfos, DatabaseProvider.defaultDatabase!!.allCallInfos) } fun testImportWhenDefaultDBIsNotEmpty() { val data = setOf( createCallInfo("A", "foo", listOf("String", "Symbol"), "Integer"), createCallInfo("B", "bar", listOf("Integer"), "String"), createCallInfo("C", "foobar", listOf("String"), "String") ) val defaultDBData = setOf( createCallInfo("A", "foo", listOf("String", "Symbol"), "Integer"), createCallInfo("B", "bar", listOf("String"), "String"), createCallInfo("D", "baz", listOf("Integer"), "String"), createCallInfo("E", "foobar", listOf("String", "Symbol"), "String") ) DatabaseProvider.connectToDB(generateTempDBFilePath(), isDefaultDatabase = true) DatabaseProvider.defaultDatabaseTransaction { defaultDBData.forEach { CallInfoTable.insertInfoIfNotContains(it) } } val dbToImport = generateTempDBFilePath().let { pathToImport: String -> val db = DatabaseProvider.connectToDB(pathToImport) transaction(db) { data.forEach { CallInfoTable.insertInfoIfNotContains(it) } } ImportContractsAction.importContractsFromFile(pathToImport, moveProgressBar = false) return@let db } Assert.assertEquals(dbToImport.allCallInfos.union(defaultDBData), DatabaseProvider.defaultDatabase!!.allCallInfos) } private val Database.allCallInfos: Set get() = transaction(this) { CallInfoRow.all().map { it.copy() } }.toSet() private fun createCallInfo(className: String, methodName: String, unnamedArgsTypes: List, returnType: String): CallInfo { val args = unnamedArgsTypes.mapIndexed { index, s -> ArgumentNameAndType(('a' + index).toString(), s) } return CallInfoImpl(MethodInfo(ClassInfo(className), methodName, RVisibility.PUBLIC), emptyList(), args, returnType) } private fun generateTempDBFilePath(prefix: String = ""): String { val dirForTempFiles = System.getProperty("java.io.tmpdir") return Paths.get(dirForTempFiles, prefix + UUID.randomUUID()).toString() + DatabaseProvider.H2_DB_FILE_EXTENSION } } ================================================ FILE: ide-plugin/src/test/testData/anonymous_module_method_call_test.rb ================================================ module A def self.foo(a, b) true end end A.foo("hey", :symbol) ================================================ FILE: ide-plugin/src/test/testData/call_info_of_nested_class_test.rb ================================================ module M class A def foo(a) a end end end a = M::A.new a.foo(a) ================================================ FILE: ide-plugin/src/test/testData/duplicates_in_callinfo_table_test.rb ================================================ def foo(a) if a == "str" return a end false end foo("str") foo("not str") 3.times { foo("str") } 3.times { foo(false) } ================================================ FILE: ide-plugin/src/test/testData/forget_call_info_when_arguments_number_changed_test_part_1.rb ================================================ class A def foo(a) :symbol end end A.new.foo("Hey") ================================================ FILE: ide-plugin/src/test/testData/forget_call_info_when_arguments_number_changed_test_part_2.rb ================================================ class A def foo(a, b) b end end A.new.foo(true, false) ================================================ FILE: ide-plugin/src/test/testData/in_project_root_test/gem_like.rb ================================================ def catch(a); end def dont_catch_2(a); end def catch_2(a) dont_catch_2(a) end def dont_catch_3(&a) yield(a) end def catch_3(&a) dont_catch_3(&a) end ================================================ FILE: ide-plugin/src/test/testData/in_project_root_test/in_project_root_test.rb ================================================ require_relative 'gem_like' catch('hey') catch_2('bro') def foo(a); end catch_3(&method(:foo)) ================================================ FILE: ide-plugin/src/test/testData/merge_test1.rb ================================================ class A end class C end class B1 def test1 end def test2 end end class B2 def test3 end def test4 end end A.class_eval < ================================================ FILE: ide-plugin/src/test/testData/merge_test1_to_run.rb ================================================ class A end class C end class B1 def test1 end def test2 end end class B2 def test3 end def test4 end end A.class_eval < ================================================ FILE: ide-plugin/src/test/testData/merge_test2_to_run.rb ================================================ class A end class C end class B1 def test1 end def test2 end end class B2 def test3 end def test4 end end A.class_eval < ================================================ FILE: ide-plugin/src/test/testData/multiple_execution_test2_to_run.rb ================================================ require 'date' class A end class C end class B def test1 end def test2 end end A.class_eval < ================================================ FILE: ide-plugin/src/test/testData/ref_links_test_to_run.rb ================================================ class A end class B def test1 end def test2 end end A.class_eval < ================================================ FILE: ide-plugin/src/test/testData/sample_kw_test_to_run.rb ================================================ require 'date' class A end class C end class B def test1 end def test2 end end A.class_eval < ================================================ FILE: ide-plugin/src/test/testData/sample_test_to_run.rb ================================================ require 'date' class A end class C end class B def test1 end def test2 end end A.class_eval < /** * Types of named arguments sorted alphabetically by [ArgumentNameAndType.name] */ val namedArguments: List val returnType: String /** * Join [unnamedArguments] to raw [String] which is used in database */ fun unnamedArgumentsTypesJoinToRawString(): String /** * Join [namedArgumentsJoinToRawString] to raw [String] which is used in database. * Should return concatenated string containing elements ordered by argument name alphabetically */ fun namedArgumentsJoinToRawString(): String fun getTypeNameByArgumentName(name: String): String? { return (unnamedArguments.find { it.name == name } ?: namedArguments.find { it.name == name })?.type } } data class ArgumentNameAndType(val name: String, val type: String) { companion object { const val NAME_AND_TYPE_SEPARATOR = "," /** * For such method: * def foo(a, b = 1); end * * And such call: * foo(a) * `b` is implicitly passed */ const val IMPLICITLY_PASSED_ARGUMENT_TYPE = "-" } } class CallInfoImpl(override val methodInfo: MethodInfo, namedArguments: List, override val unnamedArguments: List, override val returnType: String) : CallInfo { override val namedArguments = namedArguments.sortedBy { it.name } override fun namedArgumentsJoinToRawString(): String = namedArguments.joinToString(separator = ARGUMENTS_TYPES_SEPARATOR) { it.name + "," + it.type } override fun unnamedArgumentsTypesJoinToRawString(): String = unnamedArguments.joinToString(separator = ARGUMENTS_TYPES_SEPARATOR) { it.name + "," + it.type } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CallInfo) return false other as CallInfoImpl return methodInfo == other.methodInfo && unnamedArguments == other.unnamedArguments && returnType == other.returnType && namedArguments == other.namedArguments } override fun hashCode(): Int { var result = methodInfo.hashCode() result = 31 * result + unnamedArguments.hashCode() result = 31 * result + returnType.hashCode() result = 31 * result + namedArguments.hashCode() return result } override fun toString(): String { return "CallInfoIml(methodInfo=$methodInfo, namedArguments=$namedArguments, unnamedArguments=$unnamedArguments, returnType=$returnType)" } } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/ClassInfo.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature interface ClassInfo { val gemInfo: GemInfo? val classFQN: String data class Impl(override val gemInfo: GemInfo?, override val classFQN: String) : ClassInfo fun validate(): Boolean { if (classFQN.length > LENGTH_OF_FQN) { return false } val gemInfoVal = gemInfo return gemInfoVal == null || gemInfoVal.validate() } companion object { val LENGTH_OF_FQN = 200 } } fun ClassInfo(gemInfo: GemInfo?, classFQN: String) = ClassInfo.Impl(gemInfo, classFQN) fun ClassInfo(classFQN: String) = ClassInfo.Impl(null, classFQN) fun ClassInfo(copy: ClassInfo) = with(copy) { ClassInfo.Impl(gemInfo?.let { GemInfo(it) }, classFQN) } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/GemInfo.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature interface GemInfo { val name: String val version: String data class Impl(override val name: String, override val version: String) : GemInfo fun validate(): Boolean { return name.length <= LENGTH_OF_GEMNAME && version.length <= LENGTH_OF_GEMVERSION } companion object { val NONE = Impl("", "") val LENGTH_OF_GEMNAME = 50 val LENGTH_OF_GEMVERSION = 50 } } fun GemInfo(name: String, version: String) = GemInfo.Impl(name, version) fun GemInfo(copy: GemInfo) = with(copy) { GemInfo.Impl(name, version) } fun GemInfoOrNull(name: String, version: String) = GemInfo(name, version).let { if (it == GemInfo.NONE) null else it} fun gemInfoFromFilePathOrNull(path: String): GemInfo? { val gemPathPattern = """([A-Za-z0-9_-]+)-(\d+[0-9A-Za-z.]+)""" val regex = Regex(gemPathPattern) val (gemName, gemVersion) = regex.findAll(path).lastOrNull()?.destructured ?: return null return GemInfo(gemName, gemVersion) } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/MethodInfo.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature interface MethodInfo { val classInfo: ClassInfo val name: String val visibility: RVisibility val location: Location? data class Impl(override val classInfo: ClassInfo, override val name: String, override val visibility: RVisibility = RVisibility.PUBLIC, override val location: Location? = null) : MethodInfo fun validate(): Boolean { if (name.length > LENGTH_OF_NAME) { return false } val loc = location if (loc == null || loc.path.length > LENGTH_OF_PATH) { return false } return classInfo.validate() } companion object { val LENGTH_OF_NAME = 100 val LENGTH_OF_PATH = 1000 } } @JvmOverloads fun MethodInfo(classInfo: ClassInfo, name: String, visibility: RVisibility, location: Location? = null) = MethodInfo.Impl(classInfo, name, visibility, location) fun MethodInfo(copy: MethodInfo) = with(copy) { MethodInfo.Impl(ClassInfo(classInfo), name, visibility, location) } data class Location(val path: String, val lineno: Int) enum class RVisibility constructor(val value: Byte, val presentableName: String) { PRIVATE(0, "PRIVATE"), PROTECTED(1, "PROTECTED"), PUBLIC(2, "PUBLIC"); } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/ParameterInfo.java ================================================ package org.jetbrains.ruby.codeInsight.types.signature; import org.jetbrains.annotations.NotNull; public class ParameterInfo { @NotNull private final String myName; @NotNull private final ParameterInfo.Type myModifier; public ParameterInfo(@NotNull final String name, @NotNull final Type modifier) { myName = name; myModifier = modifier; } @NotNull public String getName() { return myName; } @NotNull public ParameterInfo.Type getModifier() { return myModifier; } public boolean isNamedParameter() { return myModifier == Type.KEY || myModifier == Type.KEYREQ || myModifier == Type.KEYREST; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final ParameterInfo that = (ParameterInfo) o; //noinspection SimplifiableIfStatement if (!myName.equals(that.myName)) return false; return myModifier == that.myModifier; } @Override public int hashCode() { int result = myName.hashCode(); result = 31 * result + myModifier.hashCode(); return result; } // parameter info: // // def foo(a, # mandatory (REQ) // b=1, # optional (OPT) // *c, # rest (REST) // d, # post (POST) // e:, # keywords (KEYREQ) // f:1, # optional keywords (KEY) // **g, # rest keywords (KEYREST) // &h) # block public enum Type { REQ, OPT, POST, REST, KEYREQ, KEY, KEYREST, BLOCK, } } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/RSignatureContract.java ================================================ package org.jetbrains.ruby.codeInsight.types.signature; import kotlin.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.ContractTransition; import java.util.*; import static org.jetbrains.ruby.codeInsight.types.signature.contractTransition.TransitionHelper.calculateTransition; public class RSignatureContract implements SignatureContract { @NotNull private final RSignatureContractNode myStartContractNode; @NotNull private final List myArgsInfo; @NotNull private final List> myLevels; @NotNull private final SignatureNode myTermNode; public RSignatureContract(@NotNull RTuple tuple) { myArgsInfo = tuple.getArgsInfo(); myLevels = new ArrayList<>(getArgsInfo().size() + 2); for (int i = 0; i < getArgsInfo().size() + 2; i++) { myLevels.add(new ArrayList<>()); } myStartContractNode = Objects.requireNonNull(createNodeAndAddToLevels(0)); myTermNode = Objects.requireNonNull(createNodeAndAddToLevels(myLevels.size() - 1)); addRTuple(tuple); } public RSignatureContract(@NotNull List argsInfo, @NotNull RSignatureContractNode startContractNode, @NotNull SignatureNode termNode, @NotNull List> levels) { myStartContractNode = startContractNode; myArgsInfo = argsInfo; myLevels = levels; myTermNode = termNode; // TODO recalculate mask } private RSignatureContract(@NotNull SignatureContract source) { myArgsInfo = source.getArgsInfo(); myLevels = new ArrayList<>(getArgsInfo().size() + 2); for (int i = 0; i < getArgsInfo().size() + 2; i++) { myLevels.add(new ArrayList<>()); } final Map> oldNodesToNewWithLayerNumber = new HashMap<>(); final Queue q = new ArrayDeque<>(); final RSignatureContractNode newStartNode = Objects.requireNonNull(createNodeAndAddToLevels(0)); myLevels.get(0).add(newStartNode); oldNodesToNewWithLayerNumber.put(newStartNode, new kotlin.Pair<>(newStartNode, 0)); q.add(source.getStartNode()); while (!q.isEmpty()) { final SignatureNode oldSourceNode = q.poll(); final kotlin.Pair newSourceNodeAndLevel = oldNodesToNewWithLayerNumber.get(oldSourceNode); oldSourceNode.getTransitions().forEach((contractTransition, oldTargetNode) -> { final kotlin.Pair newTargetNodeWithLayer = oldNodesToNewWithLayerNumber.computeIfAbsent(oldTargetNode, old -> { final RSignatureContractNode newNode = createNodeAndAddToLevels(newSourceNodeAndLevel.getSecond() + 1); q.add(newNode); return new kotlin.Pair<>( newNode, newSourceNodeAndLevel.getSecond() + 1 ); }); newSourceNodeAndLevel.getFirst().addLink(contractTransition, newTargetNodeWithLayer.getFirst()); }); } myStartContractNode = newStartNode; if (myLevels.get(myLevels.size() - 1).size() != 1) { throw new AssertionError("Incorrect # of nodes on the last level: " + myLevels.get(myLevels.size() - 1)); } myTermNode = myLevels.get(myLevels.size() - 1).iterator().next(); } @NotNull @Override public RSignatureContractNode getStartNode() { return myStartContractNode; } @NotNull @Override public List getArgsInfo() { return myArgsInfo; } public int getNodeCount() { return myLevels.stream().map(List::size).reduce(0, (a, b) -> a + b); } @NotNull public synchronized SignatureContract copy() { final Map oldToNew = new HashMap<>(); RSignatureContractNode newStartNode = new RSignatureContractNode(); oldToNew.put(myStartContractNode, newStartNode); for (int i = 0; i < myLevels.size() - 1; ++i) { for (final RSignatureContractNode oldSourceNode : myLevels.get(i)) { final RSignatureContractNode newSourceNode = oldToNew.get(oldSourceNode); oldSourceNode.getTransitions().forEach((transition, oldTargetNode) -> { final RSignatureContractNode newTargetNode = oldToNew.computeIfAbsent(oldTargetNode, (x) -> new RSignatureContractNode()); newSourceNode.addLink(transition, newTargetNode); }); } } return new Immutable(newStartNode, oldToNew.size(), myArgsInfo); } /** * @return true if succeeded; false otherwise */ public synchronized boolean addRTuple(@NotNull RTuple tuple) { final List argsTypes = tuple.getArgsTypes(); if (argsTypes.size() != myArgsInfo.size()) { return false; } String returnType = tuple.getReturnTypeName(); RSignatureContractNode currNode = myStartContractNode; for (int argIndex = 0; argIndex < argsTypes.size(); argIndex++) { final String type = argsTypes.get(argIndex); final ContractTransition transition = calculateTransition(tuple.getArgsTypes(), argIndex, type); if (!currNode.getTransitions().containsKey(transition)) { final RSignatureContractNode newNode = createNodeAndAddToLevels(argIndex + 1); if (newNode == null) { return false; } currNode.addLink(transition, newNode); currNode = newNode; } else { currNode = ((RSignatureContractNode) currNode.getTransitions().get(transition)); } } final ContractTransition transition = calculateTransition(tuple.getArgsTypes(), tuple.getArgsTypes().size(), returnType); currNode.addLink(transition, myTermNode); return true; } synchronized void minimize() { int numberOfLevels = myLevels.size(); for (int i = numberOfLevels - 1; i > 0; i--) { List level = myLevels.get(i); HashMap representatives = new HashMap<>(); Set uselessVertices = new HashSet<>(); for (SignatureNode node : level) { representatives.put(node, node); } for (int v1 = 0; v1 < level.size(); v1++) { for (int v2 = v1 + 1; v2 < level.size(); v2++) { SignatureNode vertex1 = level.get(v1); SignatureNode vertex2 = level.get(v2); boolean isSame = vertex1.getTransitions().size() == vertex2.getTransitions().size(); for (ContractTransition transition : vertex1.getTransitions().keySet()) { if (!vertex2.getTransitions().containsKey(transition) || vertex1.getTransitions().get(transition) != vertex2.getTransitions().get(transition)) { isSame = false; } } if (isSame) { SignatureNode vertex1presenter = representatives.get(vertex1); representatives.put(vertex2, vertex1presenter); uselessVertices.add(vertex2); } } } List prevLevel = myLevels.get(i - 1); if (!uselessVertices.isEmpty()) { for (RSignatureContractNode node : prevLevel) { for (ContractTransition transition : node.getTransitions().keySet()) { RSignatureContractNode child = ((RSignatureContractNode) node.getTransitions().get(transition)); node.addLink(transition, representatives.get(child)); } } } //noinspection SuspiciousMethodCalls level.removeAll(uselessVertices); } } @TestOnly @NotNull public List> getLevels() { return myLevels; } private void AddToBfsQueueAndUse(@NotNull SignatureNode oldNode, @NotNull SignatureNode newNode, @NotNull Queue> bfsQueue, @NotNull Set used, Integer level) { PairOfNodes newPairOfNodes = new PairOfNodes(oldNode, newNode); if (!used.contains(newPairOfNodes)) { used.add(newPairOfNodes); bfsQueue.add(new Pair<>(newPairOfNodes, level)); } } /** * @return true if succeeded; false otherwise */ public synchronized boolean mergeWith(@NotNull SignatureContract additive) { // TODO synchronize on additive (can't do this plainly due to the possible deadlock)??? Set used = new HashSet<>(); Queue> bfsQueue = new LinkedList<>(); PairOfNodes startPairOfNodes = new PairOfNodes(getStartNode(), additive.getStartNode()); bfsQueue.add(new Pair<>(startPairOfNodes, 0)); while (!bfsQueue.isEmpty()) { Pair currItem = bfsQueue.poll(); SignatureNode oldNode = currItem.getFirst().myOldNode; SignatureNode newNode = currItem.getFirst().myNewNode; Integer level = currItem.getSecond(); Map childNodesWithPows = new HashMap<>(); for (ContractTransition transition : oldNode.getTransitions().keySet()) { SignatureNode node = oldNode.getTransitions().get(transition); if (childNodesWithPows.containsKey(node)) { Integer oldPow = childNodesWithPows.get(node); childNodesWithPows.put(node, oldPow + 1); } else { childNodesWithPows.put(node, 1); } } for (ContractTransition transition : newNode.getTransitions().keySet()) { if (oldNode.getTransitions().containsKey(transition)) { SignatureNode node = oldNode.getTransitions().get(transition); if (childNodesWithPows.get(node) == 1) { AddToBfsQueueAndUse(node, newNode.getTransitions().get(transition), bfsQueue, used, level + 1); continue; } Integer oldPow = childNodesWithPows.get(node); childNodesWithPows.put(node, oldPow - 1); } RSignatureContractNode node = createNodeAndAddToLevels(level + 1); if (node == null) { return false; } if (oldNode.getTransitions().keySet().contains(transition)) { SignatureNode nodeToClone = oldNode.getTransitions().get(transition); for (ContractTransition contractTransition : nodeToClone.getTransitions().keySet()) { node.getTransitions().put(contractTransition, nodeToClone.getTransitions().get(contractTransition)); } } oldNode.getTransitions().put(transition, node); AddToBfsQueueAndUse(node, newNode.getTransitions().get(transition), bfsQueue, used, level + 1); } } minimize(); return true; } /** * @return newly created {@link RSignatureContract} if index in 0 (inclusively) until myLevels.size() (exclusively); * otherwise {@code null} */ @Nullable private RSignatureContractNode createNodeAndAddToLevels(int index) { if (index >= myLevels.size()) { return null; } //TODO if(index == myLevels.size() - 1 && !myLevels.get(index).isEmpty()) { return myLevels.get(index).get(0); } RSignatureContractNode newNode = new RSignatureContractNode(); myLevels.get(index).add(newNode); return newNode; } @Nullable public static RSignatureContract mergeMutably(@NotNull SignatureContract first, @NotNull SignatureContract second) { if (first instanceof RSignatureContract) { if (((RSignatureContract) first).mergeWith(second)) { return ((RSignatureContract) first); } else { return null; } } else { return mergeMutably(new RSignatureContract(first), second); } } private static class Immutable implements SignatureContract { @NotNull private final SignatureNode myStartNode; private final int myNodeCount; @NotNull private final List myArgsInfo; private Immutable(@NotNull SignatureNode startNode, int nodeCount, @NotNull List argsInfo) { myStartNode = startNode; myNodeCount = nodeCount; myArgsInfo = argsInfo; } @Override public int getNodeCount() { return myNodeCount; } @NotNull @Override public SignatureNode getStartNode() { return myStartNode; } @NotNull @Override public List getArgsInfo() { return myArgsInfo; } } private static class PairOfNodes { @NotNull private final SignatureNode myOldNode; @NotNull private final SignatureNode myNewNode; @NotNull PairOfNodes pairGoByTransition(@NotNull ContractTransition transition) { return new PairOfNodes(myOldNode.getTransitions().get(transition), myNewNode.getTransitions().get(transition)); } PairOfNodes(@NotNull SignatureNode node1, @NotNull SignatureNode node2) { myOldNode = node1; myNewNode = node2; } } } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/RSignatureContractContainer.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature class RSignatureContractContainer { private val myContracts: MutableMap = HashMap() private val myNumberOfCalls: MutableMap = HashMap() fun acceptTuple(tuple: RTuple): Boolean { val currInfo = tuple.methodInfo val contract = myContracts[currInfo] return contract != null && tuple.argsInfo == contract.argsInfo && SignatureContract.accept(contract, tuple) } fun addTuple(tuple: RTuple) { val currInfo = tuple.methodInfo if (myContracts.containsKey(currInfo)) { val contract = myContracts[currInfo] if (tuple.argsInfo.size == contract?.argsInfo?.size) { contract.addRTuple(tuple) myNumberOfCalls.compute(currInfo) { _, oldNumber -> (oldNumber ?: 0) + 1 } } } else { val contract = RSignatureContract(tuple) myContracts.put(currInfo, contract) } } val registeredMethods: Set get() = myContracts.keys fun getSignature(info: MethodInfo): RSignatureContract? { return myContracts[info]?.apply { minimize() } } fun clear() { myContracts.clear() } val size: Int get() = myContracts.size } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/RSignatureContractNode.java ================================================ package org.jetbrains.ruby.codeInsight.types.signature; import org.jetbrains.annotations.NotNull; import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.ContractTransition; import java.util.HashMap; import java.util.Map; public class RSignatureContractNode implements SignatureNode { @NotNull private final Map myTransitions; public RSignatureContractNode() { myTransitions = new HashMap<>(); } public void addLink(final @NotNull ContractTransition transition, @NotNull SignatureNode arrivalNode) { myTransitions.put(transition, arrivalNode); } @NotNull @Override public Map getTransitions() { return myTransitions; } } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/RTuple.java ================================================ package org.jetbrains.ruby.codeInsight.types.signature; import org.jetbrains.annotations.NotNull; import java.util.List; public class RTuple { @NotNull private final MethodInfo myMethodInfo; @NotNull private final List myArgsInfo; @NotNull private final List myArgsTypes; @NotNull private final String myReturnTypeName; public RTuple(@NotNull final MethodInfo methodInfo, @NotNull final List argsInfo, @NotNull final List argsTypeName, @NotNull final String returnTypeName) { myMethodInfo = methodInfo; myArgsInfo = argsInfo; myArgsTypes = argsTypeName; myReturnTypeName = returnTypeName; } @NotNull public MethodInfo getMethodInfo() { return myMethodInfo; } @NotNull public List getArgsInfo() { return myArgsInfo; } @NotNull List getArgsTypes() { return myArgsTypes; } @NotNull String getReturnTypeName() { return myReturnTypeName; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RTuple that = (RTuple) o; return myMethodInfo.equals(that.myMethodInfo) && myArgsInfo.equals(that.myArgsInfo) && myArgsTypes.equals(that.myArgsTypes); } @Override public int hashCode() { int result = myMethodInfo.hashCode(); result = 31 * result + myArgsInfo.hashCode(); result = 31 * result + myArgsTypes.hashCode(); return result; } } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/SignatureContract.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.ContractTransition import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.ReferenceContractTransition import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.TransitionHelper import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.TypedContractTransition import java.util.* import kotlin.collections.HashMap import kotlin.collections.HashSet /** * The `SignatureContract` interface allows for checking input type sequence validity * as well as hinting the next possible transitions for the current sequence. * * The set of type contracts representing the method is represented with * an automaton of a specific kind: * * There is exactly one start node (=state) as an entry point for each * type sequence; * * There is exactly one finish node (=state) representing a successful * read of the type sequence; * * All paths from start node the the finish node have the same length. * * There is no difference between input type and return type in the means * of the automaton used. Thus, * * * A set of input and output types `(A, B) -> C` is read in the contract iff * 1. automaton "length" is 3, and * 2. reading types `(A, B, C)` by the automaton succeeds. * * Return type for input types `(A, B)` is calculated by reading `(A, B)` * in the automaton and getting _the only_ outbound transition from the resulting node. */ interface SignatureContract { val nodeCount: Int val startNode: SignatureNode val argsInfo: List companion object { fun accept(rSignatureContract: SignatureContract, signature: RTuple): Boolean { var currNode = rSignatureContract.startNode val returnType = signature.returnTypeName val argsTypes = signature.argsTypes for (argIndex in argsTypes.indices) { val type = argsTypes[argIndex] val transition = TransitionHelper.calculateTransition(signature.argsTypes, argIndex, type) currNode = currNode.transitions[transition] ?: return false } val transition = TransitionHelper.calculateTransition(signature.argsTypes, signature.argsTypes.size, returnType) return currNode.transitions.containsKey(transition) } fun getAllReturnTypes(rSignatureContract: SignatureContract): Set { val curNode = rSignatureContract.startNode return forwardLook(curNode) } private fun getBackEdges(startNode: SignatureNode): Map> { val result = HashMap>() val queue = LinkedList() val visited = HashSet() queue.push(startNode) visited.add(startNode) while (!queue.isEmpty()) { val node = queue.poll() for ((transition, succ) in node.transitions) { if (!visited.contains(succ)) { var nodeEntry = result[succ] if (nodeEntry == null) { nodeEntry = HashMap() result.put(succ, nodeEntry) } nodeEntry.put(transition, node) visited.add(succ) queue.push(succ) } } } return result } private fun forwardLook(startNode: SignatureNode): Set { val backEdges = getBackEdges(startNode) val queue = LinkedList() val visited = HashSet() val level = HashMap() val result = HashSet() queue.push(startNode) visited.add(startNode) level[startNode] = 0 while (!queue.isEmpty()) { val signatureNode = queue.poll() for ((key, value) in signatureNode.transitions) { // is it exit node? if (value.transitions.isEmpty()) { when (key) { is TypedContractTransition -> result.add(key.type) is ReferenceContractTransition -> result.addAll( backwardLook(signatureNode, level[signatureNode]!! - getHighestSetBit(key.mask), level[signatureNode]!!, backEdges)) else -> throw IllegalStateException() } } else if (!visited.contains(value)) { visited.add(value) level[value] = level[signatureNode]!! + 1 queue.add(value) } } } return result } private fun getHighestSetBit(mask: Int): Int { var m = mask var result = 0 while (m > 0) { result++ m = m shr 1 } return result } private fun backwardLook(value: SignatureNode, numOfSteps: Int, originLevel: Int, backEdges: Map>): Collection { var nodesOnCurrentLevel = HashSet() nodesOnCurrentLevel.add(value) var currentStep = numOfSteps; while (currentStep >= 0) { if (currentStep > 0 ) { val nodesOnPreviousLevel = HashSet() for (node in nodesOnCurrentLevel) { for ((_, pred) in backEdges[node]!!) { nodesOnPreviousLevel.add(pred) } } nodesOnCurrentLevel = nodesOnPreviousLevel } else { val result = HashSet() for (node in nodesOnCurrentLevel) { for ((transition, _) in backEdges[node]!!) { if (transition is TypedContractTransition) { result.add(transition.type) } else if (transition is ReferenceContractTransition) { val curLevel = originLevel - numOfSteps val curNumOfSteps = curLevel - getHighestSetBit(transition.mask) val res = backwardLook(node, curNumOfSteps, curLevel, backEdges) result.addAll(res) } else throw IllegalArgumentException("Cannot reach") } } return result } currentStep--; } throw IllegalArgumentException("Cannot reach") } } } interface SignatureNode { val transitions: Map } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/SignatureInfo.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature interface SignatureInfo { val methodInfo: MethodInfo val contract: SignatureContract data class Impl(override val methodInfo: MethodInfo, override val contract: SignatureContract) : SignatureInfo } fun SignatureInfo(methodInfo: MethodInfo, contract: SignatureContract) = SignatureInfo.Impl(methodInfo, contract) fun SignatureInfo(copy: SignatureInfo) = with(copy) { SignatureInfo.Impl(MethodInfo(methodInfo), contract) } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/contractTransition/ContractTransition.java ================================================ package org.jetbrains.ruby.codeInsight.types.signature.contractTransition; import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.Set; public interface ContractTransition { /** * Return literal type set of this transition. This method respects reference transitions * which types depend on some previous passed values. * * @param readTypes previously read literal types. Set represents possible type unions * @return computed literal type set for this transition */ @NotNull Set getValue(@NotNull List> readTypes); } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/contractTransition/ReferenceContractTransition.java ================================================ package org.jetbrains.ruby.codeInsight.types.signature.contractTransition; import org.jetbrains.annotations.NotNull; import java.util.HashSet; import java.util.List; import java.util.Set; public class ReferenceContractTransition implements ContractTransition { private final int myMask; public ReferenceContractTransition(int mask) { myMask = mask; } @NotNull @Override public Set getValue(@NotNull List> readTypes) { int tmpMask = myMask; int cnt = 0; Set ans = null; while (tmpMask > 0) { if (tmpMask % 2 == 1) { if (ans == null) { ans = new HashSet<>(readTypes.get(cnt)); } else { ans.retainAll(readTypes.get(cnt)); } } tmpMask /= 2; cnt++; } return ans; } public int getMask() { return myMask; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ReferenceContractTransition that = (ReferenceContractTransition) o; return myMask == that.myMask; } @Override public int hashCode() { return myMask; } } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/contractTransition/TransitionHelper.java ================================================ package org.jetbrains.ruby.codeInsight.types.signature.contractTransition; import org.jetbrains.annotations.NotNull; import java.util.List; public class TransitionHelper { private TransitionHelper() { } @NotNull public static ContractTransition calculateTransition(@NotNull List argTypes, int argIndex, @NotNull String type) { final int mask = getNewMask(argTypes, argIndex, type); if (mask > 0) return new ReferenceContractTransition(mask); else return new TypedContractTransition(type); } private static int getNewMask(@NotNull List argsTypes, int argIndex, @NotNull String type) { int tempMask = 0; for (int i = argIndex - 1; i >= 0; i--) { tempMask <<= 1; if (argsTypes.get(i).equals(type)) { tempMask |= 1; } } return tempMask; } } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/contractTransition/TypedContractTransition.java ================================================ package org.jetbrains.ruby.codeInsight.types.signature.contractTransition; import org.jetbrains.annotations.NotNull; import java.util.Collections; import java.util.List; import java.util.Set; public class TypedContractTransition implements ContractTransition { @NotNull private final String myType; public TypedContractTransition(@NotNull String type) { this.myType = type; } @NotNull @Override public Set getValue(@NotNull List> readTypes) { return Collections.singleton(myType); } @NotNull public String getType() { return myType; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TypedContractTransition that = (TypedContractTransition) o; return myType.equals(that.myType); } @Override public int hashCode() { return myType.hashCode(); } } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/serialization/MethodInfoSerialization.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature.serialization import org.jetbrains.ruby.codeInsight.types.signature.* import java.io.DataInput import java.io.DataOutput import java.io.IOException fun MethodInfo.serialize(stream: DataOutput) { classInfo.serialize(stream) stream.writeUTF(name) stream.writeByte(visibility.ordinal) stream.writeBoolean(location != null) location?.serialize(stream) } fun MethodInfo(stream: DataInput): MethodInfo { val classInfo = ClassInfo(stream) val name = stream.readUTF() val visibility = RVisibility.values()[stream.readByte().toInt()] val isLocationPresent = stream.readBoolean() val location = if (isLocationPresent) Location(stream) else null return MethodInfo.Impl(classInfo, name, visibility, location) } fun ClassInfo.serialize(stream: DataOutput) { stream.writeUTF(classFQN) gemInfo.serialize(stream) } fun ClassInfo(stream: DataInput): ClassInfo { val classFQN = stream.readUTF() val gemInfo = GemInfo(stream) return ClassInfo(gemInfo, classFQN) } fun GemInfo?.serialize(stream: DataOutput) { (this ?: GemInfo.NONE).let { stream.writeUTF(it.name) stream.writeUTF(it.version) } } fun GemInfo(stream: DataInput): GemInfo? { val name = stream.readUTF() val version = stream.readUTF() return GemInfoOrNull(name, version) } fun Location.serialize(stream: DataOutput) { stream.writeUTF(path) stream.writeInt(lineno) } fun Location(stream: DataInput): Location { val path = stream.readUTF() val lineno = stream.readInt() return Location(path, lineno) } object SignatureInfoSerialization { private val PROTOCOL_VERSION = 1 fun serialize(signatureInfos: List, stream: DataOutput) { writeProtocolVersion(stream) val (classInfo2Id, gemInfo2Id) = collectClassInfoAndGemInfo(signatureInfos) serializeGemInfos(gemInfo2Id, stream) serializeClassInfos(stream, classInfo2Id, gemInfo2Id) serializeSignatureInfos(stream, signatureInfos, classInfo2Id) } fun deserialize(stream: DataInput): List { checkProtocolVersion(stream) val id2GemInfo = deserializeGemInfo(stream) val id2ClassInfo = deserializeClassInfo(stream, id2GemInfo) return deserializeSignatureInfos(stream, id2ClassInfo) } private fun writeProtocolVersion(stream: DataOutput) { stream.writeInt(PROTOCOL_VERSION) } private fun checkProtocolVersion(stream: DataInput) { val version = stream.readInt() if (version != PROTOCOL_VERSION) { throw IOException("Cannot deserialize SignatureInfos: protocol version mismatch. Expected:" + " $PROTOCOL_VERSION but got: $version") } } private fun collectClassInfoAndGemInfo(signatureInfos: List) : Pair, LinkedHashMap>{ val gemInfo2Id = LinkedHashMap() val classInfo2Id = LinkedHashMap() signatureInfos.forEach { val classInfo = it.methodInfo.classInfo val gemInfo = classInfo.gemInfo classInfo2Id.putIfAbsent(classInfo, classInfo2Id.size) gemInfo?.let { gemInfo2Id.putIfAbsent(gemInfo, gemInfo2Id.size) } } return Pair(classInfo2Id, gemInfo2Id) } private fun serializeGemInfos(gemInfo2Id: LinkedHashMap, stream: DataOutput) { stream.writeInt(gemInfo2Id.size) var iter = 0 gemInfo2Id.forEach { assert(iter++ == it.value) it.key.serialize(stream) } } private fun deserializeGemInfo(stream: DataInput): LinkedHashMap { val id2GemInfo = LinkedHashMap() val gemInfoSize = stream.readInt() for (i in 1..gemInfoSize) { val gemInfo = GemInfo(stream)!! id2GemInfo.put(i - 1, gemInfo) } id2GemInfo.put(-1, GemInfo.NONE) return id2GemInfo } private fun serializeClassInfos(stream: DataOutput, classInfo2Id: LinkedHashMap, gemInfo2Id: LinkedHashMap) { gemInfo2Id.put(GemInfo.NONE, -1) var iter = 0 stream.writeInt(classInfo2Id.size) classInfo2Id.forEach { assert(iter++ == it.value) stream.writeUTF(it.key.classFQN) stream.writeInt(gemInfo2Id.getValue(it.key.gemInfo ?: GemInfo.NONE)) } } private fun deserializeClassInfo(stream: DataInput, id2GemInfo: LinkedHashMap): LinkedHashMap { val id2ClassInfo = LinkedHashMap() val classInfoSize = stream.readInt() for (i in 1..classInfoSize) { val fqn = stream.readUTF() val gemInfo = id2GemInfo.getValue(stream.readInt()) id2ClassInfo.put(i - 1, ClassInfo(gemInfo, fqn)) } return id2ClassInfo } private fun serializeSignatureInfos(stream: DataOutput, signatureInfos: List, classInfo2Id: LinkedHashMap) { stream.writeInt(signatureInfos.size) signatureInfos.forEach { val methodInfo = it.methodInfo stream.writeUTF(methodInfo.name) stream.writeByte(methodInfo.visibility.ordinal) stream.writeBoolean(methodInfo.location != null) methodInfo.location?.serialize(stream) stream.writeInt(classInfo2Id.getValue(methodInfo.classInfo)) it.contract.serialize(stream) } } private fun deserializeSignatureInfos(stream: DataInput, id2ClassInfo: LinkedHashMap) : List { val result = ArrayList() val signatureInfoSize = stream.readInt() for (i in 1..signatureInfoSize) { val name = stream.readUTF() val visibility = RVisibility.values()[stream.readByte().toInt()] val isLocationPresent = stream.readBoolean() val location = if (isLocationPresent) Location(stream) else null val classInfo = id2ClassInfo.getValue(stream.readInt()) val methodInfo = MethodInfo.Impl(classInfo, name, visibility, location) val contract = SignatureContract(stream) result.add(SignatureInfo(methodInfo, contract)) } return result } } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/serialization/RmcDirectory.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature.serialization import org.jetbrains.ruby.codeInsight.types.signature.GemInfo import org.jetbrains.ruby.codeInsight.types.signature.SignatureInfo import java.io.* import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream interface RmcDirectory { fun save(gemInfo: GemInfo, signatures: List) fun listGems() : List fun load(gemInfo: GemInfo): List } class RmcDirectoryImpl(private val directory: File) : RmcDirectory { init { if (!directory.exists() || !directory.isDirectory) { throw IOException("Existing directory excepted") } } override fun load(gemInfo: GemInfo): List { val inputFile = File(directory, gemInfo2Filename(gemInfo)) FileInputStream(inputFile).use { GZIPInputStream(it).use { DataInputStream(it).use { return SignatureInfoSerialization.deserialize(it) } } } } override fun save(gemInfo: GemInfo, signatures: List) { val outputFile = File(directory, gemInfo2Filename(gemInfo)) FileOutputStream(outputFile).use { GZIPOutputStream(it).use { DataOutputStream(it).use { SignatureInfoSerialization.serialize(signatures, it) } } } } override fun listGems(): List = directory.listFiles().mapNotNull { file2GemInfo(it) } private fun gemInfo2Filename(gemInfo: GemInfo) = "${gemInfo.name}-${gemInfo.version}.rmc" private fun file2GemInfo(file: File): GemInfo? { if (file.extension != "rmc") { return null } val name = file.nameWithoutExtension.substringBeforeLast('-') val version = file.nameWithoutExtension.substringAfterLast('-') if (name == "" || version == "") { return null } return GemInfo(name, version) } } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/serialization/SignatureContractSerialization.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature.serialization import org.jetbrains.ruby.codeInsight.types.signature.* import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.ContractTransition import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.ReferenceContractTransition import org.jetbrains.ruby.codeInsight.types.signature.contractTransition.TypedContractTransition import java.io.DataInput import java.io.DataOutput import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap fun ContractTransition.serialize(stream: DataOutput) { stream.writeBoolean(this is ReferenceContractTransition) when (this) { is ReferenceContractTransition -> stream.writeInt(mask) is TypedContractTransition -> stream.writeUTF(type) else -> throw IllegalStateException("ContractTransition should be sealed in these classes") } } fun ContractTransition(stream: DataInput): ContractTransition { val type = stream.readBoolean() return when (type) { true -> ReferenceContractTransition(stream.readInt()) false -> TypedContractTransition(stream.readUTF()) } } fun ParameterInfo.serialize(stream: DataOutput) { stream.writeUTF(name) stream.writeByte(modifier.ordinal) } fun ParameterInfo(stream: DataInput): ParameterInfo { return ParameterInfo(stream.readUTF(), ParameterInfo.Type.values()[stream.readByte().toInt()]) } fun SignatureContract.serialize(stream: DataOutput) { stream.writeInt(argsInfo.size) argsInfo.forEach { it.serialize(stream) } stream.writeInt(nodeCount) val visited = HashMap() val q = ArrayDeque() visited[startNode] = 0 q.push(startNode) while (q.isNotEmpty()) { val v = q.poll() for (it in v.transitions.values) { if (!visited.containsKey(it)) { visited[it] = visited.size q.add(it) } } stream.writeInt(v.transitions.size) v.transitions.forEach { transition, u -> stream.writeInt(visited[u]!!) transition.serialize(stream) } } } fun SignatureContract(stream: DataInput): SignatureContract { val argsSize = stream.readInt() val argsInfo = List(argsSize) { ParameterInfo(stream) } val nodesSize = stream.readInt() val nodes = List(nodesSize) { RSignatureContractNode() } val distance = IntArray(nodesSize, { 0 }) repeat(nodesSize) { currentNodeIndex -> val transitionsN = stream.readInt() repeat(transitionsN) { val toIndex = stream.readInt() distance[toIndex] = distance[currentNodeIndex] + 1 val transition = ContractTransition(stream) // todo replace with constructor (iterate from the end) nodes[currentNodeIndex].addLink(transition, nodes[toIndex]) } } val levels = List(argsSize + 2) { ArrayList() } nodes.indices.forEach { levels[distance[it]].add(nodes[it]) } return RSignatureContract(argsInfo, nodes.first(), nodes.last(), levels) } ================================================ FILE: ruby-call-signature/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/serialization/TestSerialization.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature.serialization import java.io.DataInput import java.io.DataOutput import java.util.* class StringDataOutput : DataOutput { val result = StringBuilder() private var wasNewline = true fun newline() { result.append("\n") wasNewline = true } private fun writeSpace() { if (!wasNewline) { result.append(' ') } wasNewline = false } override fun writeShort(v: Int): Unit = TODO("not implemented") override fun writeLong(v: Long): Unit = TODO("not implemented") override fun writeDouble(v: Double): Unit = TODO("not implemented") override fun writeBytes(s: String?): Unit = TODO("not implemented") override fun writeByte(v: Int) { writeSpace() result.append(v) } override fun writeFloat(v: Float): Unit = TODO("not implemented") override fun write(b: Int): Unit = TODO("not implemented") override fun write(b: ByteArray?): Unit = TODO("not implemented") override fun write(b: ByteArray?, off: Int, len: Int): Unit = TODO("not implemented") override fun writeChars(s: String?): Unit = TODO("not implemented") override fun writeChar(v: Int): Unit = TODO("not implemented") override fun writeBoolean(v: Boolean) { writeSpace() result.append(if (v) '1' else '0') } override fun writeUTF(s: String?) { writeSpace() result.append(s) } override fun writeInt(v: Int) { writeSpace() result.append(v) } } class StringDataInput(s: String) : DataInput { private val scanner = Scanner(s) override fun readFully(b: ByteArray?): Unit = TODO("not implemented") override fun readFully(b: ByteArray?, off: Int, len: Int): Unit = TODO("not implemented") override fun readInt(): Int = scanner.nextInt() override fun readUnsignedShort(): Int = TODO("not implemented") override fun readUnsignedByte(): Int = TODO("not implemented") override fun readUTF(): String = scanner.next() override fun readChar(): Char = TODO("not implemented") override fun readLine(): String = TODO("not implemented") override fun readByte(): Byte = scanner.nextByte() override fun readFloat(): Float = TODO("not implemented") override fun skipBytes(n: Int): Int = TODO("not implemented") override fun readLong(): Long = TODO("not implemented") override fun readDouble(): Double = TODO("not implemented") override fun readBoolean(): Boolean = (scanner.nextInt() == 1) override fun readShort(): Short = TODO("not implemented") } ================================================ FILE: ruby-call-signature/src/test/java/org/jetbrains/ruby/codeInsight/types/signature/GemInfoFromPathTest.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature import junit.framework.TestCase import org.junit.Test class GemInfoFromPathTest : TestCase() { private fun doTest(path: String, gemName: String, gemVersion: String) { assertEquals(GemInfoOrNull(gemName, gemVersion), gemInfoFromFilePathOrNull(path)) } @Test fun testToplevel() { doTest("/home/valich/foo.rb", "", "") } @Test fun testRubyBundled() { doTest("/Users/valich/.rvm/rubies/ruby-2.3.3/lib/ruby/2.3.0/mkmf.rb", "ruby", "2.3.3") } @Test fun testRakeRVM() { doTest("/Users/valich/.rvm/rubies/ruby-2.3.3/lib/ruby/gems/2.3.0/gems/rake-10.4.2/lib/rake.rb", "rake", "10.4.2") } @Test fun testGemNameWithDashes() { doTest("/Users/valich/.rvm/gems/ruby-2.3.3/gems/debase-ruby_core_source-0.9.9/lib/debase/ruby_core_source.rb", "debase-ruby_core_source", "0.9.9") } @Test fun testMacPreinstalledGem() { doTest("/System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/ruby/gems/2.3.0/gems/sqlite3-1.3.11/lib/sqlite3.rb", "sqlite3", "1.3.11") } @Test fun testMacSystemGem() { doTest("/Users/valich/.gem/ruby/2.3.0/gems/activerecord-5.0.1/lib/active_record.rb", "activerecord", "5.0.1") } } ================================================ FILE: ruby-call-signature/src/test/java/org/jetbrains/ruby/codeInsight/types/signature/SignatureContractMergeTest.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature import org.junit.Test class SignatureContractMergeTest : SignatureContractTestBase() { @Test fun testSimpleMerge() { val contract = generateSimpleContract() val testArgs1 = listOf("Int1", "Int2", "Int3") val testArgs2 = listOf("String1", "Int2", "Int3") val testTuple1 = generateRTuple(testArgs1, "String4") val testTuple2 = generateRTuple(testArgs2, "String4") assertTrue(SignatureContract.accept(contract, testTuple1)) assertFalse(SignatureContract.accept(contract, testTuple2)) checkSerialization(contract, MergeTestData.testSimpleMerge) } @Test fun testComplicatedMerge() { val testArgs1 = listOf("a1", "b2", "a3", "d4") val testArgs2 = listOf("a1", "c2", "b3", "d4") val testTuple1 = generateRTuple(testArgs1, "a5") val testTuple2 = generateRTuple(testArgs2, "a5") val contract = generateComplicatedContract() assertFalse(SignatureContract.accept(contract, testTuple1)) assertTrue(SignatureContract.accept(contract, testTuple2)) checkSerialization(contract, MergeTestData.testComplicatedMerge) } @Test fun testMultipleReturnTypeMerge() { val contract = generateMultipleReturnTypeContract() checkSerialization(contract, MergeTestData.testMultipleReturnTypeMerge) } @Test fun testAdd() { val testArgs1 = listOf("String1", "Date2", "String3") val testTuple1 = generateRTuple(testArgs1, "String4") val contract = generateAddContract() assertTrue(SignatureContract.accept(contract, testTuple1)) checkSerialization(contract, MergeTestData.testAddResult) } } ================================================ FILE: ruby-call-signature/src/test/java/org/jetbrains/ruby/codeInsight/types/signature/SignatureContractSerializationTest.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature import org.jetbrains.ruby.codeInsight.types.signature.serialization.* import org.junit.Test import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.DataInputStream import java.io.DataOutputStream import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream class SignatureContractSerializationTest : SignatureContractTestBase() { private fun checkSignaturesSerialization(signatures: List, newSignatures: List, contractsTestData: List) { assertTrue(signatures.size == newSignatures.size) for (i in 0 until newSignatures.size) { assertTrue(signatures[i].methodInfo == newSignatures[i].methodInfo) checkSerialization(signatures[i].contract, contractsTestData[i % 4]) } } private fun generateSignatures(): Pair, List> { val gems = listOf( GemInfo("gem", "1.2.3"), GemInfo("anothergem", "3.4.5"), GemInfo("supergem", "0.99") ) val classNames = listOf("A::B::C", "B::C::D", "D::E::F") val classes = gems.map { gem -> classNames.map { ClassInfo(gem, it) } }.flatten() val methodNames = listOf("foo", "bar", "baz", "foobar") val methods = classes.map { clazz -> methodNames.map { MethodInfo(clazz, it, RVisibility.PUBLIC) } }.flatten() val contracts = listOf(generateSimpleContract(), generateComplicatedContract(), generateMultipleReturnTypeContract(), generateAddContract()) val contractsTestData = listOf(MergeTestData.testSimpleMerge, MergeTestData.testComplicatedMerge, MergeTestData.testMultipleReturnTypeMerge, MergeTestData.testAddResult) assertTrue(contracts.size == contractsTestData.size) var idx = 0 val signatures = methods.map { SignatureInfo(it, contracts[idx++ % contracts.size]) } return Pair(contractsTestData, signatures) } private fun doTest(contract: String) { val normalizedInput = contract.trim().replace('\n', ' ') val signatureContract = SignatureContract(StringDataInput(normalizedInput)) val serialized = StringDataOutput().let { signatureContract.serialize(it) it.result.toString() } assertEquals(normalizedInput, serialized) } fun testSimple() { doTest(SignatureTestData.simpleContract) } @Test fun testSerializationList() { val (contractsTestData, signatures) = generateSignatures() val dataOutput = StringDataOutput() SignatureInfoSerialization.serialize(signatures, dataOutput) val newSignatures = SignatureInfoSerialization.deserialize(StringDataInput(dataOutput.result.toString())) checkSignaturesSerialization(signatures, newSignatures, contractsTestData) } @Test fun testBinarySerialization() { val (contractsTestData, signatures) = generateSignatures() val outputStream = ByteArrayOutputStream() GZIPOutputStream(outputStream).use { DataOutputStream(outputStream).use { SignatureInfoSerialization.serialize(signatures, it) } } val inputStream = ByteArrayInputStream(outputStream.toByteArray()) GZIPInputStream(inputStream).use { DataInputStream(inputStream).use { val newSignatures = SignatureInfoSerialization.deserialize(it) checkSignaturesSerialization(signatures, newSignatures, contractsTestData) } } } } ================================================ FILE: ruby-call-signature/src/test/java/org/jetbrains/ruby/codeInsight/types/signature/SignatureContractTestBase.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature import junit.framework.TestCase import org.jetbrains.ruby.codeInsight.types.signature.serialization.StringDataOutput import org.jetbrains.ruby.codeInsight.types.signature.serialization.serialize abstract class SignatureContractTestBase : TestCase() { protected fun checkSerialization(rContract: SignatureContract, testData: String) { val serialized = StringDataOutput().let { rContract.serialize(it) it.result.toString() } val testDataClean = testData.trim().replace('\n', ' ') assertEquals(serialized, testDataClean) } protected fun generateComplicatedContract() : RSignatureContract { val args1 = listOf("a1", "c2", "a3", "a4") val args2 = listOf("a1", "b2", "a3", "a4") val args3 = listOf("a1", "c2", "b3", "a4") val args4 = listOf("a1", "b2", "b3", "a4") val args5 = listOf("a1", "c2", "b3", "d4") val args6 = listOf("a1", "b2", "b3", "d4") val tuple1 = generateRTuple(args1, "e5") val tuple2 = generateRTuple(args2, "e5") val tuple3 = generateRTuple(args3, "e5") val tuple4 = generateRTuple(args4, "e5") val tuple5 = generateRTuple(args5, "a5") val tuple6 = generateRTuple(args6, "a5") val contract1 = RSignatureContract(tuple1) contract1.addRTuple(tuple2) contract1.addRTuple(tuple3) contract1.addRTuple(tuple4) contract1.minimize() val contract2 = RSignatureContract(tuple5) contract2.addRTuple(tuple6) contract2.minimize() contract1.mergeWith(contract2) return contract1 } protected fun generateSimpleContract() : RSignatureContract { val args1 = listOf("String1", "String2", "String3") val args2 = listOf("Int1", "String2", "String3") val args3 = listOf("String1", "Int2", "String3") val args4 = listOf("Int1", "Int2", "String3") val args5 = listOf("Int1", "Int2", "Int3") val tuple1 = generateRTuple(args1, "String4") val tuple2 = generateRTuple(args2, "String4") val tuple3 = generateRTuple(args3, "String4") val tuple4 = generateRTuple(args4, "String4") val tuple5 = generateRTuple(args5, "String4") val contract1 = RSignatureContract(tuple1) contract1.addRTuple(tuple2) contract1.addRTuple(tuple3) contract1.addRTuple(tuple4) contract1.minimize() val contract2 = RSignatureContract(tuple5) contract1.mergeWith(contract2) return contract1 } protected fun generateMultipleReturnTypeContract(): RSignatureContract { val args1 = listOf("a1") val args2 = listOf("a1") val args3 = listOf("a1") val tuple1 = generateRTuple(args1, "b2") val tuple2 = generateRTuple(args2, "c2") val tuple3 = generateRTuple(args3, "d2") val contract1 = RSignatureContract(tuple1) contract1.addRTuple(tuple2) contract1.minimize() val contract2 = RSignatureContract(tuple3) contract1.mergeWith(contract2) return contract1 } protected fun generateAddContract(): RSignatureContract { val testArgs1 = listOf("String1", "Date2", "String3") val testTuple1 = generateRTuple(testArgs1, "String4") val args1 = listOf("String1", "String2", "String3") val args2 = listOf("String1", "Int2", "String3") val tuple1 = generateRTuple(args1, "String4") val tuple2 = generateRTuple(args2, "String4") val contract1 = RSignatureContract(tuple1) contract1.addRTuple(tuple2) contract1.minimize() val contract2 = RSignatureContract(testTuple1) contract1.mergeWith(contract2) return contract1 } protected fun generateRTuple(args: List, returnType: String): RTuple { val gemInfo = GemInfo.Impl("test_gem", "1.2.3") val classInfo = ClassInfo.Impl(gemInfo, "TEST1::Fqn") val location = Location("test1test1", 11) val methodInfo = MethodInfo.Impl(classInfo, "met1", RVisibility.PUBLIC, location) val params = args.indices.map { ParameterInfo("a" + it, ParameterInfo.Type.REQ) } return RTuple(methodInfo, params, args, returnType) } object SignatureTestData { val simpleContract = """ 1 arg 0 4 3 1 0 a 2 0 b 2 0 c 1 3 0 d 1 3 1 0 0 """ val trivialContract = """ 0 2 1 1 0 a 0 """ } object MergeTestData { val testAddResult = """ 3 a0 0 a1 0 a2 0 5 1 1 0 String1 3 2 0 Int2 2 0 Date2 2 0 String2 1 3 0 String3 1 4 0 String4 0 """ val testSimpleMerge = """ 3 a0 0 a1 0 a2 0 7 2 1 0 Int1 2 0 String1 2 3 0 Int2 4 0 String2 2 4 0 Int2 4 0 String2 2 5 0 Int3 5 0 String3 1 5 0 String3 1 6 0 String4 0 """ val testComplicatedMerge = """ 4 a0 0 a1 0 a2 0 a3 0 8 1 1 0 a1 2 2 0 b2 2 0 c2 2 3 0 b3 4 0 a3 2 5 0 d4 6 0 a4 1 6 0 a4 1 7 0 a5 1 7 0 e5 0 """ val testMultipleReturnTypeMerge = """ 1 a0 0 3 1 1 0 a1 3 2 0 b2 2 0 d2 2 0 c2 0 """ } } ================================================ FILE: settings.gradle ================================================ include 'ruby-call-signature', 'storage-server-api', 'lambda-update-handler', 'lambda-put-handler' include 'contract-creator' include 'ide-plugin' include 'signature-viewer' include 'state-tracker' include 'common' ================================================ FILE: signature-viewer/build.gradle ================================================ version 'unspecified' apply plugin: 'java' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile project(':ruby-call-signature') compile project(':storage-server-api') // compile 'com.h2database:h2:1.4.193' compile group: 'mysql', name: 'mysql-connector-java', version: '6.0.6' } task runViewer(type: JavaExec) { classpath sourceSets.main.runtimeClasspath main = 'org.jetbrains.ruby.runtime.signature.SignatureViewerKt' } task runExport(type: JavaExec) { if (project.hasProperty("outputDir")) { args = ["$outputDir"] } else { args = ["rmcOutput"] } classpath sourceSets.main.runtimeClasspath main = 'org.jetbrains.ruby.runtime.signature.SignatureExportKt' } task runImport(type: JavaExec) { if (project.hasProperty("inputDir")) { args = ["$inputDir"] } else { args = ["rmcInput"] } classpath sourceSets.main.runtimeClasspath main = 'org.jetbrains.ruby.runtime.signature.SignatureImportKt' } sourceSets { main.java.srcDirs = ['src'] } ================================================ FILE: signature-viewer/src/org/jetbrains/ruby/runtime/signature/DBViewer.kt ================================================ package org.jetbrains.ruby.runtime.signature import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoRow import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoTable /** * Just prints content of [CallInfoTable] */ fun main(args: Array) { val dpPath = parseDBViewerCommandLineArgs(args) DatabaseProvider.connectToDB(dpPath) transaction { val table = CallInfoRow.all().map { it.copy() } table.forEach { println("" + (it.methodInfo.classInfo.gemInfo?.name ?: "No gem") + " " + (it.methodInfo.classInfo.gemInfo?.version ?: "No version") + " " + it.methodInfo.location?.path + " " + it.methodInfo.location?.lineno + " " + it.methodInfo.visibility + " " + it.methodInfo.classInfo.classFQN + " " + it.methodInfo.name + " " + "args:${it.unnamedArgumentsTypesJoinToRawString()} " + "return:${it.returnType}") } println("Size: ${table.size}") } } fun parseDBViewerCommandLineArgs(args: Array): String { if (args.size != 1) { println("Usage: ") System.exit(1) } return args.single() } ================================================ FILE: signature-viewer/src/org/jetbrains/ruby/runtime/signature/EraseLocation.kt ================================================ package org.jetbrains.ruby.runtime.signature import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.impl.Location import org.jetbrains.ruby.codeInsight.types.storage.server.impl.MethodInfo import org.jetbrains.ruby.codeInsight.types.storage.server.impl.MethodInfoTable /** * Erases location. This is needed because annotated [MethodInfo]s shouldn't contain any info related to how * machine of developer who annotated some lib is configured. But [Location] contains home dir, * .rvm or .rbenv folder, e.t.c so it's needed to be erased for annotated libs. */ fun main(args: Array) { val dpPath = parseDBViewerCommandLineArgs(args) DatabaseProvider.connectToDB(dpPath) transaction { // This is updateAll MethodInfoTable.update { it[MethodInfoTable.locationFile] = null } } } ================================================ FILE: signature-viewer/src/org/jetbrains/ruby/runtime/signature/SignatureExport.kt ================================================ package org.jetbrains.ruby.runtime.signature import org.jetbrains.ruby.codeInsight.types.signature.SignatureInfo import org.jetbrains.ruby.codeInsight.types.signature.serialization.RmcDirectoryImpl import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.StorageException import org.jetbrains.ruby.codeInsight.types.storage.server.impl.RSignatureProviderImpl import java.io.File import java.nio.file.Paths const val DB_NAME = "ruby-type-inference-db" + DatabaseProvider.H2_DB_FILE_EXTENSION fun main(arg : Array) { val outputDirPath = parseCommandLine(arg) DatabaseProvider.connectToDB(Paths.get(outputDirPath, DB_NAME).toString()) val outputDir = File(outputDirPath) if (!outputDir.exists()) { outputDir.mkdirs() } val rmcDirectory = RmcDirectoryImpl(outputDir) try { for (gem in RSignatureProviderImpl.registeredGems) { val signatureInfos = ArrayList() for (clazz in RSignatureProviderImpl.getRegisteredClasses(gem)) { RSignatureProviderImpl.getRegisteredMethods(clazz).mapNotNullTo(signatureInfos) { RSignatureProviderImpl.getSignature(it) } } rmcDirectory.save(gem, signatureInfos) } } catch (e: StorageException) { e.printStackTrace() } } fun parseCommandLine(arg: Array): String { if (arg.size != 1) { println("Usage: ") System.exit(-1) } return arg[0] } ================================================ FILE: signature-viewer/src/org/jetbrains/ruby/runtime/signature/SignatureImport.kt ================================================ package org.jetbrains.ruby.runtime.signature import org.jetbrains.ruby.codeInsight.types.signature.serialization.RmcDirectoryImpl import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.StorageException import org.jetbrains.ruby.codeInsight.types.storage.server.impl.RSignatureProviderImpl import java.io.File import java.nio.file.Paths fun main(arg : Array) { val outputDirPath = parseCommandLine(arg) DatabaseProvider.connectToDB(Paths.get(outputDirPath, DB_NAME).toString()) val inputDirectory = File(outputDirPath) if (!inputDirectory.exists()) { inputDirectory.mkdirs() } val rmcDirectory = RmcDirectoryImpl(inputDirectory) try { rmcDirectory.listGems() .map { rmcDirectory.load(it) } .forEach { signatureInfos -> signatureInfos.forEach { RSignatureProviderImpl.putSignature(it) } } } catch (e: StorageException) { e.printStackTrace() } } ================================================ FILE: signature-viewer/src/org/jetbrains/ruby/runtime/signature/SignatureViewer.kt ================================================ package org.jetbrains.ruby.runtime.signature import org.jetbrains.ruby.codeInsight.types.signature.SignatureContract import org.jetbrains.ruby.codeInsight.types.signature.SignatureInfo import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.StorageException import org.jetbrains.ruby.codeInsight.types.storage.server.impl.RSignatureProviderImpl import java.nio.file.Paths fun dumpSignatureInfo(signatureInfo: SignatureInfo) { val methodInfo = signatureInfo.methodInfo val classInfo = methodInfo.classInfo val gemInfo = classInfo.gemInfo!! println(gemInfo.name + " " + gemInfo.version + " " + classInfo.classFQN) print(methodInfo.name + " ") methodInfo.location?.let { print( it.path + " " + it.lineno) } SignatureContract.getAllReturnTypes(signatureInfo.contract).forEach { print(it + ", ") } println() } fun main(args : Array) { val outputDirPath = parseCommandLine(args) DatabaseProvider.connectToDB(Paths.get(outputDirPath, DB_NAME).toString()) val map = HashMap>() try { for (gem in RSignatureProviderImpl.registeredGems) { for (clazz in RSignatureProviderImpl.getRegisteredClasses(gem)) { for (method in RSignatureProviderImpl.getRegisteredMethods(clazz)) { val signatureInfo = RSignatureProviderImpl.getSignature(method) signatureInfo?.let { var list = map[method.name] if (list == null) { list = ArrayList() list.add(signatureInfo) map.put(method.name, list) } else { list.add(signatureInfo) } } } } } println("All loaded") while (true) { val line = readLine() map[line]?.let { it.forEach { dumpSignatureInfo(it)} } } } catch (e: StorageException) { e.printStackTrace() } } ================================================ FILE: signature-viewer/src/org/jetbrains/ruby/runtime/signature/SplitDB.kt ================================================ package org.jetbrains.ruby.runtime.signature import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.ruby.codeInsight.types.signature.GemInfo import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoRow import org.jetbrains.ruby.codeInsight.types.storage.server.impl.CallInfoTable import java.nio.file.Paths val gemToDBMap = HashMap() fun gemToDB(info: GemInfo?, outputDir: String, rubyVersion: String): Database = gemToDBMap[info] ?: DatabaseProvider.connectToDB(Paths.get(outputDir, "${info?.name?.plus("-")?.plus(info.version) ?: "no_gem"}-ruby-$rubyVersion" + DatabaseProvider.H2_DB_FILE_EXTENSION).toString()) .also { gemToDBMap[info] = it } fun input(msg: String): String { println(msg) return readLine()!! } /** * This small script splits massive database into small databases. Each * small database is responsible for particular gem and named accordingly */ fun main(args: Array) { val dpPath = parseDBViewerCommandLineArgs(args) val input = DatabaseProvider.connectToDB(dpPath) val outputDir = input("Enter output dir: ") val rubyVersion = input("Enter ruby version: ") transaction(input) { CallInfoRow.all().forEach { val callInfo = it.copy() transaction(gemToDB(callInfo.methodInfo.classInfo.gemInfo, outputDir, rubyVersion)) { CallInfoTable.insertInfoIfNotContains(callInfo) } } } } ================================================ FILE: state-tracker/build.gradle ================================================ buildscript { ext.kotlin_version = '1.2.70' repositories { mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } version 'unspecified' apply plugin: 'java' apply plugin: 'kotlin' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testCompile group: 'junit', name: 'junit', version: '4.12' } compileKotlin { kotlinOptions.jvmTarget = "1.8" } compileTestKotlin { kotlinOptions.jvmTarget = "1.8" } sourceSets { main.java.srcDirs = ['src/main/java'] test.java.srcDirs = ['src/test/java'] test.resources.srcDirs=['src/test/java/testData'] } ================================================ FILE: state-tracker/src/main/java/org/jetbrains/ruby/stateTracker/RubyClassHierarchy.kt ================================================ package org.jetbrains.ruby.stateTracker interface RubyClassHierarchy { val loadPaths: List val topLevelConstants: Map fun getRubyModule(fqn: String) : RubyModule? class Impl(override val loadPaths: List, rubyModules: List, override val topLevelConstants: Map) : RubyClassHierarchy { private val name2modules = rubyModules.associateBy( {it.name} , {it}) override fun getRubyModule(fqn: String): RubyModule? { return name2modules[fqn] } } } interface RubyConstant { val name: String val type: String val extended: List data class Impl(override val name: String, override val type: String, override val extended: List) : RubyConstant } interface RubyModule { val name: String val classDirectAncestors: List val instanceDirectAncestors: List val classMethods: List val instanceMethods: List class Impl(override val name: String, override val classDirectAncestors: List, override val instanceDirectAncestors: List, override val classMethods: List, override val instanceMethods: List) : RubyModule } interface RubyClass: RubyModule { val superClass : RubyClass class Impl(override val name: String, override val classDirectAncestors: List, override val instanceDirectAncestors: List, override val classMethods: List, override val instanceMethods: List, override val superClass: RubyClass) : RubyClass companion object : RubyClass { val EMPTY = this override val name: String get() = "" override val classDirectAncestors: List get() = emptyList() override val instanceDirectAncestors: List get() = emptyList() override val classMethods: List get() = emptyList() override val instanceMethods: List get() = emptyList() override val superClass: RubyClass get() = this } } interface RubyMethod { val name: String val location: Location? val arguments: List data class ArgInfo(val kind: ArgumentKind, val name: String) class Impl(override val name: String, override val location: Location?, override val arguments: List) : RubyMethod enum class ArgumentKind { REQ, OPT, REST, KEY, KEY_REST, KEY_REQ, BLOCK; companion object { fun fromString(name : String): ArgumentKind { return when (name) { "req" -> REQ "opt" -> OPT "rest" -> REST "key" -> KEY "keyrest" -> KEY_REST "keyreq" -> KEY_REQ "block" -> BLOCK else -> throw IllegalArgumentException(name) } } } } } interface Location { val path: String val lineNo: Int data class Impl(override val path: String, override val lineNo: Int) : Location } ================================================ FILE: state-tracker/src/main/java/org/jetbrains/ruby/stateTracker/RubyClassHierarchyLoader.kt ================================================ package org.jetbrains.ruby.stateTracker import com.google.gson.Gson import java.util.* import kotlin.collections.ArrayList object RubyClassHierarchyLoader { private val gson = Gson() fun mergeJsons(jsons: List): String { return gson.toJson(jsons.map { gson.fromJson(it, Root::class.java) }.reduce { a, b -> joinRoots(a, b) }) } fun fromJson(json: String): RubyClassHierarchy { val root = gson.fromJson(json, Root::class.java) return RubyClassHierarchy.Impl(root.load_path, MapHelper(TopsortHelper(root.modules).topsort()).map(), root.top_level_constants.associate { Pair(it.name, RubyConstant.Impl( it.name, it.class_name, it.extended )) }) } private fun joinRoots(one: Root, another: Root) : Root { return Root( joinTopLevelConstants(one.top_level_constants, another.top_level_constants), joinLoadPath(one.load_path, another.load_path), joinModules(one.modules, another.modules) ) } private fun joinTopLevelConstants(one: List, another: List): List { return one.union(another).associateBy(TopLevelConstant::name).values.toList() } private fun joinLoadPath(one: List, another: List): List = one.union(another).toList() private fun joinModules(one: List, another: List): List { return one.union(another).associateBy(Module::name).values.toList() } private data class Method(var name: String, var path: String?, var line: String?, var parameters: List>) private data class Module(var name: String, var type: String, var superclass: String?, var singleton_class_ancestors: List?, var ancestors: List?, var class_methods: List, var instance_methods: List) private data class TopLevelConstant(var name: String, var class_name: String, var extended: List) private data class Root(var top_level_constants: List, var load_path: List, var modules: List) private class MapHelper(val topsortedList: List) { private val name2RubyModule = HashMap() private val name2Module = topsortedList.associateBy { it.name } fun map() : List = topsortedList.map { toRubyModule(it) } private fun toRubyModule(module: Module): RubyModule { val rubyModule = when (module.type) { "Module" -> createModule(module) "Class" -> createClass(module) //TODO some module/class objects can have type derived from Module/Class. //For example: RSpec::Rails::AssertionDelegator //lets have a look at superclass else -> if (module.superclass.isNullOrBlank()) createModule(module) else createClass(module) } name2RubyModule[rubyModule.name] = rubyModule return rubyModule } private fun createClass(module: Module): RubyClass.Impl { return RubyClass.Impl(module.name, toRubyModules(removeAllNonDirectAncestors(module) { m -> m.singleton_class_ancestors }), toRubyModules(removeAllNonDirectAncestors(module) { m -> m.ancestors }), toRubyMethods(module.class_methods), toRubyMethods(module.instance_methods), (name2RubyModule[module.superclass] ?: RubyClass.EMPTY) as RubyClass) } private fun createModule(module: Module): RubyModule.Impl { return RubyModule.Impl(module.name, toRubyModules(removeAllNonDirectAncestors(module) { m -> m.singleton_class_ancestors }), toRubyModules(removeAllNonDirectAncestors(module) { m -> m.ancestors }), toRubyMethods(module.class_methods), toRubyMethods(module.instance_methods)) } private fun toRubyModules(names: List): List = names.mapNotNull { name2RubyModule[it] } private fun toRubyMethods(methods: List): List = methods.map { RubyMethod.Impl( it.name, if (it.path != null && it.line != null) { Location.Impl(it.path!!, it.line!!.toInt()) } else null, it.parameters.map { RubyMethod.ArgInfo(RubyMethod.ArgumentKind.fromString(it[0]), if (it.size == 2) it[1] else "") }) } /** * Removes all non direct ancestors. * * @param ancestorGetter the rule which by given [Module] gives it ancestor (for example it * may return [Module.ancestors] or [Module.singleton_class_ancestors] by given [Module]) * @return [List] where all non direct ancestors are excluded */ private fun removeAllNonDirectAncestors(module: Module, ancestorGetter: (Module) -> (List?)): List { val toRemove = HashSet() ancestorGetter(module)?.forEach { val firstLevelAncestor: String = it if (firstLevelAncestor != module.name && !toRemove.contains(firstLevelAncestor)) { name2Module[firstLevelAncestor]?.let { ancestorGetter(it)?.forEach { val secondLevelAncestor: String = it if (secondLevelAncestor != firstLevelAncestor) toRemove.add(secondLevelAncestor) } } } } return ancestorGetter(module)?.filter { !toRemove.contains(it) } ?: listOf() } } private class TopsortHelper(val modules: List) { private val visited = HashSet() private val result = ArrayList() private val name2Module = modules.associateBy { it.name } fun topsort(): List { visited.add("") modules.forEach { tryVisit(it.name) } return result } private fun dfs(module: Module) { tryVisit(module.superclass) module.ancestors?.forEach {tryVisit(it)} module.singleton_class_ancestors?.forEach {tryVisit(it)} result.add(module) } private fun tryVisit(name: String?) { if (name == null) return if (visited.add(name)) { name2Module[name]?.let { dfs(it) } } } } } ================================================ FILE: state-tracker/src/test/java/org/jetbrains/ruby/stateTracker/RubyClassHierarchyLoaderNonStandardModuleTypeTest.kt ================================================ package org.jetbrains.ruby.stateTracker import junit.framework.TestCase import org.junit.Test class RubyClassHierarchyLoaderNonStandardModuleTypeTest : TestCase() { private var classHierarchy : RubyClassHierarchy? = null override fun setUp() { val inputStream = javaClass.classLoader.getResourceAsStream("non-standard-module-type.json") val inputString = inputStream.bufferedReader().use { it.readText() } classHierarchy = RubyClassHierarchyLoader.fromJson(inputString) } @Test fun testHierarchyLoaded() { assertNotNull(classHierarchy) classHierarchy?.let { assertNotNull(it.getRubyModule("AAAA")) assertNotNull(it.getRubyModule("BBBB")) } } } ================================================ FILE: state-tracker/src/test/java/org/jetbrains/ruby/stateTracker/RubyClassHierarchyLoaderTest.kt ================================================ package org.jetbrains.ruby.stateTracker import junit.framework.TestCase import org.junit.Test class RubyClassHierarchyLoaderTest : TestCase() { private var classHierarchy : RubyClassHierarchy? = null override fun setUp() { val inputStream = javaClass.classLoader.getResourceAsStream("classes.json") val inputString = inputStream.bufferedReader().use { it.readText() } classHierarchy = RubyClassHierarchyLoader.fromJson(inputString) } @Test fun testHasBasicObject() { assertNotNull(classHierarchy) classHierarchy?.let { assertNotNull(it.getRubyModule("BasicObject")) } } @Test fun testIncluded() { assertNotNull(classHierarchy) classHierarchy?.let { val module = it.getRubyModule("Gem::Resolver::Molinillo::Resolver::Resolution") assertNotNull(module) assertTrue(module!!.instanceDirectAncestors.any {it.name == "Kernel"}) assertTrue(module.instanceDirectAncestors.any {it.name == "Gem::Resolver::Molinillo::Delegates::ResolutionState"}) } } @Test fun testAllNonDirectAncestorsAreExcluded() { assertNotNull(classHierarchy) classHierarchy?.let { val module = it.getRubyModule("CGI") assertNotNull(module) assertTrue(module!!.classDirectAncestors.none {it.name == "Kernel"}) assertTrue(module.classDirectAncestors.any {it.name == "CGI::Util"}) } } @Test fun testSuperClass() { assertNotNull(classHierarchy) classHierarchy?.let { val module = it.getRubyModule("Timeout::Error") as RubyClass assertTrue(module.superClass.name == "RuntimeError") } } @Test fun testHasMethod() { assertNotNull(classHierarchy) classHierarchy?.let { val module = it.getRubyModule("Timeout::Error") as RubyClass val expectedLocation = Location.Impl("/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/timeout.rb", 28) assertTrue(module.instanceMethods.any{it.name == "thread" && it.location == expectedLocation }) } } @Test fun testConstants() { assertNotNull(classHierarchy) classHierarchy?.let { val elem = it.topLevelConstants["STDIN"] assertNotNull(elem) assertTrue(elem!!.extended.isEmpty()) assertTrue(elem.name == "STDIN") assertTrue(elem.type == "IO") } } @Test fun testParameters() { assertNotNull(classHierarchy) classHierarchy?.let { val module = it.getRubyModule("Dir::Tmpname")!! assertTrue(module.classMethods.any { it.name == "create" && it.arguments.any { it.kind == RubyMethod.ArgumentKind.KEY_REST } && it.arguments.any { it.kind == RubyMethod.ArgumentKind.OPT } && it.arguments.any { it.kind == RubyMethod.ArgumentKind.KEY } && it.arguments.any { it.kind == RubyMethod.ArgumentKind.REQ } }) } } } ================================================ FILE: state-tracker/src/test/java/testData/classes.json ================================================ { "top_level_constants": [ { "class_name": "IO", "extended": [], "name": "STDIN" } ], "modules": [ { "name": "Gem::Resolver::Molinillo::Resolver::Resolution", "type": "Class", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "Gem::Resolver::Molinillo::Resolver::Resolution", "Gem::Resolver::Molinillo::Delegates::SpecificationProvider", "Gem::Resolver::Molinillo::Delegates::ResolutionState", "RequireAll", "Kernel" ], "class_methods": [ ], "instance_methods": [ { "name": "base", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/resolution.rb", "line": 37 }, { "name": "resolve", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/resolution.rb", "line": 63 }, { "name": "specification_provider", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/resolution.rb", "line": 29 }, { "name": "resolver_ui", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/resolution.rb", "line": 33 }, { "name": "original_requested", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/resolution.rb", "line": 40 }, { "name": "iteration_rate=", "parameters": [ [ "req" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/resolution.rb", "line": 88 }, { "name": "started_at=", "parameters": [ [ "req" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/resolution.rb", "line": 92 }, { "name": "states=", "parameters": [ [ "req" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/resolution.rb", "line": 96 } ], "superclass": "Object" }, { "name": "CGI", "type": "Class", "singleton_class_ancestors": [ "CGI::Escape", "CGI::Util", "RequireAll", "Kernel" ], "ancestors": [ "CGI", "RequireAll", "Kernel" ], "class_methods": [ { "name": "parse", "parameters": [ [ "req", "query" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/core.rb", "line": 375 }, { "name": "accept_charset", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/core.rb", "line": 740 }, { "name": "accept_charset=", "parameters": [ [ "req", "accept_charset" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/core.rb", "line": 745 } ], "instance_methods": [ { "name": "print", "parameters": [ [ "rest", "options" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/core.rb", "line": 365 }, { "name": "out", "parameters": [ [ "opt", "options" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/core.rb", "line": 349 }, { "name": "header", "parameters": [ [ "opt", "options" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/core.rb", "line": 153 }, { "name": "http_header", "parameters": [ [ "opt", "options" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/core.rb", "line": 153 }, { "name": "nph?", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/core.rb", "line": 256 }, { "name": "accept_charset", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/core.rb", "line": 750 } ], "superclass": "Object" }, { "name": "Timeout::Error", "type": "Class", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "Timeout::Error", "RequireAll", "Kernel" ], "class_methods": [ { "name": "catch", "parameters": [ [ "rest", "args" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/timeout.rb", "line": 30 } ], "instance_methods": [ { "name": "exception", "parameters": [ [ "rest" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/timeout.rb", "line": 36 }, { "name": "thread", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/timeout.rb", "line": 28 } ], "superclass": "RuntimeError" }, { "name": "Dir::Tmpname", "type": "Module", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "Dir::Tmpname" ], "class_methods": [ { "name": "create", "parameters": [ [ "req", "basename" ], [ "opt", "tmpdir" ], [ "key", "max_try" ], [ "keyrest", "opts" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/tmpdir.rb", "line": 121 }, { "name": "tmpdir", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/tmpdir.rb", "line": 105 }, { "name": "make_tmpname", "parameters": [ [ "req" ], [ "req", "n" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/tmpdir.rb", "line": 109 } ], "instance_methods": [ ] }, { "name": "RequireAll", "type": "Module", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "RequireAll" ], "class_methods": [ ], "instance_methods": [ { "name": "require_all", "parameters": [ [ "rest", "args" ] ], "path": "/Users/vkkoshelev/.rvm/gems/ruby-2.4.1/gems/require_all-1.4.0/lib/require_all.rb", "line": 34 }, { "name": "require_rel", "parameters": [ [ "rest", "paths" ] ], "path": "/Users/vkkoshelev/.rvm/gems/ruby-2.4.1/gems/require_all-1.4.0/lib/require_all.rb", "line": 146 }, { "name": "load_all", "parameters": [ [ "rest", "paths" ] ], "path": "/Users/vkkoshelev/.rvm/gems/ruby-2.4.1/gems/require_all-1.4.0/lib/require_all.rb", "line": 158 }, { "name": "load_rel", "parameters": [ [ "rest", "paths" ] ], "path": "/Users/vkkoshelev/.rvm/gems/ruby-2.4.1/gems/require_all-1.4.0/lib/require_all.rb", "line": 164 }, { "name": "autoload_all", "parameters": [ [ "rest", "paths" ] ], "path": "/Users/vkkoshelev/.rvm/gems/ruby-2.4.1/gems/require_all-1.4.0/lib/require_all.rb", "line": 210 }, { "name": "autoload_rel", "parameters": [ [ "rest", "paths" ] ], "path": "/Users/vkkoshelev/.rvm/gems/ruby-2.4.1/gems/require_all-1.4.0/lib/require_all.rb", "line": 224 } ] }, { "name": "Kernel", "type": "Module", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "Kernel" ], "class_methods": [ { "name": "`", "parameters": [ [ "req" ] ] }, { "name": "gets", "parameters": [ [ "rest" ] ] }, { "name": "proc", "parameters": [ ] }, { "name": "lambda", "parameters": [ ] }, { "name": "sprintf", "parameters": [ [ "rest" ] ] }, { "name": "format", "parameters": [ [ "rest" ] ] }, { "name": "Integer", "parameters": [ [ "rest" ] ] }, { "name": "Float", "parameters": [ [ "req" ] ] }, { "name": "String", "parameters": [ [ "req" ] ] }, { "name": "Array", "parameters": [ [ "req" ] ] }, { "name": "Hash", "parameters": [ [ "req" ] ] }, { "name": "select", "parameters": [ [ "rest" ] ] }, { "name": "local_variables", "parameters": [ ] }, { "name": "warn", "parameters": [ [ "rest" ] ] }, { "name": "raise", "parameters": [ [ "rest" ] ] }, { "name": "fail", "parameters": [ [ "rest" ] ] }, { "name": "global_variables", "parameters": [ ] }, { "name": "__method__", "parameters": [ ] }, { "name": "__callee__", "parameters": [ ] }, { "name": "__dir__", "parameters": [ ] }, { "name": "eval", "parameters": [ [ "rest" ] ] }, { "name": "iterator?", "parameters": [ ] }, { "name": "block_given?", "parameters": [ ] }, { "name": "catch", "parameters": [ [ "rest" ] ] }, { "name": "throw", "parameters": [ [ "rest" ] ] }, { "name": "loop", "parameters": [ ] }, { "name": "trace_var", "parameters": [ [ "rest" ] ] }, { "name": "untrace_var", "parameters": [ [ "rest" ] ] }, { "name": "at_exit", "parameters": [ ] }, { "name": "load", "parameters": [ [ "rest" ] ] }, { "name": "syscall", "parameters": [ [ "rest" ] ] }, { "name": "open", "parameters": [ [ "rest" ] ] }, { "name": "printf", "parameters": [ [ "rest" ] ] }, { "name": "print", "parameters": [ [ "rest" ] ] }, { "name": "putc", "parameters": [ [ "req" ] ] }, { "name": "puts", "parameters": [ [ "rest" ] ] }, { "name": "readline", "parameters": [ [ "rest" ] ] }, { "name": "readlines", "parameters": [ [ "rest" ] ] }, { "name": "p", "parameters": [ [ "rest" ] ] }, { "name": "test", "parameters": [ [ "rest" ] ] }, { "name": "rand", "parameters": [ [ "rest" ] ] }, { "name": "srand", "parameters": [ [ "rest" ] ] }, { "name": "trap", "parameters": [ [ "rest" ] ] }, { "name": "require", "parameters": [ [ "req" ] ] }, { "name": "require_relative", "parameters": [ [ "req" ] ] }, { "name": "autoload", "parameters": [ [ "req" ], [ "req" ] ] }, { "name": "autoload?", "parameters": [ [ "req" ] ] }, { "name": "binding", "parameters": [ ] }, { "name": "caller", "parameters": [ [ "rest" ] ] }, { "name": "caller_locations", "parameters": [ [ "rest" ] ] }, { "name": "fork", "parameters": [ ] }, { "name": "exit", "parameters": [ [ "rest" ] ] }, { "name": "sleep", "parameters": [ [ "rest" ] ] }, { "name": "exec", "parameters": [ [ "rest" ] ] }, { "name": "exit!", "parameters": [ [ "rest" ] ] }, { "name": "system", "parameters": [ [ "rest" ] ] }, { "name": "spawn", "parameters": [ [ "rest" ] ] }, { "name": "abort", "parameters": [ [ "rest" ] ] }, { "name": "Rational", "parameters": [ [ "rest" ] ] }, { "name": "Complex", "parameters": [ [ "rest" ] ] }, { "name": "set_trace_func", "parameters": [ [ "req" ] ] }, { "name": "URI", "parameters": [ [ "req", "uri" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/uri/common.rb", "line": 733 } ], "instance_methods": [ { "name": "instance_of?", "parameters": [ [ "req" ] ] }, { "name": "kind_of?", "parameters": [ [ "req" ] ] }, { "name": "is_a?", "parameters": [ [ "req" ] ] }, { "name": "tap", "parameters": [ ] }, { "name": "public_send", "parameters": [ [ "rest" ] ] }, { "name": "method", "parameters": [ [ "req" ] ] }, { "name": "public_method", "parameters": [ [ "req" ] ] }, { "name": "singleton_method", "parameters": [ [ "req" ] ] }, { "name": "remove_instance_variable", "parameters": [ [ "req" ] ] }, { "name": "define_singleton_method", "parameters": [ [ "rest" ] ] }, { "name": "instance_variable_set", "parameters": [ [ "req" ], [ "req" ] ] }, { "name": "extend", "parameters": [ [ "rest" ] ] }, { "name": "to_enum", "parameters": [ [ "rest" ] ] }, { "name": "enum_for", "parameters": [ [ "rest" ] ] }, { "name": "<=>", "parameters": [ [ "req" ] ] }, { "name": "===", "parameters": [ [ "req" ] ] }, { "name": "=~", "parameters": [ [ "req" ] ] }, { "name": "!~", "parameters": [ [ "req" ] ] }, { "name": "eql?", "parameters": [ [ "req" ] ] }, { "name": "respond_to?", "parameters": [ [ "rest" ] ] }, { "name": "freeze", "parameters": [ ] }, { "name": "inspect", "parameters": [ ] }, { "name": "object_id", "parameters": [ ] }, { "name": "send", "parameters": [ [ "rest" ] ] }, { "name": "display", "parameters": [ [ "rest" ] ] }, { "name": "to_s", "parameters": [ ] }, { "name": "nil?", "parameters": [ ] }, { "name": "hash", "parameters": [ ] }, { "name": "class", "parameters": [ ] }, { "name": "singleton_class", "parameters": [ ] }, { "name": "clone", "parameters": [ [ "rest" ] ] }, { "name": "dup", "parameters": [ ] }, { "name": "itself", "parameters": [ ] }, { "name": "taint", "parameters": [ ] }, { "name": "tainted?", "parameters": [ ] }, { "name": "untaint", "parameters": [ ] }, { "name": "untrust", "parameters": [ ] }, { "name": "untrusted?", "parameters": [ ] }, { "name": "trust", "parameters": [ ] }, { "name": "frozen?", "parameters": [ ] }, { "name": "methods", "parameters": [ [ "rest" ] ] }, { "name": "singleton_methods", "parameters": [ [ "rest" ] ] }, { "name": "protected_methods", "parameters": [ [ "rest" ] ] }, { "name": "private_methods", "parameters": [ [ "rest" ] ] }, { "name": "public_methods", "parameters": [ [ "rest" ] ] }, { "name": "instance_variable_get", "parameters": [ [ "req" ] ] }, { "name": "instance_variables", "parameters": [ ] }, { "name": "instance_variable_defined?", "parameters": [ [ "req" ] ] } ] }, { "name": "Gem::Resolver::Molinillo::Delegates::SpecificationProvider", "type": "Module", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "Gem::Resolver::Molinillo::Delegates::SpecificationProvider" ], "class_methods": [ ], "instance_methods": [ { "name": "search_for", "parameters": [ [ "req", "dependency" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/specification_provider.rb", "line": 8 }, { "name": "dependencies_for", "parameters": [ [ "req", "specification" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/specification_provider.rb", "line": 15 }, { "name": "requirement_satisfied_by?", "parameters": [ [ "req", "requirement" ], [ "req", "activated" ], [ "req", "spec" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/specification_provider.rb", "line": 22 }, { "name": "name_for", "parameters": [ [ "req", "dependency" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/specification_provider.rb", "line": 29 }, { "name": "allow_missing?", "parameters": [ [ "req", "dependency" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/specification_provider.rb", "line": 57 }, { "name": "sort_dependencies", "parameters": [ [ "req", "dependencies" ], [ "req", "activated" ], [ "req", "conflicts" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/specification_provider.rb", "line": 50 }, { "name": "name_for_explicit_dependency_source", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/specification_provider.rb", "line": 36 }, { "name": "name_for_locking_dependency_source", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/specification_provider.rb", "line": 43 } ] }, { "name": "Gem::Resolver::Molinillo::Delegates::ResolutionState", "type": "Module", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "Gem::Resolver::Molinillo::Delegates::ResolutionState" ], "class_methods": [ ], "instance_methods": [ { "name": "name", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/resolution_state.rb", "line": 8 }, { "name": "requirements", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/resolution_state.rb", "line": 14 }, { "name": "requirement", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/resolution_state.rb", "line": 26 }, { "name": "conflicts", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/resolution_state.rb", "line": 44 }, { "name": "activated", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/resolution_state.rb", "line": 20 }, { "name": "depth", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/resolution_state.rb", "line": 38 }, { "name": "possibilities", "parameters": [ ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/rubygems/resolver/molinillo/lib/molinillo/delegates/resolution_state.rb", "line": 32 } ] }, { "name": "Object", "type": "Class", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "Object", "RequireAll", "Kernel" ], "class_methods": [ ], "instance_methods": [ ], "superclass": "BasicObject" }, { "name": "CGI::Escape", "type": "Module", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "CGI::Escape" ], "class_methods": [ ], "instance_methods": [ { "name": "escape", "parameters": [ [ "req" ] ] }, { "name": "unescape", "parameters": [ [ "rest" ] ] }, { "name": "escapeHTML", "parameters": [ [ "req" ] ] }, { "name": "unescapeHTML", "parameters": [ [ "req" ] ] } ] }, { "name": "CGI::Util", "type": "Module", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "CGI::Util", "CGI::Escape" ], "class_methods": [ ], "instance_methods": [ { "name": "escape", "parameters": [ [ "req" ] ] }, { "name": "h", "parameters": [ [ "req" ] ] }, { "name": "unescape", "parameters": [ [ "rest" ] ] }, { "name": "rfc1123_date", "parameters": [ [ "req", "time" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/util.rb", "line": 183 }, { "name": "escapeHTML", "parameters": [ [ "req" ] ] }, { "name": "unescapeHTML", "parameters": [ [ "req" ] ] }, { "name": "escape_html", "parameters": [ [ "req" ] ] }, { "name": "unescape_html", "parameters": [ [ "req" ] ] }, { "name": "escapeElement", "parameters": [ [ "req", "string" ], [ "rest", "elements" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/util.rb", "line": 136 }, { "name": "unescapeElement", "parameters": [ [ "req", "string" ], [ "rest", "elements" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/util.rb", "line": 156 }, { "name": "escape_element", "parameters": [ [ "req", "string" ], [ "rest", "elements" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/util.rb", "line": 136 }, { "name": "unescape_element", "parameters": [ [ "req", "string" ], [ "rest", "elements" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/util.rb", "line": 156 }, { "name": "pretty", "parameters": [ [ "req", "string" ], [ "opt", "shift" ] ], "path": "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/cgi/util.rb", "line": 207 } ] }, { "name": "RuntimeError", "type": "Class", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "RuntimeError", "RequireAll", "Kernel" ], "class_methods": [ ], "instance_methods": [ ], "superclass": "StandardError" }, { "name": "BasicObject", "type": "Class", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "BasicObject" ], "class_methods": [ ], "instance_methods": [ { "name": "!", "parameters": [ ] }, { "name": "==", "parameters": [ [ "req" ] ] }, { "name": "!=", "parameters": [ [ "req" ] ] }, { "name": "__send__", "parameters": [ [ "rest" ] ] }, { "name": "equal?", "parameters": [ [ "req" ] ] }, { "name": "instance_eval", "parameters": [ [ "rest" ] ] }, { "name": "instance_exec", "parameters": [ [ "rest" ] ] }, { "name": "__id__", "parameters": [ ] } ], "superclass": null }, { "name": "StandardError", "type": "Class", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "StandardError", "RequireAll", "Kernel" ], "class_methods": [ ], "instance_methods": [ ], "superclass": "Exception" }, { "name": "Exception", "type": "Class", "singleton_class_ancestors": [ "RequireAll", "Kernel" ], "ancestors": [ "Exception", "RequireAll", "Kernel" ], "class_methods": [ { "name": "exception", "parameters": [ [ "rest" ] ] } ], "instance_methods": [ { "name": "==", "parameters": [ [ "req" ] ] }, { "name": "respond_to?", "parameters": [ [ "rest" ] ] }, { "name": "inspect", "parameters": [ ] }, { "name": "to_s", "parameters": [ ] }, { "name": "exception", "parameters": [ [ "rest" ] ] }, { "name": "message", "parameters": [ ] }, { "name": "backtrace", "parameters": [ ] }, { "name": "backtrace_locations", "parameters": [ ] }, { "name": "set_backtrace", "parameters": [ [ "req" ] ] }, { "name": "cause", "parameters": [ ] } ], "superclass": "Object" } ], "load_path": [ "/Users/vkkoshelev/.rvm/gems/ruby-2.4.1@global/gems/did_you_mean-1.1.0/lib", "/Users/vkkoshelev/.rvm/gems/ruby-2.4.1/gems/require_all-1.4.0/lib", "/Users/vkkoshelev/.rvm/gems/ruby-2.4.1/gems/json-2.1.0/lib", "/Users/vkkoshelev/.rvm/gems/ruby-2.4.1/extensions/x86_64-darwin-16/2.4.0/json-2.1.0", "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0", "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby/2.4.0/x86_64-darwin16", "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/site_ruby", "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/vendor_ruby/2.4.0", "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/vendor_ruby/2.4.0/x86_64-darwin16", "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/vendor_ruby", "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0", "/Users/vkkoshelev/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/x86_64-darwin16" ] } ================================================ FILE: state-tracker/src/test/java/testData/non-standard-module-type.json ================================================ { "top_level_constants": [ { "class_name": "IO", "extended": [], "name": "STDIN" } ], "load_path": [ "/Users/vkkoshelev/.rvm/gems/ruby-2.4.1@global/gems/did_you_mean-1.1.0/li" ], "modules": [ { "name": "AAAA", "type": "BBBB", "singleton_class_included": [ ], "included": [ ], "class_methods": [ ], "instance_methods": [ ] }, { "name": "BBBB", "type": "Module", "singleton_class_included": [ ], "included": [ ], "class_methods": [ ], "instance_methods": [ ], "superclass": "Module" } ] } ================================================ FILE: storage-server-api/build.gradle ================================================ buildscript { repositories { jcenter() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } dependencies { compile project(':common') compile project(':ruby-call-signature') compile "org.jetbrains.exposed:exposed:$exposedVersion" compile 'com.h2database:h2:1.4.197' } sourceSets { main.java.srcDirs = ['src/main/java'] main.kotlin.srcDirs = ['src/main/java'] test.kotlin.srcDirs = ['src/test/java'] } ================================================ FILE: storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/signature/serialization/BlobSerialization.kt ================================================ package org.jetbrains.ruby.codeInsight.types.signature.serialization import org.jetbrains.exposed.dao.EntityHook import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.ruby.codeInsight.types.signature.SignatureContract import org.jetbrains.ruby.codeInsight.types.storage.server.impl.SignatureContractRow import java.io.DataInputStream import java.io.DataOutputStream import java.sql.Blob import java.util.concurrent.CopyOnWriteArrayList import kotlin.reflect.KProperty class BlobDeserializer { companion object { private val openBlobs: MutableCollection = CopyOnWriteArrayList() init { EntityHook.subscribe { openBlobs.forEach { it.free() } openBlobs.clear() } } } @Volatile private var cachedContract: SignatureContract? = null operator fun getValue(signatureContractRow: SignatureContractRow, property: KProperty<*>): SignatureContract { cachedContract?.let { return it } val blob = signatureContractRow.contractRaw try { val result = SignatureContract(DataInputStream(blob.binaryStream)) cachedContract = result return result } finally { blob.free() } } operator fun setValue(signatureContractRow: SignatureContractRow, property: KProperty<*>, signatureContract: SignatureContract) { val blob = TransactionManager.current().connection.createBlob() openBlobs.add(blob) BlobSerializer.writeToBlob(signatureContract, blob) signatureContractRow.contractRaw = blob cachedContract = signatureContract } } object BlobSerializer { fun writeToBlob(signatureContract: SignatureContract, blob: Blob): Blob { val binaryStream = blob.setBinaryStream(1) signatureContract.serialize(DataOutputStream(binaryStream)) binaryStream.close() return blob } } ================================================ FILE: storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/DatabaseProvider.kt ================================================ package org.jetbrains.ruby.codeInsight.types.storage.server import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.Transaction import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.ruby.codeInsight.Logger import org.jetbrains.ruby.codeInsight.injector import org.jetbrains.ruby.codeInsight.types.storage.server.impl.* object DatabaseProvider { var defaultDatabase: Database? = null private set /** * Default database file path with .mv.db suffix included */ var defaultDatabaseFilePath: String? = null private set private const val IN_MEMORY_URL = "jdbc:h2:mem:test" private const val H2_DRIVER = "org.h2.Driver" const val H2_DB_FILE_EXTENSION = ".mv.db" private val logger: Logger = injector.getLogger(DatabaseProvider::class.java) @JvmStatic fun connectToInMemoryDB(isDefaultDatabase: Boolean = false): Database { val database = Database.connect(IN_MEMORY_URL, driver = H2_DRIVER) if (isDefaultDatabase) { defaultDatabase = database } logger.info("Connected to in memory DB") logDatabaseSize(database) return database } @JvmStatic fun connectToDB(filePath: String, isDefaultDatabase: Boolean = false): Database { check(filePath.endsWith(H2_DB_FILE_EXTENSION)) { "File path must end with $H2_DB_FILE_EXTENSION suffix" } val filePathForUrl = filePath.substring(0, filePath.lastIndexOf(H2_DB_FILE_EXTENSION)) val database = Database.connect("jdbc:h2:$filePathForUrl", driver = H2_DRIVER) if (isDefaultDatabase) { defaultDatabase = database defaultDatabaseFilePath = filePath } logger.info("Connected to DB: $filePath") createAllDatabases(database) logDatabaseSize(database) return database } @JvmStatic fun defaultDatabaseTransaction(statement: Transaction.() -> T): T { val defaultDatabaseLocal = defaultDatabase ?: throw IllegalStateException("Assign defaultDatabase firstly") return transaction(defaultDatabaseLocal, statement) } @JvmOverloads fun createAllDatabases(db: Database? = null) { transaction(db ?: defaultDatabase) { SchemaUtils.create(GemInfoTable, ClassInfoTable, MethodInfoTable, SignatureTable, CallInfoTable) } } @JvmOverloads fun dropAllDatabases(db: Database? = null) { transaction(db ?: defaultDatabase) { SchemaUtils.drop(GemInfoTable, ClassInfoTable, MethodInfoTable, SignatureTable, CallInfoTable) } } private fun logDatabaseSize(db: Database) { transaction(db) { for (table in listOf(CallInfoTable, MethodInfoTable, ClassInfoTable, SignatureTable, GemInfoTable)) { logger.info("${table.tableName} table's number of rows ${table.selectAll().count()}") } } } } ================================================ FILE: storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/RSignatureProvider.java ================================================ package org.jetbrains.ruby.codeInsight.types.storage.server; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.ruby.codeInsight.types.signature.*; import java.util.Collection; /** *

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

*

*

The general workflow is the following:

*
    *
  • 1. Determine which gem statistics are to be used. If one wants to receive code insight for some project, * they must know which gems are available at runtime. *
  • 2. Since a precalculated information for the particular gem may not be available, one searches for the * closest gem version with calculated stats via {@link #getClosestRegisteredGem(GemInfo)} *
  • 3. In order to get the registered classes available upon requiring the given gem one may use * {@link #getRegisteredClasses(GemInfo)} *
  • 4. Given a class of a receiver object one may get the registered methods available for sending * via {@link #getRegisteredMethods(ClassInfo)} *
  • 5. Given a call, which is represented as a method of a particular class in a particular gem one may * get Signature contract via {@link #getSignature(MethodInfo)}. It allows for getting params * information, deducing return type from given input types, etc. *
*/ public interface RSignatureProvider { @NotNull Collection getRegisteredGems() throws StorageException; @Nullable GemInfo getClosestRegisteredGem(@NotNull GemInfo usedGem) throws StorageException; @NotNull Collection getRegisteredClasses(@NotNull GemInfo gem) throws StorageException; @NotNull Collection getAllClassesWithFQN(@NotNull String fqn) throws StorageException; @NotNull Collection getRegisteredMethods(@NotNull ClassInfo containerClass) throws StorageException; /** * Get registered {@link CallInfo}s by given {@code methodInfo} */ @NotNull Collection getRegisteredCallInfos(@NotNull MethodInfo methodInfo) throws StorageException; @Nullable SignatureInfo getSignature(@NotNull MethodInfo method) throws StorageException; void deleteSignature(@NotNull MethodInfo method) throws StorageException; void putSignature(@NotNull SignatureInfo signatureInfo) throws StorageException; } ================================================ FILE: storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/RSignatureStorage.java ================================================ package org.jetbrains.ruby.codeInsight.types.storage.server; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.ruby.codeInsight.types.signature.*; import java.util.Collection; public interface RSignatureStorage extends RSignatureProvider { default void readPacket(@NotNull T packet) throws StorageException { for (final SignatureInfo signatureInfo : packet.getSignatures()) { final MethodInfo methodInfo = signatureInfo.getMethodInfo(); final SignatureInfo oldSignature = getSignature(methodInfo); RSignatureContract contract; if (oldSignature != null && (contract = RSignatureContract.mergeMutably(oldSignature.getContract(), signatureInfo.getContract())) != null) { putSignature(SignatureInfoKt.SignatureInfo(methodInfo, contract)); } else { putSignature(signatureInfo); } } } @NotNull Collection formPackets(@Nullable ExportDescriptor descriptor) throws StorageException; class ExportDescriptor { private final boolean myInclude; @NotNull private final Collection myGemsToIncludeOrExclude; public ExportDescriptor(boolean include, @NotNull Collection gemsToIncludeOrExclude) { myInclude = include; myGemsToIncludeOrExclude = gemsToIncludeOrExclude; } public boolean isInclude() { return myInclude; } @NotNull public Collection getGemsToIncludeOrExclude() { return myGemsToIncludeOrExclude; } } interface Packet { Collection getSignatures(); } } ================================================ FILE: storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/StorageException.java ================================================ package org.jetbrains.ruby.codeInsight.types.storage.server; @SuppressWarnings("unused") public class StorageException extends Exception { public StorageException() { } public StorageException(String message) { super(message); } public StorageException(String message, Throwable cause) { super(message, cause); } public StorageException(Throwable cause) { super(cause); } public StorageException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } ================================================ FILE: storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/impl/IntIdTableWithPossibleDependency.kt ================================================ package org.jetbrains.ruby.codeInsight.types.storage.server.impl import org.jetbrains.exposed.dao.EntityID import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.InsertStatement import org.jetbrains.exposed.sql.statements.UpdateBuilder import org.jetbrains.exposed.sql.statements.UpdateStatement import org.jetbrains.exposed.sql.transactions.transaction /** * Represents SQL table which can possibly have dependency (by having dependency we mean that `this` SQL table can * possibly have column where we store id which refers to some row in other table called [dependency]) * @param dependency SQL table which rows can be possibly referred from `this` SQL table * @param T type of info stored in this SQL table * @param F type of info stored in [dependency] SQL table */ abstract class IntIdTableWithPossibleDependency( private val dependency: IntIdTableWithPossibleDependency?) : IntIdTable() { /** * Find id of row where [info] is located. **Call this function only inside [transaction] block** */ fun findRowId(info: T): EntityID? { if (info is IntEntity) { return info.id } if (!validateInfo(info)) { return null } return traverseDependencies(info).let { it.joinedWithDependencies.select { it.searchCriteria } }.firstOrNull()?.get(id) } /** * Insert info into SQL table if [info] is not still in the table; otherwise updates information in row according to [info]. * **Call this function only inside [transaction] block** * * @return id of row where [info] was inserted or if SQL table already contains [info] than returns id of corresponding row */ open fun insertInfoIfNotContains(info: T): EntityID? { val dependencyId: EntityID? = convertInfoToDependencyFormant(info)?.let { dependency?.insertInfoIfNotContains(it) } if (!validateInfoBeforeWritingToBuilder(info, dependencyId)) { return null } var ret = findRowId(info) if (ret != null) { update(where = { id eq ret!! }) { updateStatementBuilder: UpdateStatement -> writeInfoToBuilder(updateStatementBuilder, info, dependencyId) } } else { ret = insertAndGetId { insertStatementBuilder: InsertStatement> -> writeInfoToBuilder(insertStatementBuilder, info, dependencyId) } } removeInvalidInfo(info) return ret } protected open fun removeInvalidInfo(validInfo: T) { } protected open fun validateInfo(info: T): Boolean = true protected open fun validateInfoBeforeWritingToBuilder(info: T, dependencyId: EntityID?) = validateInfo(info) protected abstract fun writeInfoToBuilder(builder: UpdateBuilder<*>, info: T, dependencyId: EntityID?) protected abstract fun SqlExpressionBuilder.createSearchCriteriaForInfo(info: T): Op protected abstract fun convertInfoToDependencyFormant(info: T): F? private data class TraverseResult(var joinedWithDependencies: ColumnSet, var searchCriteria: Op) private fun traverseDependencies(info: T): TraverseResult { val searchCriteria = SqlExpressionBuilder.createSearchCriteriaForInfo(info) val convertedInfo by lazy { convertInfoToDependencyFormant(info) } if (dependency == null || convertedInfo == null) { return TraverseResult(this, searchCriteria) } val ret = dependency.traverseDependencies(convertedInfo) ret.joinedWithDependencies = ret.joinedWithDependencies.innerJoin(this) ret.searchCriteria = ret.searchCriteria and searchCriteria return ret } } /** * @see IntIdTableWithPossibleDependency * @see IntIdTableWithDependency * @see IntIdTableWithNullableDependency */ abstract class IntIdTableWithoutDependency : IntIdTableWithPossibleDependency(null) { final override fun convertInfoToDependencyFormant(info: T): Nothing? = null } /** * Represents SQL table with nullable [dependency]. (nullable dependency means that column which contains id refers * to some row in [dependency] table can possibly be `null`) * * @see IntIdTableWithPossibleDependency * @see IntIdTableWithoutDependency * @see IntIdTableWithDependency */ abstract class IntIdTableWithNullableDependency(dependency: IntIdTableWithPossibleDependency) : IntIdTableWithPossibleDependency(dependency) { final override fun validateInfoBeforeWritingToBuilder(info: T, dependencyId: EntityID?): Boolean { return super.validateInfoBeforeWritingToBuilder(info, dependencyId) } } /** * Represents SQL table with not nullable dependency (not nullable dependency means that column which contains id refers * to some row in [dependency] table cannot be `null`) * * @see IntIdTableWithPossibleDependency * @see IntIdTableWithoutDependency * @see IntIdTableWithNullableDependency */ abstract class IntIdTableWithDependency(dependency: IntIdTableWithPossibleDependency) : IntIdTableWithPossibleDependency(dependency) { final override fun validateInfoBeforeWritingToBuilder(info: T, dependencyId: EntityID?): Boolean { return dependencyId != null && super.validateInfoBeforeWritingToBuilder(info, dependencyId) } final override fun writeInfoToBuilder(builder: UpdateBuilder<*>, info: T, dependencyId: EntityID?) { /** * can do unsafe dereference as soon as [dependencyId] was checked in [validateInfoBeforeWritingToBuilder] */ writeInfoToBuilderNotNullableDependency(builder, info, dependencyId!!) } protected abstract fun writeInfoToBuilderNotNullableDependency(builder: UpdateBuilder<*>, info: T, dependencyId: EntityID) } ================================================ FILE: storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/impl/RSignatureProviderImpl.kt ================================================ package org.jetbrains.ruby.codeInsight.types.storage.server.impl import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.select import org.jetbrains.ruby.codeInsight.types.signature.* import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.RSignatureProvider object RSignatureProviderImpl : RSignatureProvider { override fun getRegisteredGems(): Collection { return DatabaseProvider.defaultDatabaseTransaction { GemInfoRow.all() }.map { it.copy() } } override fun getClosestRegisteredGem(usedGem: GemInfo): GemInfo? { val (upperBound, lowerBound) = DatabaseProvider.defaultDatabaseTransaction { val upperBound = GemInfoTable.select { GemInfoTable.name.eq(usedGem.name) and GemInfoTable.version.greaterEq(usedGem.version) } .orderBy(GemInfoTable.version) .limit(1) .firstOrNull() ?.let { GemInfoRow.wrapRow(it) } val lowerBound = GemInfoTable.select { GemInfoTable.name.eq(usedGem.name) and GemInfoTable.version.lessEq(usedGem.version) } .orderBy(GemInfoTable.version, isAsc = false) .limit(1) .firstOrNull() ?.let { GemInfoRow.wrapRow(it) } return@defaultDatabaseTransaction Pair(upperBound?.copy(), lowerBound?.copy()) } if (lowerBound == null || upperBound == null) { return lowerBound ?: upperBound } else { return if (firstStringCloser(usedGem.version, lowerBound.version, upperBound.version)) lowerBound else upperBound } } override fun getRegisteredClasses(gem: GemInfo): Collection { return DatabaseProvider.defaultDatabaseTransaction { val gemId = GemInfoTable.findRowId(gem) ?: return@defaultDatabaseTransaction emptyList() return@defaultDatabaseTransaction ClassInfoRow.find { ClassInfoTable.gemInfo eq gemId }.map { it.copy() } } } override fun getAllClassesWithFQN(fqn: String): Collection { return DatabaseProvider.defaultDatabaseTransaction { ClassInfoRow.find { ClassInfoTable.fqn eq fqn }.map { it.copy() } } } override fun getRegisteredMethods(containerClass: ClassInfo): Collection { return DatabaseProvider.defaultDatabaseTransaction { val classId = ClassInfoTable.findRowId(containerClass) ?: return@defaultDatabaseTransaction emptyList() return@defaultDatabaseTransaction MethodInfoRow.find { MethodInfoTable.classInfo eq classId }.map { it.copy() } } } override fun getSignature(method: MethodInfo): SignatureInfo? { return DatabaseProvider.defaultDatabaseTransaction { val methodId = MethodInfoTable.findRowId(method) ?: return@defaultDatabaseTransaction null return@defaultDatabaseTransaction SignatureContractRow.find { SignatureTable.methodInfo eq methodId }.firstOrNull()?.copy() } } override fun deleteSignature(method: MethodInfo) { return DatabaseProvider.defaultDatabaseTransaction { val methodId = MethodInfoTable.findRowId(method) ?: return@defaultDatabaseTransaction SignatureTable.deleteWhere { SignatureTable.methodInfo eq methodId } } } override fun putSignature(signatureInfo: SignatureInfo) { SignatureTable.insertInfoIfNotContains(signatureInfo) } override fun getRegisteredCallInfos(methodInfo: MethodInfo): List { return DatabaseProvider.defaultDatabaseTransaction { val methodId = MethodInfoTable.findRowId(methodInfo) ?: return@defaultDatabaseTransaction emptyList() return@defaultDatabaseTransaction CallInfoRow.find { CallInfoTable.methodInfoId eq methodId }.map { it.copy() } } } } fun firstStringCloser(gemVersion: String, firstVersion: String, secondVersion: String): Boolean { val lcpLengthFirst = longestCommonPrefixLength(gemVersion, firstVersion) val lcpLengthSecond = longestCommonPrefixLength(gemVersion, secondVersion) return lcpLengthFirst > lcpLengthSecond || lcpLengthFirst > 0 && lcpLengthFirst == lcpLengthSecond && Math.abs(gemVersion.rawChar(lcpLengthFirst) - firstVersion.rawChar(lcpLengthFirst)) < Math.abs(gemVersion.rawChar(lcpLengthFirst) - secondVersion.rawChar(lcpLengthSecond)) } private fun String.rawChar(index: Int): Int = if (index < length) this[index].toInt() else 0 private fun longestCommonPrefixLength(str1: String, str2: String): Int { val minLength = Math.min(str1.length, str2.length) return (0 until minLength).firstOrNull { str1[it] != str2[it] } ?: minLength } ================================================ FILE: storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/impl/RowConversions.kt ================================================ package org.jetbrains.ruby.codeInsight.types.storage.server.impl import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.ruby.codeInsight.types.signature.* import org.jetbrains.ruby.codeInsight.types.signature.serialization.SignatureContract import java.io.DataInputStream fun GemInfo(row: ResultRow): GemInfo = GemInfo(row[GemInfoTable.name], row[GemInfoTable.version]) fun ClassInfo(row: ResultRow): ClassInfo = ClassInfo(GemInfo(row), row[ClassInfoTable.fqn]) fun Location(row: ResultRow): Location? { val locationFile = row[MethodInfoTable.locationFile] ?: return null return Location(locationFile, row[MethodInfoTable.locationLineno]) } fun MethodInfo(row: ResultRow): MethodInfo = MethodInfo.Impl( ClassInfo(row), row[MethodInfoTable.name], row[MethodInfoTable.visibility], Location(row)) fun SignatureInfo(row: ResultRow): SignatureInfo { val blob = row[SignatureTable.contract] try { return SignatureInfo(MethodInfo(row), SignatureContract(DataInputStream(blob.binaryStream))) } finally { blob.free() } } ================================================ FILE: storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/impl/Schema.kt ================================================ package org.jetbrains.ruby.codeInsight.types.storage.server.impl import org.jetbrains.exposed.dao.EntityID import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.UpdateBuilder import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.ruby.codeInsight.types.signature.* import org.jetbrains.ruby.codeInsight.types.signature.serialization.BlobDeserializer import org.jetbrains.ruby.codeInsight.types.signature.serialization.BlobSerializer import java.sql.Blob import kotlin.reflect.KProperty object GemInfoTable : IntIdTableWithoutDependency() { val name = varchar("name", GemInfo.LENGTH_OF_GEMNAME).index() val version = varchar("version", GemInfo.LENGTH_OF_GEMVERSION) override fun SqlExpressionBuilder.createSearchCriteriaForInfo(info: GemInfo): Op { return (name eq info.name) and (version eq info.version) } override fun validateInfo(info: GemInfo): Boolean { return info.name.length <= GemInfo.LENGTH_OF_GEMNAME && info.version.length <= GemInfo.LENGTH_OF_GEMVERSION } override fun writeInfoToBuilder(builder: UpdateBuilder<*>, info: GemInfo, dependencyId: EntityID?) { builder[name] = info.name builder[version] = info.version } } class GemInfoRow(id: EntityID) : IntEntity(id), GemInfo { companion object : IntEntityClass(GemInfoTable) override var name: String by GemInfoTable.name override var version: String by GemInfoTable.version fun copy(): GemInfo = GemInfo(this) } object ClassInfoTable : IntIdTableWithNullableDependency(GemInfoTable) { val gemInfo = reference("gem_info", GemInfoTable, ReferenceOption.CASCADE).nullable() val fqn = varchar("fqn", ClassInfo.LENGTH_OF_FQN).index() override fun SqlExpressionBuilder.createSearchCriteriaForInfo(info: ClassInfo): Op { // HACK: as soon as fqn in RubyMine is not fully qualified (search criteria must be: fqn eq info.classFQN) return fqn like "%${info.classFQN}" } override fun convertInfoToDependencyFormant(info: ClassInfo): GemInfo? { return info.gemInfo } override fun validateInfo(info: ClassInfo): Boolean { return info.classFQN.length <= ClassInfo.LENGTH_OF_FQN } override fun writeInfoToBuilder(builder: UpdateBuilder<*>, info: ClassInfo, dependencyId: EntityID?) { builder[fqn] = info.classFQN builder[gemInfo] = dependencyId } } class ClassInfoRow(id: EntityID) : IntEntity(id), ClassInfo { companion object : IntEntityClass(ClassInfoTable) override val gemInfo: GemInfoRow? by GemInfoRow optionalReferencedOn ClassInfoTable.gemInfo override val classFQN: String by ClassInfoTable.fqn fun copy(): ClassInfo = ClassInfo(this) } object MethodInfoTable : IntIdTableWithDependency(ClassInfoTable) { val classInfo = reference("class_info", ClassInfoTable, ReferenceOption.CASCADE) val name = varchar("name", MethodInfo.LENGTH_OF_NAME).index() val visibility = enumeration("visibility", RVisibility::class) val locationFile = varchar("location_file", MethodInfo.LENGTH_OF_PATH).nullable() val locationLineno = integer("location_lineno").default(0) override fun convertInfoToDependencyFormant(info: MethodInfo): ClassInfo? { return info.classInfo } override fun SqlExpressionBuilder.createSearchCriteriaForInfo(info: MethodInfo): Op { return name eq info.name } override fun validateInfo(info: MethodInfo): Boolean { return info.name.length <= MethodInfo.LENGTH_OF_NAME && info.location?.let { it.path.length <= MethodInfo.LENGTH_OF_PATH } ?: true } override fun writeInfoToBuilderNotNullableDependency(builder: UpdateBuilder<*>, info: MethodInfo, dependencyId: EntityID) { builder[classInfo] = dependencyId builder[name] = info.name builder[visibility] = info.visibility builder[locationFile] = info.location?.path builder[locationLineno] = info.location?.lineno ?: 0 } } class MethodInfoRow(id: EntityID) : IntEntity(id), MethodInfo { companion object : IntEntityClass(MethodInfoTable) override var classInfo: ClassInfoRow by ClassInfoRow referencedOn MethodInfoTable.classInfo override var name: String by MethodInfoTable.name override var visibility: RVisibility by MethodInfoTable.visibility override var location: Location? by object { operator fun getValue(methodInfoRow: MethodInfoRow, property: KProperty<*>): Location? { val file = MethodInfoTable.locationFile.getValue(methodInfoRow, property) return file?.let { Location(it, MethodInfoTable.locationLineno.getValue(methodInfoRow, property)) } } operator fun setValue(methodInfoRow: MethodInfoRow, property: KProperty<*>, location: Location?) { MethodInfoTable.locationFile.setValue(methodInfoRow, property, location?.path) MethodInfoTable.locationLineno.setValue(methodInfoRow, property, location?.lineno ?: 0) } } override fun toString(): String { return name } fun copy(): MethodInfo = MethodInfo(this) } /** * Represent SQL table which contains information about every Ruby method call */ object CallInfoTable : IntIdTableWithDependency(MethodInfoTable) { private const val ARGS_TYPES_STRING_LENGTH = 300 private const val RETURN_TYPE_STRING_LENGTH = 50 private const val CALL_INFOS_LIMIT_FOR_PARTICULAR_METHOD = 10 val methodInfoId = reference("method_info_id", MethodInfoTable, ReferenceOption.NO_ACTION).index() /** * string containing types of unnamed args (e.g. REQ, KEYREQ args) splitted by separator */ val unnamedArgsTypes = varchar("required_args_types", ARGS_TYPES_STRING_LENGTH) val namedArgsTypes = varchar("named_args_types", ARGS_TYPES_STRING_LENGTH) private val numberOfUnnamedArguments = integer("number_of_unnamed_arguments") val returnType = varchar("return_type", RETURN_TYPE_STRING_LENGTH) /** * Deletes all info from [CallInfoTable] related to [methodInfo]. * **Call this function only inside [transaction] block.** */ fun deleteAllInfoRelatedTo(methodInfo: MethodInfo) { val methodInfoId = MethodInfoTable.findRowId(methodInfo) ?: return CallInfoTable.deleteWhere { CallInfoTable.methodInfoId eq methodInfoId } } override fun insertInfoIfNotContains(info: CallInfo): EntityID? { val count = MethodInfoTable.findRowId(info.methodInfo)?.let { CallInfoTable.select { CallInfoTable.methodInfoId eq it }.count() } ?: 0 if (count >= CALL_INFOS_LIMIT_FOR_PARTICULAR_METHOD) { return null } else { return super.insertInfoIfNotContains(info) } } override fun SqlExpressionBuilder.createSearchCriteriaForInfo(info: CallInfo): Op { return (numberOfUnnamedArguments eq info.unnamedArguments.size) and (unnamedArgsTypes eq info.unnamedArgumentsTypesJoinToRawString()) and (namedArgsTypes eq info.namedArgumentsJoinToRawString()) and (returnType eq info.returnType) } override fun convertInfoToDependencyFormant(info: CallInfo): MethodInfo? { return info.methodInfo } override fun validateInfo(info: CallInfo): Boolean { return info.unnamedArgumentsTypesJoinToRawString().length <= ARGS_TYPES_STRING_LENGTH && info.returnType.length <= RETURN_TYPE_STRING_LENGTH } override fun writeInfoToBuilderNotNullableDependency(builder: UpdateBuilder<*>, info: CallInfo, dependencyId: EntityID) { builder[methodInfoId] = dependencyId builder[unnamedArgsTypes] = info.unnamedArgumentsTypesJoinToRawString() builder[namedArgsTypes] = info.namedArgumentsJoinToRawString() builder[numberOfUnnamedArguments] = info.unnamedArguments.size builder[returnType] = info.returnType } override fun removeInvalidInfo(validInfo: CallInfo) { val methodInfoId = MethodInfoTable.findRowId(validInfo.methodInfo) ?: return deleteWhere { (CallInfoTable.methodInfoId eq methodInfoId) and (CallInfoTable.numberOfUnnamedArguments neq validInfo.unnamedArguments.size) } } } class CallInfoRow(id: EntityID) : IntEntity(id), CallInfo { companion object : IntEntityClass(CallInfoTable) private val requiredArgsTypesRaw: String by CallInfoTable.unnamedArgsTypes private val namedArgsTypesRaw: String by CallInfoTable.namedArgsTypes override val namedArguments: List by lazy { namedArgsTypesRaw.takeIf { it != "" }?.split(ARGUMENTS_TYPES_SEPARATOR)?.asSequence()?.map { val (name, type) = it.split(ArgumentNameAndType.NAME_AND_TYPE_SEPARATOR) return@map ArgumentNameAndType(name, type) }?.toList() ?: emptyList() } override fun namedArgumentsJoinToRawString(): String = namedArgsTypesRaw override val methodInfo: MethodInfoRow by MethodInfoRow referencedOn CallInfoTable.methodInfoId override val unnamedArguments: List by lazy { requiredArgsTypesRaw.takeIf { it != "" }?.split(ARGUMENTS_TYPES_SEPARATOR)?.map { val (name, type) = it.split(ArgumentNameAndType.NAME_AND_TYPE_SEPARATOR) return@map ArgumentNameAndType(name, type) } ?: emptyList() } override val returnType: String by CallInfoTable.returnType override fun unnamedArgumentsTypesJoinToRawString(): String = requiredArgsTypesRaw override fun toString(): String { // just for pretty debugging :) return "name: ${methodInfo.name} unnamedArguments: " + unnamedArguments.joinToString(separator = ", ", prefix = "[", postfix = "]") + " return: $returnType" } fun copy(): CallInfo = CallInfoImpl(methodInfo.copy(), namedArguments, unnamedArguments, returnType) } object SignatureTable : IntIdTableWithDependency(MethodInfoTable) { val methodInfo = reference("method_info", MethodInfoTable, ReferenceOption.CASCADE).index() val contract = blob("contract") override fun insertInfoIfNotContains(info: SignatureInfo): EntityID? { val methodInfoRow: MethodInfoRow = MethodInfoTable.insertInfoIfNotContains(info.methodInfo) ?.let { MethodInfoRow[it] } ?: return null val existingContractData = SignatureContractRow.find { SignatureTable.methodInfo eq methodInfoRow.id } .firstOrNull() if (existingContractData != null) { existingContractData.contract = info.contract } else { SignatureContractRow.new { this.methodInfo = methodInfoRow; contract = info.contract } } return SignatureContractRow.new { this.methodInfo = methodInfoRow; contract = info.contract }.id } override fun writeInfoToBuilderNotNullableDependency(builder: UpdateBuilder<*>, info: SignatureInfo, dependencyId: EntityID) { builder[methodInfo] = dependencyId builder[contract] = BlobSerializer.writeToBlob(info.contract, TransactionManager.current().connection.createBlob()) } override fun SqlExpressionBuilder.createSearchCriteriaForInfo(info: SignatureInfo): Op { return contract eq BlobSerializer.writeToBlob(info.contract, TransactionManager.current().connection.createBlob()) } override fun convertInfoToDependencyFormant(info: SignatureInfo): MethodInfo { return info.methodInfo } } class SignatureContractRow(id: EntityID) : IntEntity(id), SignatureInfo { companion object : IntEntityClass(SignatureTable) override var methodInfo: MethodInfoRow by MethodInfoRow referencedOn SignatureTable.methodInfo override var contract: SignatureContract by BlobDeserializer() var contractRaw: Blob by SignatureTable.contract fun copy(): SignatureInfo = SignatureInfo(this) } ================================================ FILE: storage-server-api/src/main/java/org/jetbrains/ruby/codeInsight/types/storage/server/testutil/DatabaseTestUtils.kt ================================================ package org.jetbrains.ruby.codeInsight.types.storage.server.testutil import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider /** * This function is used to test database related things. Just creates new clean databases, * executes [block] and remove created databases. * * [TestCase.setUp] and [TestCase.tearDown] functions won't help because [DatabaseProvider.createAllDatabases] * must be called in the same [transaction] block for in memory database */ fun doDBTest(block: () -> Unit) { transaction { DatabaseProvider.createAllDatabases() block() DatabaseProvider.dropAllDatabases() } } ================================================ FILE: storage-server-api/src/test/java/org/jetbrains/ruby/codeInsight/types/storage/server/impl/RSignatureProviderTest.kt ================================================ package org.jetbrains.ruby.codeInsight.types.storage.server.impl import junit.framework.TestCase import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.ruby.codeInsight.types.signature.* import org.jetbrains.ruby.codeInsight.types.signature.serialization.BlobSerializer import org.jetbrains.ruby.codeInsight.types.signature.serialization.SignatureContract import org.jetbrains.ruby.codeInsight.types.signature.serialization.StringDataInput import org.jetbrains.ruby.codeInsight.types.storage.server.DatabaseProvider import org.jetbrains.ruby.codeInsight.types.storage.server.testutil.doDBTest import org.junit.Test class RSignatureProviderTest : TestCase() { init { DatabaseProvider.connectToInMemoryDB(isDefaultDatabase = true) } @Test fun testPutGet() = doDBTest { GemInfoTable.insert { it[name] = "rails"; it[version] = "5.0.0.beta1" } val insertedGem = GemInfoRow.all().first() assertEquals("rails", insertedGem.name) assertEquals("5.0.0.beta1", insertedGem.version) ClassInfoTable.insert { it[gemInfo] = insertedGem.id; it[fqn] = "ActiveRecord::Base" } val insertedClass = ClassInfoRow.all().first() assertEquals("ActiveRecord::Base", insertedClass.classFQN) assertEquals("rails", insertedClass.gemInfo?.name) MethodInfoTable.insert { it[classInfo] = insertedClass.id; it[name] = "[]="; it[visibility] = RVisibility.PUBLIC } val insertedMethod = MethodInfoRow.all().first() assertEquals("[]=", insertedMethod.name) assertEquals(RVisibility.PUBLIC, insertedMethod.visibility) assertEquals("ActiveRecord::Base", insertedMethod.classInfo.classFQN) } @Test fun testClosestGem() = doDBTest { val gems = listOf( GemInfo("name1", "0.1"), GemInfo("name1", "0.2"), GemInfo("name1", "0.3"), GemInfo("name2", "1.0") ) for (gem in gems) { GemInfoTable.insert { it[name] = gem.name; it[version] = gem.version } } val provider = RSignatureProviderImpl assertEquals("0.1", provider.getClosestRegisteredGem(GemInfo("name1", "0.0"))?.version) assertEquals("0.1", provider.getClosestRegisteredGem(GemInfo("name1", "0.1"))?.version) assertEquals("0.1", provider.getClosestRegisteredGem(GemInfo("name1", "0.1.2"))?.version) assertEquals("0.1", provider.getClosestRegisteredGem(GemInfo("name1", "0.1.9"))?.version) assertEquals("0.2", provider.getClosestRegisteredGem(GemInfo("name1", "0.2"))?.version) assertEquals("0.3", provider.getClosestRegisteredGem(GemInfo("name1", "0.4"))?.version) assertEquals("0.3", provider.getClosestRegisteredGem(GemInfo("name1", "1.0"))?.version) assertEquals("1.0", provider.getClosestRegisteredGem(GemInfo("name2", "1.0"))?.version) assertEquals("1.0", provider.getClosestRegisteredGem(GemInfo("name2", "0.1"))?.version) assertEquals("1.0", provider.getClosestRegisteredGem(GemInfo("name2", "2.0"))?.version) assertEquals(null, provider.getClosestRegisteredGem(GemInfo("name3", "2.0"))) } @Test fun testRegisteredClasses() = doDBTest { val insertResult = GemInfoTable.insertAndGetId { it[name] = "test_gem"; it[version] = "0.1" } ClassInfoTable.insert { it[fqn] = "Test1"; it[gemInfo] = insertResult } ClassInfoTable.insert { it[fqn] = "Test2"; it[gemInfo] = insertResult } ClassInfoTable.insert { it[fqn] = "Test3" } val provider = RSignatureProviderImpl val classes = provider.getRegisteredClasses(GemInfo("test_gem", "0.1")) assertEquals(2, classes.size) assertEquals(setOf("Test1", "Test2"), classes.map { it.classFQN }.toSet()) } @Test fun testRegisteredMethods() = doDBTest { val gem = GemInfoTable.insertAndGetId { it[name] = "test_gem"; it[version] = "1.2.3" } val class1 = ClassInfoTable.insertAndGetId { it[fqn] = "Test::Fqn" } val class2 = ClassInfoTable.insertAndGetId { it[fqn] = "Test2::Fqn" } val class3 = ClassInfoTable.insertAndGetId { it[fqn] = "Test::Fqn"; it[gemInfo] = gem } MethodInfoTable.insert { it[name] = "met1"; it[visibility] = RVisibility.PUBLIC; it[classInfo] = class1 } MethodInfoTable.insert { it[name] = "met2"; it[visibility] = RVisibility.PUBLIC; it[classInfo] = class1 } MethodInfoTable.insert { it[name] = "met3"; it[visibility] = RVisibility.PUBLIC; it[classInfo] = class2 } MethodInfoTable.insert { it[name] = "met4"; it[visibility] = RVisibility.PUBLIC; it[classInfo] = class3 } val provider = RSignatureProviderImpl val methodsWithNullGem = provider.getRegisteredMethods(ClassInfo("Test::Fqn")) assertEquals(2, methodsWithNullGem.size) assertEquals(setOf("met1", "met2"), methodsWithNullGem.map { it.name }.toSet()) val methodsWithGivenGem = provider.getRegisteredMethods(ClassInfo(GemInfo("test_gem", "1.2.3"), "Test::Fqn")) assertEquals(1, methodsWithGivenGem.size) assertEquals(setOf("met4"), methodsWithGivenGem.map { it.name }.toSet()) } @Test fun testSignatures() = doDBTest { val gem = GemInfoTable.insertAndGetId { it[name] = "test_gem"; it[version] = "1.2.3" } val clazz = ClassInfoTable.insertAndGetId { it[fqn] = "Test::Fqn"; it[gemInfo] = gem } val method1 = MethodInfoTable.insertAndGetId { it[name] = "met1" it[visibility] = RVisibility.PUBLIC it[classInfo] = clazz } val method2 = MethodInfoTable.insertAndGetId { it[name] = "met2" it[visibility] = RVisibility.PUBLIC it[classInfo] = clazz } val contract1 = SignatureContract(StringDataInput(SignatureTestData.simpleContract)) val contract2 = SignatureContract(StringDataInput(SignatureTestData.trivialContract)) val blob1 = TransactionManager.current().connection.createBlob() SignatureTable.insert { it[contract] = BlobSerializer.writeToBlob(contract1, blob1); it[methodInfo] = method1 } blob1.free() val blob2 = TransactionManager.current().connection.createBlob() SignatureTable.insert { it[contract] = BlobSerializer.writeToBlob(contract2, blob2); it[methodInfo] = method2 } blob2.free() val provider = RSignatureProviderImpl val signatureInfo1 = provider.getSignature(MethodInfo(ClassInfo(GemInfo("test_gem", "1.2.3"), "Test::Fqn"), "met1", RVisibility.PUBLIC)) val signatureInfo2 = provider.getSignature(MethodInfo(ClassInfo(GemInfo("test_gem", "1.2.3"), "Test::Fqn"), "met2", RVisibility.PUBLIC)) assertNotNull(signatureInfo1) assertEquals(4, signatureInfo1!!.contract.nodeCount) assertNotNull(signatureInfo2) assertEquals(2, signatureInfo2!!.contract.nodeCount) } @Test fun testSignaturesWithAPIPut() = doDBTest { val gem = GemInfo("test_gem", "1.2.3") val clazz = ClassInfo(gem, "Test::Fqn") val method1 = MethodInfo(clazz, "met1", RVisibility.PUBLIC) val method2 = MethodInfo(clazz, "met2", RVisibility.PUBLIC) val contract1 = SignatureContract(StringDataInput(SignatureTestData.simpleContract)) val contract2 = SignatureContract(StringDataInput(SignatureTestData.trivialContract)) val provider = RSignatureProviderImpl provider.putSignature(SignatureInfo(method1, contract1)) provider.putSignature(SignatureInfo(method2, contract2)) val signatureInfo1 = provider.getSignature(MethodInfo(ClassInfo(GemInfo("test_gem", "1.2.3"), "Test::Fqn"), "met1", RVisibility.PUBLIC)) val signatureInfo2 = provider.getSignature(MethodInfo(ClassInfo(GemInfo("test_gem", "1.2.3"), "Test::Fqn"), "met2", RVisibility.PUBLIC)) assertNotNull(signatureInfo1) assertEquals(4, signatureInfo1!!.contract.nodeCount) assertNotNull(signatureInfo2) assertEquals(2, signatureInfo2!!.contract.nodeCount) } @Test fun testSignaturesWithSignatureReplace() = doDBTest { val gem = GemInfo("test_gem", "1.2.3") val clazz = ClassInfo(gem, "Test::Fqn") val method = MethodInfo(clazz, "met1", RVisibility.PUBLIC) val contract1 = SignatureContract(StringDataInput(SignatureTestData.simpleContract)) val contract2 = SignatureContract(StringDataInput(SignatureTestData.trivialContract)) val provider = RSignatureProviderImpl provider.putSignature(SignatureInfo(method, contract1)) val signatureInfo1 = provider.getSignature(MethodInfo(ClassInfo(GemInfo("test_gem", "1.2.3"), "Test::Fqn"), "met1", RVisibility.PUBLIC)) assertNotNull(signatureInfo1) assertEquals(4, signatureInfo1!!.contract.nodeCount) provider.putSignature(SignatureInfo(method, contract2)) val signatureInfo2 = provider.getSignature(MethodInfo(ClassInfo(GemInfo("test_gem", "1.2.3"), "Test::Fqn"), "met1", RVisibility.PUBLIC)) assertNotNull(signatureInfo2) assertEquals(2, signatureInfo2!!.contract.nodeCount) } object SignatureTestData { const val simpleContract = """ 1 arg 0 4 3 1 0 a 2 0 b 2 0 c 1 3 0 d 1 3 1 0 0 """ const val trivialContract = """ 0 2 1 1 0 a 0 """ } }