Skip to content

Latest commit

 

History

History
421 lines (302 loc) · 15.9 KB

CONTRIBUTING.md

File metadata and controls

421 lines (302 loc) · 15.9 KB

Kotlin

We use Kotlin language for the plugin. If you can program in Java, you should be able to read and write Kotlin code right away. Kotlin is deeply similar to Java, but has less verbose syntax and better safety. It also shares some characteristics with Rust: type inference, immutability by default, no null pointers (apart from those that come from Java).

If you are unsure how to implement something in Kotlin, ask a question in our Gitter, send a PR in Java, or use Convert Java to Kotlin action in the IDE.

Getting started

Environment

Java 17 is required for development. For example, you can install openJDK or Amazon Corretto. You can also install Java just from your IntelliJ IDEA.

Clone

git clone https://github.com/intellij-rust/intellij-rust.git
cd intellij-rust

Building & Running

Run RunIDEA or RunCLion run configuration to build and launch development IDE (IntelliJ IDEA or CLion respectively) with the plugin.

We use gradle with gradle-intellij plugin to build the plugin. It comes with a wrapper script (gradlew in the root of the repository) which downloads appropriate version of gradle automatically as long as you have JDK installed.

Common Gradle tasks are:

  • ./gradlew :plugin:buildPlugin -- fully build plugin and create an archive at plugin/build/distributions which can be installed into your IDE via Install plugin from disk... action found in Settings > Plugins.

  • ./gradlew :plugin:runIde -- run a development IDE with the plugin installed. Can run IntelliJ IDEA or CLion. The particular IDE depends on baseIDE property in gradle.properties.

  • ./gradlew :test -- more than five thousands tests. We love tests!

Note the : in front of the task name. The repository contains several modules that belong to two independent plugins for Rust and TOML, which are organized as gradle subprojects. Running ./gradlew :task executes the task only for root module (core module of Rust plugin), ./gradlew :intellij-toml:task will run the task for TOML module and ./gradlew task will do for all modules.

Development in IntelliJ IDEA

You can get the latest IntelliJ IDEA Community Edition here, it is free.

Import the plugin project as you would do with any other gradle based project. For example, Ctrl + Shift + A, Import project and select build.gradle.kts from the root directory of the plugin.

There are Test, RunIDEA, RunCLion and Generate Parser run configurations for the most common tasks. However, try executing ./gradlew :test first, to download all necessary dependencies and launch all code generation tasks. Unfortunately during import IDEA may delete .idea/runConfigurations, just revert changes in the directory if this happens.

You might want to install the following plugins:

  • Grammar-Kit to get highlighting for the files with BNFish grammar.
  • PsiViewer to view the AST of Rust files right in the IDE.

Contributing

To find a problem to work on, look for help wanted issues on Github, or, even better, try to fix a problem you face yourself when using the plugin.

When choosing an issue, you can navigate by the E- labels. They describe an experience needed to solve an issue (E-easy, E-medium, E-hard or E-unknown). The E-mentor label means that someone knows how to fix the issue and most likely they provided some instructions about how to fix it, links to the relevant code and so on. If you are looking for a good first issue, E-mentor-labeled one is the best choice.

To familiarize yourself with the plugin source code, read the architecture document and look at some existing pull requests. Please do ask questions in our Gitter!

Work in progress pull requests are very welcome! It is also a great way to ask questions.

Here are some example pull requests:

  • Adding an inspection: #713.

  • Adding an intention: #318.

  • Adding a gutter icon: #758.

And also a tutorial series "Contributing to Intellij-Rust" by @Kobzol:

Code style

Please use reformat code action to maintain consistent style. Pay attention to IDEA's warning and suggestions, and try to keep the code green. If you are sure that the warning is false positive, use an annotation to suppress it.

Try to avoid copy-paste and boilerplate as much as possible. For example, proactively use ?: to deal with nullable values.

