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 | iexirm https://ur.dev/install.ps1 | iex
# On Linux or macOS: curl -fsSL https://ur.dev/install.sh | sh -s -- --preview
# 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.
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.