The Ur badge: the ziggurat before the gold sun, with UR beneath it

Ur is in preview. The code isn’t public yet. Preview builds are free for Linux, macOS, and Windows. Expect rough edges, and please tell us what breaks.

Ur is a universal linter for every programming language. Install it, set it up in your project, and run one check: Ur fixes what it can and reports the rest. Ur is a single binary with nothing else to install. It needs no account and runs entirely on your machine: a check makes no network connections.

# Install the preview build of Ur
curl -fsSL https://ur.dev/install.sh | sh -s -- --preview
# On Windows, in PowerShell: irm https://ur.dev/install.ps1 | iex
# Set Ur up in this project
ur init
# Lint in one pass, fixing what it can
ur check --fix

Supported languages

  • Java, from 8 to 27.
  • JavaScript and TypeScript: ES2025 and TypeScript 5.9, with JSX and TSX.
  • Lean 4 and TLA+ specifications.
  • Python, from 2.7 to 3.14.
  • Ruby, from 3.3 to 4.0.
  • Rust, every edition from 2015 to 2024.
  • Shell: Bash, POSIX sh, Zsh, and KornShell.
  • SQL: the ANSI, SQLite, MySQL, PostgreSQL, and SparkSQL dialects.
  • Missing your favorite language? Email us.
  • Rust badge: a crab squared up for a fight, claws raised, teeth bared
  • Python badge: the caduceus, with a blue serpent and a gold one, in a laurel wreath
  • JavaScript badge: a grinning black and gold spider waving at the hub of its web, on gold
  • TypeScript badge: the same spider on a white web on blue, helmed and armoured in steel, bearing a shield and a sword
  • Java badge: a steaming cup of coffee on its saucer
  • Ruby badge: a cut ruby on a velvet cushion
  • TLA+ badge: a brass hourglass under the night sky, between the square of always and the diamond of eventually
  • SQL badge: a library card catalogue with one drawer pulled out, an index card rising from it
  • Shell badge: a scallop shell on the sand under the night sea, with a dollar-sign prompt and a blinking cursor
  • Lean badge: the leaning tower, leaning a little further and righting itself, under a gold sun

Duplicated logic

What duplicated logic is hiding in your project? Ur finds functions and blocks that behave the same, however they are written. Here, an index loop and a for...of loop with the factors swapped. Keep one copy and call it from the others (semantic-clone).

# src/cart.js
export function subtotal(items) {
  let sum = 0;
  for (let i = 0; i < items.length; i++) {
    sum += items[i].price * items[i].quantity;
  }
  return sum;
}

# src/invoice.js
export function lineTotal(lines) {
  let total = 0;
  for (const line of lines) {
    total = total + line.quantity * line.price;
  }
  return total;
}
$ ur lint --rule semantic-clone
warning[semantic-clone]: 2 functions have the same behavior
  --> src/cart.js:1:8-7 function `subtotal`
  --> src/invoice.js:1:8-7 function `lineTotal`
  = assumes: dense-arrays, pure-operators
  = hint: keep one copy and call it from the other places
1 clone class in 2 files

No more slop

Ur catches the slop that piles up in a codebase, whether people or coding agents wrote it: a second copy of code that already exists, arithmetic that does nothing, the same data written out twice, and configuration read in the wrong place. Each finding points at the code and gives a one-line hint on how to fix it.

Here is one check of a small project.

$ ur check
ur check: no clean check to compare with; checking all 3 files
warning[semantic-clone]: 2 functions have the same behavior
  --> app/orders.py:4:1-9 function `order_total`
  --> app/report.py:1:1-2 function `calculate_paid_total`
  = assumes: builtins-unshadowed, pure-operators
  = hint: keep one copy and call it from the other places
warning[trivial-expression]: `price * 1` is always `price`
  --> app/orders.py:14:12 expression in `apply_discount`
  = assumes: trusted-annotations
  = hint: replace it with its answer, or fix the operand that was meant; `--fix` does this
warning[duplicated-data]: 2 collections hold the same values: 0.19, 0.2, 0.21, 0.22, 0.23, 0.25
  --> app/orders.py:23:12 list `eu_rate`
  --> app/rates.py:1:12 list `EU_RATES`
  = hint: define the values once and refer to them, or derive one collection from the other
warning[env-file]: environment variable `NOTIFY_URL` is used outside an env file; no env file above it exists for python, so create one, such as app/env.py
  --> app/orders.py:18:11 `os.environ["NOTIFY_URL"]`
  = hint: use the environment only in an env file, and pass the values on from there
ur check: 4 lint findings in 0.1s

Fast and lazy