If you add a new file, please make sure that it contains a license preamble, as all other files do.

Commit Messages

Consider prefixing commit with a TAG: which describes the area of the change. Common tags are:

Tags list
  • GRAM for changes to .bnf files

  • PSI for other PSI related changes

  • RES for name resolution

  • TY for type inference

  • COMP for code completion

  • STUB for PSI stubs

  • MACRO for macro expansion

  • FMT for formatter

  • TYPE for editor-related functions

  • ANN for error highlighting and annotators

  • INSP for inspections

  • INT for intentions

  • REF for refactorings

  • RUN for run configurations

  • QDOC for quick documentation

  • PERF for performance optimizations

  • PRJ for project creation changes

  • ACT for actions

  • DBG for debugger

  • LI for language injections

  • REPL for REPL integration

  • CARGO for cargo-related changes

  • GRD for build changes

  • T for tests

  • DOC for documentation

  • L10N for changes related to localization

Try to keep the summary line of a commit message under 72 characters.

Project structure

Rust plugin sources are divided into several modules. Almost all modules (except root and plugin ones) support some functionality in particular IDE or integrate with another plugin. Like debugging in CLion or integration with TOML plugin.

The main goal is to separate code that can be compiled only with specific dependencies (IDEA, CLion or another plugin) from each other. It helps to avoid accidental using of code from wrong optional dependency. Also, it allows us to compile and run tests of core module with different platforms like IDEA and CLion.

The current Rust plugin modules:

  • : - root/core module
  • :plugin - module to build/run/publish Rust plugin
  • :idea - contains code available only in IDEA
  • :clion - contains code available only in CLion
  • :debugger - debugger related code
  • :profiler - profiler related code
  • :toml - integration with TOML plugin
  • :intelliLang - integration with intelliLang plugin
  • :copyright - integration with copyright plugin
  • :duplicates - support Duplicated code fragment inspection
  • :coverage - integration with coverage plugin
  • :grazie - integration with grazie plugin
  • :js - interop with JavaScript language
  • :ml-completion - integration with Machine Learning Code Completion plugin

If you want to implement integration with another plugin/IDE, you should create a new gradle module for that.

Platform versions

You can build the plugin for different major platform versions. We usually support the latest release platform version and EAPs of the next one. But each plugin artifact is compatible only with single major version. platformVersion property in gradle.properties is used to specify major version the plugin artifact will be compatible with. Supported values are the same as platform branch numbers.

Source code for different platform versions

Sometimes there are not compatible changes in new platform version. To avoid creating several parallel vcs branches for each version, we have separate folders for each version to keep platform dependent code.

For example, current platform version is 181, next version is 182 and org.rust.ide.navigation.goto.RsImplsSearch should have separate implementations for each version. Then project source code structure will be

 +-- src
 |   +-- 181/kotlin
 |       +-- org/rust/ide/navigation/goto
 |           +-- RsImplsSearch.kt
 |   +-- 182/kotlin
 |       +-- org/rust/ide/navigation/goto
 |           +-- RsImplsSearch.kt
 |   +-- main/kotlin
 |       +-- other platfrom independent code

Of course, only one batch of platform dependent code will be used in compilation.

Testing

It is much easier to understand code changes if they are accompanied with tests. Most tests are fixture-driven. They typically:

  1. Load a rust file that represents the initial state
  2. Execute your method under test
  3. Verify the final state, which may also be represented as a fixture

Structure

All test classes are placed in the src/test/kotlin directory. There are two ways of providing fixtures for the tests. The first one is to put Rust files in src/test/resources.

In the example below RsFormatterTest.kt is the test class, blocks.rs is the fixture for the initial state and blocks_after.rs is the fixture for the final state. It is good practice to put fixtures in the same package as tests.

 +-- src/test/kotlin
 |    +-- org/rust/ide/formatter
 |        +-- RsFormatterTest.kt
 |
 +-- src/test/resources
     +-- org/rust/ide/formatter
         +-- fixtures
             +-- blocks.rs
             +-- blocks_after.rs

