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:

## Type providing for method parameters

## Type providing for return value

## 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 [](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [](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:

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 [](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