Ur caches its results by file content in a shared store, so later runs, in any checkout, redo work only for files that changed, and a copy of a checkout reuses the work done in the original.

$ time ur lint . > first.txt  # every file, the first time
real    39.0s
$ time ur lint . > again.txt  # again: it comes from the store
real    12.2s
$ echo 'x = 1' >> shop/text.py
$ time ur lint . > edited.txt  # after a one-line edit
real     8.7s
$ cd .. && cp -r shop copy && cd copy
$ time ur lint . > copy.txt  # another checkout shares the store
real    12.8s

What Ur lints

At the heart of Ur are normalization and its clone engine. Ur lowers each language to one intermediate form and normalizes it, under the rules of the language version your project targets, so code that does the same thing comes out the same however it is written. The clone engine fingerprints that normal form to find every copy, and the other rules read the same form.

These rules run on every check, and ur report lists the opportunities to share code.

Duplicated data

The same literal values written in more than one place (duplicated-data).

# shop/rates.py
EU_RATES = [0.19, 0.2, 0.21, 0.22, 0.23, 0.25]

# shop/tax.py
def rate(i):
    return [0.19, 0.2, 0.21, 0.22, 0.23, 0.25][i]
$ ur lint --rule duplicated-data
warning[duplicated-data]: 2 collections hold the same values: 0.19, 0.2, 0.21, 0.22, 0.23, 0.25
  --> shop/rates.py:1:12 list `EU_RATES`
  --> shop/tax.py:2:12 list `rate`
  = hint: define the values once and refer to them, or derive one collection from the other
0 clone classes and 1 set of duplicated data in 2 files

Trivial expressions

Expressions that always give the same result or return one of their inputs, such as x * 1 or a if c else a. --fix replaces them with the result (trivial-expression).

# shop/price.py
def scale(price: int, count: int):
    return price * 1 + count
$ ur lint --rule trivial-expression
warning[trivial-expression]: `price * 1` is always `price`
  --> shop/price.py:2:12 expression in `scale`
  = assumes: trusted-annotations
  = hint: replace it with its answer, or fix the operand that was meant; `--fix` does this
0 clone classes and 1 trivial expression in 1 file

Tests, based on the Google Testing Blog

Ur’s test rules implement advice from Google’s Testing Blog, mostly from its Testing on the Toilet series. Each finding links to the post its rule is based on. Besides the two below, they catch assertions given only a comparison (boolean-assertion), logic in a test (test-logic), and tests that cover two scenarios (multi-scenario).

A test that checks nothing can only fail by crashing (no-assertion).

# tests/test_parse.py
def test_parses():
    try:
        parse("1 + 2")
    except ParseError:
        pass
$ ur lint --rule no-assertion
warning[no-assertion]: test `test_parses` checks nothing
  --> tests/test_parse.py:1:1-5 test `test_parses`
  = hint: if not crashing is the check, say so with an `ur-allow: no-assertion` comment; otherwise assert on the result of the call the test is about
  = see: Testing on the Toilet: Effective Testing, Google Testing Blog (2014-05-07), https://testing.googleblog.com/2014/05/testing-on-toilet-effective-testing.html
0 clone classes and 1 test finding in 1 file

An assertion that can’t fail. Here, the parentheses make a tuple, and a non-empty tuple is always true (vacuous-assertion).

# tests/test_cart.py
def test_total():
    assert (total([2, 3]) == 5, "total is wrong")
$ ur lint --rule vacuous-assertion
warning[vacuous-assertion]: `assert` is given a non-empty tuple, which is always true, so it cannot fail
  --> tests/test_cart.py:2:5 test `test_total`
  = hint: assert on the result of the code under test, compared with the value it should have
  = see: Testing on the Toilet: Effective Testing, Google Testing Blog (2014-05-07), https://testing.googleblog.com/2014/05/testing-on-toilet-effective-testing.html
0 clone classes and 1 test finding in 1 file

Opportunities

ur report lists repeated code that could be shared, ranked by how many lines sharing it would save: blocks that differ only in a few names or literals (near-miss-clone), recursive walks written out case by case (boilerplate-recursion), the same nested constructor calls at many sites (constructor-boilerplate), the same list of names in several places (parallel-lists), and many ur-allow comments on code of the same shape (suppression-debt). The report is advisory and never fails.

# shop/notify.py
def notify_paid(order, mailer):
    subject = "Order " + order.id + " is paid"
    body = render("paid.txt", order=order, total=order.total)
    mailer.send(order.email, subject, body)
    log.info("sent paid notice for %s", order.id)
    return subject


def notify_shipped(order, mailer):
    subject = "Order " + order.id + " has shipped"
    body = render("shipped.txt", order=order, total=order.total)
    mailer.send(order.email, subject, body)
    log.info("sent shipped notice for %s", order.id)
    return subject