Another way of providing fixtures is to use Kotlin's triple quoted multiline string literals. You can get Rust syntax highlighting inside them if you have a @Language("Rust") annotation applied. You can see an example here.

In general, triple quoted string fixtures should be preferred over separate Rust files.

Fixtures

Fixture files are very simple: they're rust code! Output fixtures on the other hand, can be rust code over which you've run an action, HTML (for generated documentation) or any other output you'd like to verify. Output fixtures have the same filename as the initial fixture, but with _after appended.

Continuing with our example above, our initial fixture blocks.rs could look like:

pub fn main() {
let x = {
92
};
x;
}

While our expected-output fixture blocks_after.rs contains:

pub fn main() {
    let x = {
        92
    };
    x;
}

Some tests are dependent on the position of the editor caret. Fixtures support a special marker <caret> for this purpose. Multiple such markers for more complex tests. An example of a fixture with a caret:

pub fn main>() {
  let _ = S {
    <caret>
};

Test Classes

Test classes are JUnit and written in Kotlin. They specify the resource path in which fixtures are found and contain a number of test methods. Test methods follow a simple convention: their name is the initial fixture name camel-cased. For example, RsFormatterTest.kt would look like:

class RsFormatterTest : FormatterTestCase() {
    override fun getTestDataPath() = "src/test/resources"
    override fun getFileExtension() = "rs"

    fun testBlocks() = stubOnlyResolve()
}

The test method testBlocks states that this test uses blocks.rs as the initial fixture and blocks_after.rs as the expected output fixture. A more complicated fixture name like a_longer_fixture_name.rs would use the test method testALongerFixtureName().

Pull requests best practices

It's much easier to review small, focused pull requests. If you can split your changes into several pull requests then please do it. There is no such thing as a "too small" pull request.

Here is my typical workflow for submitting a pull request. You don't need to follow it exactly. I will show command line commands, but you can use any git client of your choice.

First, I press the fork button on the GitHub website to fork https://github.com/intellij-rust/intellij-rust to https://github.com/matklad/intellij-rust. Then I clone my fork:

$ git clone git://github.com/matklad/intellij-rust && cd intellij-rust

The next thing is usually creating a branch:

$ git checkout -b "useful-fix"

I can work directly on my fork's master branch, but having a dedicated PR branch helps if I want to synchronize my work with upstream repository or if I want to submit several pull requests.

Usually I try to keep my PRs one commit long:

$ hack hack hack
$ git commit -am"(INSP): add a useful inspection"
$ ./gradlew test && git push -u origin useful-fix

Now I am ready to press "create pull request" button on the GitHub website!

Incorporating code review suggestions

If my pull request consists of a single commit then to address the review I just push additional commits to the pull request branch:

$ more hacking
$ git commit -am "Fix code style issues"
$ ./gradlew test && git push

I don't pay much attention to the commit messages, because after everything is fine the PR will be squash merged as a single good commit.

If my PR consists of several commits, then the situation is a bit tricky. I like to keep the history clean, so I do some form of the rebasing:

$ more hacking
$ git add .
$ git commit --fixup aef92cc
$ git rebase --autosquash -i HEAD~3

And then I force push the branch

$ ./gradlew test && git push --force-with-lease

Updating the pull request to solve merge conflicts

If my PR starts to conflict with the upstream changes, I need to update it. First, I add the original repository as a remote, so that I can pull changes from it.

$ git remote add upstream https://github.com/intellij-rust/intellij-rust
$ git fetch upstream
$ git merge upstream/master master  # The dedicated PR branch helps a lot here.

Then I rebase my work on top of the updated master:

$ git rebase master useful-fix

And now I need to force push the PR branch:

$ ./gradlew test && git push --force-with-lease