# Contributing

Contributions are very much welcome!

JuliaFormatter is a pretty difficult project to get into.
Conceptually there is not _that_ much going on: the [How It Works](@ref how-it-works) page has a high-level overview of the different stages of formatting.
However, the complexity comes from the fact that:

- Some parts of Julia's syntax itself can be quite complex! Array literals for example are a massive headache for formatters.

- JuliaSyntax's CST contains quite a few quirks and edge cases.

- Changing the formatting of one node can often have knock-on effects on other places especially with more complex examples.

I'm happy to help with any questions about how best to implement stuff, although please note I'm also finding my way around the codebase myself, so I might not always have the best answer.

In this page I've attempted to document some assorted bits of information that may be useful if you are contributing to JuliaFormatter.

## The `JuliaFormatter.Internal` module

JuliaFormatter contains some useful utilities which are explicitly marked as internal (i.e., not semver-compliant), inside the `JuliaFormatter.Internal` module.

There are currently two functions.
`format_to_stage` is more of a helper function for investigating what goes on during the formatting process.
However, `test_format` is used extensively in the test suite.
**Please note that all new tests that check the output of formatting should be written using `test_format`.**

```@docs
JuliaFormatter.Internal.format_to_stage
JuliaFormatter.Internal.test_format
```

## Disabling precompilation for development

JuliaFormatter contains a nontrivial precompilation workload, which can be quite annoying to run locally when iterating on changes.
To disable it, consider adding the following to `LocalPreferences.toml`.
The file belongs in the directory corresponding to your Julia environment.
For example, if you use `julia --project=/path/to/JuliaFormatter`, then the file should be located at `/path/to/JuliaFormatter/LocalPreferences.toml`:

```toml
[JuliaFormatter]
precompile_workload = false
```

(This suggestion is taken from [the PrecompileTools.jl docs](https://julialang.github.io/PrecompileTools.jl/stable/#Package-developers:-reducing-the-cost-of-precompilation-during-development).)

## Installing an MCP tool

If you are actively developing JuliaFormatter and are using agentic AI to investigate bugs, you likely want to install an MCP tool or similar which the agent can use to run JuliaFormatter.

In particular, I have [a fork of `julia-mcp`](https://github.com/penelopeysm/julia-format-mcp) which includes an additional `julia-format` tool specifically for running JuliaFormatter.
JuliaFormatter contains an `AGENTS.md` file which instructs your agent to use this tool.

To set this up, run the following commands:

```bash
# You can clone it anywhere you like, but I like to keep it inside ~/.julia.
git clone https://github.com/penelopeysm/julia-format-mcp ~/.julia/julia-format-mcp
claude mcp add --scope user julia -- uv run --directory ~/.julia/julia-format-mcp python server.py
```

For other harnesses, please see the [julia-mcp docs](https://github.com/aplavin/julia-mcp#usage) for corresponding instructions.

Claude Code (as of v2.1.177) has [an annoying issue](https://github.com/anthropics/claude-code/issues/45839) where results from the MCP tool are only displayed to the user in JSON, which is impossible to read whenever multiline strings are involved.
To get around this, you can install a post-tool hook which prints the tool's results directly into your Claude Code session.
If you are using the fork above, you can add this block to your `~/.claude/settings.json` file:

```json
{
  ...,
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "mcp__julia__julia_eval|mcp__julia__julia_format",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_response | fromjson | (.content[0].text // .result)' | jq -Rs '{systemMessage: (\"\\n\" + .)}' 2>/dev/null || true"
          }
        ]
      }
    ]
  },
  ...,
}
```

and every time `julia_eval` or `julia_format` is called, the hook will print whatever output Julia generated.

## GitHub-specific things

### FormatBot

JuliaFormatter has a small Python script in `.github/scripts/formatbot.py`, whose job is to compare differences between two revisions of JuliaFormatter.
It can be invoked as follows:

```
python3 formatbot.py
    owner/repo[@REV]
    [--base-ref BASE_REF]
    [jlfmt_options...]
```

This script **must** be run from a local Git checkout of JuliaFormatter, as that will be used as the 'new' version of JuliaFormatter.
It will then:

1. Clones the target repository at the specified revision (or the default branch if no revision is specified).

1. Uses the current local checkout of JuliaFormatter to format the target repository.
   The changes are saved to a branch which we'll call `fmt` (the script technically generates a random name for this branch to avoid collisions).

1. Resets the target repository to the unformatted code if needed.

1. **If `--base-ref` is given**, clones JuliaFormatter at that specified revision into a tempdir, and uses that to format the target repository.
   The changes are saved to a branch, which again technically has a random name, but which we'll call `basefmt`.

   **If `--base-ref` is not given**, the `basefmt` branch is simply the unformatted code.

1. Diffs the `fmt` branch against `basefmt`.

FormatBot will also include a warning if formatting is not idempotent, or fails.

All other command-line options are passed directly to the `jlfmt` app.

This script can also be invoked on GitHub by posting a comment on a PR with the following format:

```
!formatbot owner/repo[@REV] [jlfmt_options...]
```

In this case, the base revision is always the target branch of the PR, and the formatting is checked against the head commit of the PR.
Once the script has finished, it will post a comment on the PR with the results.

### [Issue triage](@id issue-triage)

Issues (and sometimes PRs) will be assigned a priority label, with a larger number indicating more important.
The exact label for an issue is left to my discretion, but generally these are the categories that they will fall into:

- **P5** - silently changes the meaning of code. This is absolutely terrible!

- **P4** - fails to format code (still bad, but at least this errors loudly). Non-idempotent formatting also falls into this category.

- **P3** - fails to respect formatting options

- **P2** - formatting is ugly but not a bug *per se*, or feature requests in general

- **P1** - really small things, like typos

Additionally where possible I'll also add **good first issue** tags for changes that are quite self-contained.
These may include any priority level but should generally be P3 or lower.

You can see a list of all triaged issues [here](https://github.com/JuliaEditorSupport/JuliaFormatter.jl/issues?q=is%3Aissue%20state%3Aopen%20label%3AP5%2CP4%2CP3%2CP2%2CP1).

## AI usage

LLMs and AI in general can be really useful for tracing through some of the logic in the codebase.
(I use them a lot myself!)
However, my experience is that they are still quite poor at design choices:

- They tend to chase short-term gains at the expense of long-term maintainability (i.e., they're happy to fix a bug by inserting hacky code).

- Unless prompted carefully, they won't recognise the effect that a change in one part of the codebase will have on other parts.

Essentially, LLMs if not used carefully are a great path into a _local minimum_.

Please do feel free to use LLMs to help explore the codebase, but when making PRs please consider the above points.

I don't think it's sensible (or indeed possible) completely ban LLM usage, but maintaining an open-source project is difficult and my experience is that LLM contributions tend to be [extractive (in the words of the LLVM project)](https://llvm.org/docs/AIToolPolicy.html).
I don't want to spend all my time cleaning up LLM stuff.
**Therefore, PRs which are clearly LLM-generated, and additionally do not show evidence of human engagement or are too big to be easily reviewed will be rejected.**

If you're just getting started with the codebase and want to help out but don't know where to start, please do just reach out.
I'm genuinely very happy to help *people* with this.
But please don't just ask an LLM to make a PR for you.

Finally, following the LLVM policy, **it is also forbidden to use LLMs to fix good first issues**.