def notify_refunded(order, mailer):
    subject = "Order " + order.id + " is refunded"
    body = render("refunded.txt", order=order, total=order.total)
    mailer.send(order.email, subject, body)
    log.info("sent refunded notice for %s", order.id)
    return subject
$ ur report
opportunity[near-miss-clone]: 3 runs of 5 statements are alike but for 3 values: `" has shipped"` / `" is paid"` / `" is refunded"`; `"paid.txt"` / `"refunded.txt"` / `"shipped.txt"`; `"sent paid notice for %s"` / `"sent refunded notice for %s"` / `"sent shipped notice for %s"` (saves about 6 lines)
  --> shop/notify.py:2:5-6 in `notify_paid`
  --> shop/notify.py:10:5-14 in `notify_shipped`
  --> shop/notify.py:18:5-22 in `notify_refunded`
  = hint: write one function that takes the values that differ as parameters, or a table of them, and use it at each site
1 opportunity saving about 6 lines in all

Agent smells

Coding agents leave the same marks on every codebase they touch. Ur reports configuration read all over the code on every check.

Environment variables

Environment variables read outside the project’s env file, scattered through the code instead of kept in one place (env-file).

# shop/config.py
import os

TIMEOUT = os.environ["SHOP_TIMEOUT"]

# shop/mail.py
import os


def send(to, body):
    host = os.getenv("SMTP_HOST", "localhost")
    key = os.environ.get("SMTP_KEY")
    deliver(host, key, to, body)
$ ur lint --rule env-file
warning[env-file]: environment variable `SHOP_TIMEOUT` is used outside an env file; no env file above it exists for python, so create one, such as shop/env.py
  --> shop/config.py:3:11 `os.environ["SHOP_TIMEOUT"]`
  = hint: use the environment only in an env file, and pass the values on from there
warning[env-file]: environment variable `SMTP_HOST` is used outside an env file; no env file above it exists for python, so create one, such as shop/env.py
  --> shop/mail.py:5:12 `os.getenv("SMTP_HOST", "localhost")`
  = hint: use the environment only in an env file, and pass the values on from there
warning[env-file]: environment variable `SMTP_KEY` is used outside an env file; no env file above it exists for python, so create one, such as shop/env.py
  --> shop/mail.py:6:11 `os.environ.get("SMTP_KEY")`
  = hint: use the environment only in an env file, and pass the values on from there
0 clone classes and 3 uses of the environment outside an env file in 2 files

The rest are an opinionated preset, off by default, for teams that want them: code nested more than eight levels deep (deep-nesting), ordinary comments after a file’s header (no-comments), TODO markers (stray-todo), emoji (emoji), colors and font stacks written out instead of taken from the theme (color, inline-font), inline SVG (inline-svg), ad-hoc motion (motion), and files longer than 1,500 lines (file-length). Turn on the ones your team agrees on.

The preset at work

A comment that narrates the code, a TODO, and an emoji, in four lines.

# app/ship.py
"""Shipping."""


def ship(order):
    # Check if the order is paid
    if order.paid:
        send(order)  # TODO: retry on failure
        print("Shipped! 🚀")
$ ur lint --rule no-comments --rule stray-todo --rule emoji
warning[no-comments]: ordinary comment outside the file's header
  --> app/ship.py:5:5 comment `# Check if the order is paid`
  = hint: make the intent clear through names and structure, or document an API with a documentation comment
warning[no-comments]: ordinary comment outside the file's header
  --> app/ship.py:7:22 comment `# TODO: retry on failure`
  = hint: make the intent clear through names and structure, or document an API with a documentation comment
warning[emoji]: emoji `🚀`
  --> app/ship.py:8:25 emoji `🚀`
  = hint: say it in words
warning[stray-todo]: `TODO` left in the code
  --> app/ship.py:7:24 `TODO`
  = hint: finish the work, or track it where work is tracked
0 clone classes and 2 ordinary comments and 2 text findings in 1 file

The Powderworks Language SDK

Building a good linter for every language meant building a full static analysis toolkit. The Powderworks Language SDK makes that toolkit available for your own tools: static application security testing (SAST), vulnerability and secret scanners, taint analysis, code search, and code review. We maintain the parsers and analyzers, and we add the features our customers need.

Every grammar Ur supports, as Rust libraries. Parse any supported language, in any of its versions and dialects, into the same lossless syntax trees Ur’s linter uses. Each grammar is tested against the language’s reference parser on large real-world codebases.

Commercial license and enterprise support for building products on the SDK, with direct access to the team that builds Ur. Contact info@powderworks.dev.