Skip to content
This repository has been archived by the owner on Oct 31, 2023. It is now read-only.

Latest commit

 

History

History
113 lines (85 loc) · 5.07 KB

CONTRIBUTING.md

File metadata and controls

113 lines (85 loc) · 5.07 KB

Contributing

Implementing language support

To add am_list support for a new language, you mostly need to know the usage patterns of autometrics in this language, and understand how to make and use tree-sitter queries.

Function detection architecture

The existing implementations can also be used as a stencil to add a new language. They all follow the same pattern:

  • a src/{lang}.rs which implements walking down a directory and collecting the ExpectedAmLabel labels on the files it traverses. This is the location where the crate::ListAmFunctions trait is implemented.
  • a src/{lang}/queries.rs which is a wrapper around a tree-sitter query, using the rust bindings.
  • a runtime/queries/{lang} folder which contains the actual queries being passed to tree-sitter:
    • the .scm files are direct queries, ready to be used "as-is",
    • the .scm.tpl files are format strings, which are meant to be templated at runtime according to other detection happening in the file. For example, when autometrics is a wrapper function that can be renamed in a file (import { autometrics as am } from '@autometrics/autometrics' in typescript), we need to change the exact query to detect wrapper usage in the file.

Building queries

The hardest part about supporting a language is to build the queries for it. The best resources to create those are on the main website of tree-sitter library:

  • the help section on Query syntax helps understand how to create queries, and
  • the playground allows to paste some text and see both the syntax tree and the current highlight from a query. This enables creating queries incrementally.

Screenshot of the Tree-sitter playground on their website

If you're using Neovim, it also has a very good playground plugin you can use.

The goal is to test the query on the common usage patterns of autometrics, and make sure that you can match the function names. There are non-trivial limitations about what a tree-sitter query can and cannot match, so if you're blocked, don't hesitate to create a discussion or an issue to ask a question, and we will try to solve the problem together.

Using queries

Once you have you query, it's easier to wrap it in a Rust structure to keep track of the indices of the "captures" created within the query. It is best to look at how the queries are used in the existing implementations and try to replicate this, seeing how the bindings are used. In order of complexity, you should look at:

  • the Go implementation, which has a very simple query.
  • the Typescript implementation, which uses the result of a first query to dynamically create a second one, and collect the results of everything.
  • the Rust implementation, which uses dynamically created queries, but also uses a recursion pattern to keep track of modules declared in-file.

Release management

The complete release cycle is handled and automated by the combination of cargo-dist and cargo-release.

When ready for a release, using cargo release --(patch|minor) or cargo release NEW_MAJOR should be enough.

Prerequisites

Only cargo-release is needed to create a new release:

$ cargo install --locked cargo-release

Also, you need to have the authorization to both:

  • create and push tags on release/* branches in the Github repo
  • create and push crates on crates-io registry for am_list crate

Release a breaking 0.Y version

  • Create a branch release/0.Y from main
  • Run cargo release 0.Y.0 and make sure everything is okay

If everything went ok

  • Run the same command, but with --execute flag

If something went wrong

  • Nothing got actually published, so you can reset the release/0.Y branch to main,
  • Then make and commit the changes on your release/0.Y branch (that is still local),
  • Then try the cargo release 0.Y.0 again.

Release a new version on a release branch

In the example, we will push a new 0.2.3 version on 0.2

  • Create a release branch rel_0.2.3 with all the changes that need to be included
    • new features are cherry-picked from main
    • bugfixes happen directly on the release branch (and later get cherry-picked to main if relevant)
  • Push the new branch rel_0.2.3 and create a PR to release/0.2
  • Prepare the release: cargo release --no-publish --no-tag --allow-branch=rel_0.2.3 patch
  • Cleanup the CHANGELOG.md that got bad replacement patterns because of the 2-step process
  • Push the new commits, and merge the PR
  • Switch locally to release/0.2 (the "main" release branch), and pull the latest changes
  • Publish the tag and the new crate: cargo release publish --execute && cargo release tag --execute && cargo release push --execute

Bonus:

  • Merge the CHANGELOG/README/CONTRIBUTING back from release/0.2 to main