WolOS
Reference contents

Reference · Apps and the UI kit

Crystal rules (RCR)

The rules every Crystal file in WolOS follows, the standard library policy, the pinned toolchain and the advisories list.

On this page

What it is

The RCR rules are what every Crystal file WolOS ships or tests must follow: a header, no raw pointers outside the binding files, commands as argument arrays, timeouts on the network, pinned shards and one pinned compiler. A test reads the sources and enforces each rule.

Why

A rule a machine checks is kept; a rule people have to remember is not. Each rule has an id and a reason, and an old file that breaks a rule is listed in a baseline that may only shrink.

How

Run wol app check in your project: it applies the same rules. The full table, the standard library policy and the toolchain pin are below.

Cryswol · Crystal shards

Reference

Consistency and safety rules for the Crystal code WolOS ships or tests: wopkg/ (the package tool), hue/ (wol-hue), crates/wol-abi/bindings/crystal/ (the libwol examples) and any .cr tree added later (the test walks the whole checkout, so a new tree is covered the day it appears). Crystal is the userland language (ADR 0036); this catalogue gives it the discipline the shell has (RULES-system.md). Each automated rule is one test in crates/controller/tests/rules_crystal.rs (function name in the Test column). Run: cargo test -p controller --test rules_crystal.

The scanners read files. The only programs the tests start are crystal tool format --check (RCR-004) and ameba (RCR-005, RCR-006). When one is missing the test prints a message and passes; with WOLOS_REQUIRE_CRYSTAL_LINT=1 (CI sets it) a missing tool fails the test instead.

How baselines work

A file that does not meet a rule today is listed in a B_* constant at the top of the test file (status baseline). The list only shrinks: a new violation fails the test; a fixed one is reported on stderr so its entry is removed. B_UNTYPED_PARSE and B_CATCH_ALL are ceilings per file (counts), not lists. A new baseline entry needs a reason in the commit that adds it. Three lists are allowlists, not baselines: FFI_ALLOW (the files that may touch raw memory or declare C bindings, each group with its reason in a comment above the list), ALLOW_CATCH_ALL (files that may hold a fixed number of catch-all rescues, each with a reason) and ALLOWED_SHARDS (the dependencies the repo may have). They change only with an ADR.

What the scanner is: a small lexer that blanks comments, string bodies, regex bodies, percent literals and heredoc bodies, then looks for patterns in what is left (bare) or in the string bodies (strs). It is not a parser. A pattern hidden behind a macro or built from string pieces is not seen; review catches that, and the manual rows below say so.

Specs (spec/ directories and *_spec.cr) are held to every rule except RCR-002, RCR-022, RCR-024, RCR-026, RCR-030 and RCR-035: a spec names its unit in describe, reads its own output, and uses fixed paths as data.

Rules

IDAreaRuleTestStatus
RCR-001fileLine 1 is # SPDX-License-Identifier: <id>.rcr_001_spdx_on_line_oneenforced
RCR-002fileHeader: 40 or more characters of purpose comment (SPDX and notice lines left out) in the first 30 lines of every non-spec file.rcr_002_header_says_what_the_file_is_forenforced
RCR-003fileNo carriage returns, a final newline, no blank line at the end.rcr_003_text_hygieneenforced
RCR-004toolcrystal tool format --check is clean over every .cr file (needs the pinned crystal, RCR-046: the formatter output differs between releases; an older compiler counts as missing).rcr_004_crystal_format_is_cleanenforced
RCR-005toolameba --up-to-version 1.7.0 is clean with the committed .ameba.yml.rcr_005_ameba_is_cleanenforced
RCR-006tool.ameba.yml decides every rule the installed ameba has (Enabled: true or false, no rule left to its default), keeps the scanner fixtures out of Globs, and keeps the lint rules that guard crashes and error handling on. A rule a newer ameba adds fails this test until it is decided.rcr_006_ameba_config_lists_every_rule_explicitlyenforced
RCR-007toolThe Excluded: lists in .ameba.yml (the legacy files that still break a rule) only shrink, and name existing files.rcr_007_ameba_exclusions_only_shrinkbaseline
RCR-010memoryNo raw pointers or unchecked access outside the FFI allowlist: Pointer, pointerof, uninitialized, to_unsafe, unsafe_as/fetch/put/chr, realloc, LibC, .address, Box(T).box/unbox, a cast .as(T*). Allowlist policy: FFI_ALLOW holds the generated bindings, the lib_* files, hue color/image, wopkg core, and the cryswol files callbacks, context, error, handle, info, string and pkg/ffi (reason: libwol handle/callback boundary); a new entry needs an ADR and a reason.rcr_010_unsafe_constructs_stay_in_the_ffi_modulesbaseline
RCR-011memorylib, fun and @[Link] declarations only in the FFI allowlist (FFI_ALLOW); every entry of the allowlist exists.rcr_011_c_bindings_stay_in_the_ffi_modulesenforced
RCR-020processA command is a program plus an argument array. Not allowed: shell: true, sh -c (any shell with -c), a program string that carries arguments or shell syntax, backticks, %x(), system().rcr_020_commands_go_out_as_argvbaseline
RCR-021codeNo code built or loaded at run time: a macro that runs a program ({{ run(...) }}, {{ system(...) }}), dlopen/dlsym, starting crystal itself, requiring the compiler.rcr_021_no_code_built_or_loaded_at_run_timeenforced
RCR-022networkNo HTTP::Client.get/post/... class method (it cannot take timeouts); HTTP::Client.new needs connect_timeout and read_timeout in the file; TCPSocket.new has connect_timeout:.rcr_022_network_clients_set_timeoutsbaseline
RCR-023networkTLS verification stays on: no VerifyMode::NONE, no verify_mode =, no Context::Client.insecure.rcr_023_tls_verification_stays_onenforced
RCR-024inputJSON and YAML from outside decode into types (Type.from_json, from_yaml, JSON::Serializable); JSON.parse, YAML.parse, YAML::Nodes.parse and the pull parsers are counted per non-spec file and the count may only fall.rcr_024_json_and_yaml_decode_into_typesbaseline
RCR-025errorsNothing rescues Exception itself.rcr_025_nothing_rescues_exceptionenforced
RCR-026errorsA rescue names its exception types. Counted per non-spec file, may only fall: bare rescue, rescue e with no type, and the modifier expr rescue value. Exception: ALLOW_CATCH_ALL, an allowlist with a reason and a maximum count per file (not a baseline): cryswol src/wol/callbacks.cr, 3 rescues, because a callback must not unwind into C and exceptions are routed to Wol::Loop.on_error; a fourth fails.rcr_026_rescues_name_their_exceptionsbaseline
RCR-030filesNo fixed /tmp/, /var/tmp/ or /dev/shm/ path in a string and no File.join(Dir.tempdir, "fixed"); use File.tempfile (created exclusively) or a directory the caller owns.rcr_030_no_fixed_tmp_pathsbaseline
RCR-031filesNothing is created or made world-writable: no mode whose last octal digit allows write (0o777, 0o666, 0o722) next to chmod/perm/mkdir/File.new/File.open/File.write, no OtherWrite, no Permissions::All, no "777"/a+w/o+w argument.rcr_031_nothing_is_made_world_writableenforced
RCR-032cryptoNo MD5, MD4 or SHA-1 (Digest::MD5, Digest::SHA1, require "digest/md5", OpenSSL::Digest.new("SHA1")). Use SHA-256 or better.rcr_032_no_md5_or_sha1baseline
RCR-033cryptoTokens, nonces and keys come from Random::Secure; Random.new, Random.rand, Random::DEFAULT, rand( are refused.rcr_033_secrets_use_the_secure_randombaseline
RCR-034secretsNo literal credential assigned to a name containing password, passwd, passphrase, secret, token, api_key or private_key; no pasted private key, ghp_ or AKIA token.rcr_034_no_literal_credentialsenforced
RCR-035portabilityNo /home/ or /Users/ literal in a string of non-spec code.rcr_035_no_home_directory_literalsbaseline
RCR-040supplyshard.yml has name, version, crystal and license; every dependency (and development dependency) is pinned by commit: with the full 40-character hash. No version: range, branch:, tag: or path: (a tag moves; a range resolves to whatever is newest).rcr_040_shard_dependencies_are_pinned_by_commitenforced
RCR-041supplyA shard.yml with dependencies has a committed shard.lock that names each of them and carries the same commit (+git.commit.<sha>).rcr_041_shard_lock_is_committed_and_matchesenforced
RCR-042supplyDependency census: the shards the checkout uses (shard.yml entries, shard.lock entries, vendored shards under a lib/ directory) are in ALLOWED_SHARDS and not more than SHARD_CENSUS. Today both hold one shard, cryswol (WolOS's own, pinned by the wolkit shard at the commit of wolos-shards/curated.yml); a third-party shard is added with an ADR, in ALLOWED_SHARDS and in THIRD-PARTY.md.rcr_042_shard_census_only_fallsenforced
RCR-043supplyA require names the standard library (STDLIB) or a relative file of this repo; anything else is a shard nobody counted.rcr_043_requires_are_stdlib_or_in_repoenforced
RCR-044supplyNo build file (PKGBUILD, Makefile, .sh, workflow, .install) runs shards install/update/build/check/run/prune without --frozen --local: the build never fetches shards from the network. A shard that is ever needed arrives as a vendored source in source=(...) with a real checksum (not SKIP) and is built with shards install --frozen --local.rcr_044_build_files_do_not_fetch_shardsenforced
RCR-045supplyNo entry of a shard.lock in the checkout and no toolchain package recorded in tools/crystal/toolchain.json (crystal, gc, llvm-libs) is older than the fixed_in of an advisory for it in tools/crystal/advisories.json (an array of {"package", "fixed_in", "note"}, exactly those keys, sorted). See "Toolchain and advisories".rcr_045_no_locked_shard_or_toolchain_package_is_below_an_advisoryenforced
RCR-046supplyOne Crystal version is pinned, in tools/crystal/toolchain.json (crystal >= 1.22.0): every shard.yml asks for crystal: ">= <pin>", every recipe that depends on crystal says crystal>=<pin>, the snapshot's compiler is on the pinned line, the snapshot date matches the recipes', tools/crystal/lint.sh reads the pin, and no document or workflow names another Crystal floor.rcr_046_one_crystal_version_is_pinned_and_read_everywherebaseline
RCR-050templateThe templates wol app new copies (wolos-crystal/templates/{app,cli,tool,ai}, the list in crates/controller/src/app.rs TEMPLATES) each have shard.yml, Makefile (with all, spec, check, format and run), README.md and .gitignore; every file ends in .tmpl (so the repo's checks never read it as code) and uses only the I7 placeholders {{id}}, {{name}}, {{module}} (the app template also the wolkit generator's {{class_name}}, {{app_id}}).rcr_050_templates_use_only_the_i7_placeholdersenforced
RCR-051template-Dgc_none (no garbage collector) appears in a build line only in the cli template's Makefile: a short command may skip the collector, a program with a window, a libwol callback or a loop may not (ADR 0036). Comments do not count. wol app check applies it to a project that requires cryswol or wolkit.rcr_051_gc_none_only_in_the_cli_templateenforced
RCR-052templateEvery public method (def, def self., abstract def; not private or protected) of a generated project's non-spec code has a doc comment on the line above it (annotations may sit between). Checked on every rendered template and by wol app check; fixtures fixtures/crystal/app/.rcr_052_template_public_methods_are_documentedenforced
RCR-053templatewol app check finds nothing in a project fresh from any template: the per-file rules above (RCR-043 also allows the system shards cryswol and wolkit and the shards shard.yml pins), RCR-040/041 on shard.yml, RCR-044 on the build files, RCR-051 and RCR-052.rcr_053_wol_app_check_passes_every_templateenforced
RCR-090metaEvery scanner has a bad fixture under crates/controller/tests/fixtures/crystal/ that trips exactly the rule on its # trips: line; the good fixtures trip nothing; with crystal and ameba installed good.cr is clean and bad-format.cr/bad-ameba.cr are not.rcr_090_fixtures_trip_exactly_their_ruleenforced
RCR-091metaThis document and the rcr_nnn_* tests list the same rules, each documented test exists, and every baseline constant is named here.rcr_091_doc_and_tests_agreeenforced
RCR-092metaBaselines, allowlists and the stdlib list are sorted and free of duplicates.rcr_092_baselines_are_sorted_and_uniqueenforced
RCR-093metaThe walk finds the Crystal trees (at least 39 files; wopkg, hue and crates).rcr_093_every_crystal_file_is_foundenforced

Standard library and security policy

Crystal programs in WolOS use the standard library of the pinned toolchain and nothing else unless a shard is counted (RCR-042, ALLOWED_SHARDS). A require of a name outside STDLIB (in crates/controller/tests/support/crystal_scan.rs) is a shard (RCR-043). The table below is that list with the rule each part carries; rcr_091_doc_and_tests_agree fails when the table and STDLIB differ.

KindModulesPolicy
stdlibannotations, array, big, bit_array, bool, box, char, class, comparable, complex, deque, enum, enumerable, float, hash, indexable, int, iterable, iterator, levenshtein, math, named_tuple, nil, number, object, proc, range, reference, semantic_version, set, slice, static_array, string, string_pool, string_scanner, struct, symbol, tuple, unicode, union, value, weak_refValues and collections. No rule beyond the general ones. Box(T).box/unbox only in the FFI allowlist (RCR-010).
stdlibpointerRaw memory: only in the FFI_ALLOW files (RCR-010, RCR-011).
stdlibatomic, channel, concurrent, fiber, mutexConcurrency through execution contexts (ADR 0036): view state on the UI context only, blocking work in Wol.background.
stdlibbase64, compress, csv, ecr, html, ini, json, mime, uri, uuid, xml, yamlData from outside decodes into types (Type.from_json, from_yaml, JSON::Serializable), never JSON.parse/YAML.parse (RCR-024); input and archives are read with a size limit, Compress readers with a byte budget (RCR-191); ecr templates are compiled in, never loaded at run time (RCR-021).
stdlibhttp, oauth, oauth2, openssl, socketNo HTTP::Client.get/post/... class methods; a client sets connect_timeout and read_timeout, a TCPSocket its connect_timeout (RCR-022); TLS verification stays on (RCR-023).
stdlibcrypto, digest, randomSHA-256 or better, no MD5 or SHA-1 (RCR-032); tokens, nonces and keys from Random::Secure (RCR-033); no literal secrets (RCR-034).
stdlibdir, env, errno, file, file_utils, io, path, process, signal, system, termiosA command is a program and an argument array (RCR-020) with a timeout (RCR-192); File.tempfile, no fixed /tmp names (RCR-030); nothing world-writable (RCR-031); paths from outside are checked (RCR-190).
stdlibbenchmark, colorize, crystal, exception, gc, humanize, kernel, log, option_parser, prelude, pretty_print, regex, timeRuntime and tools. Errors are rescued by type (RCR-025, RCR-026). A regex built from outside input is checked first (RCR-190).
stdlibcompiler, specspec only in specs. compiler is named so RCR-043 recognises it; requiring it in a program is refused by RCR-021 (no code built at run time).

Toolchain and advisories

One pin. tools/crystal/toolchain.json holds the one Crystal version WolOS builds and tests with: crystal >= 1.22.0 ("crystal": "1.22.0", the only release tested; the snapshot ships 1.22.2). Everything else reads it or is checked against it by RCR-046: every shard.yml asks for crystal: ">= 1.22.0" (baseline B_CRYSTAL_FLOOR, empty), the recipes depend on crystal>=1.22.0 (baseline B_RECIPE_PIN, empty; checked when the sibling wolos-pkgbuilds checkout is there), crates/wol-abi/tests/bindings.rs reads the pin for the shard specs, tools/crystal/lint.sh (and so CI) treats an older compiler as missing, RCR-004 takes the pin as its formatter floor, and no document or workflow names another floor. The file also records the distribution snapshot the recipes build on ("snapshot", the same date as wolos-pkgbuilds/snapshot) and the versions of crystal, gc and llvm-libs it ships ("packages").

Advisories. tools/crystal/advisories.json is an array of {"package", "fixed_in", "note"} (schema frozen; sorted by package, then version). RCR-045 compares every entry of every shard.lock in the checkout and every packages version of the toolchain file with it: a version below fixed_in (package-manager order, [epoch:]pkgver[-pkgrel]; a shard.lock +git.commit.<sha> suffix is not part of the version) fails. The fix for a toolchain package is a newer snapshot plus a pkgrel bump of every recipe that compiles Crystal (wopkg, wolos-hue, the apps), because the standard library and the collector are compiled into each program; for a shard it is a new pin and lock. The procedure is in tools/crystal/README.md. This replaces the manual rule RCR-193.

Not automated

IDAreaRuleTestStatus
RCR-190inputA value that reaches a path, an argument or a regex from outside (a package name, a repo name, a file from a download) is validated against .., absolute roots and a character set before use. The scanner cannot see data flow.nonemanual
RCR-191inputBodies and archives from the network are read with a size limit (IO::Sized, Compress readers with a byte budget); a read timeout does not bound size.nonemanual
RCR-192processA child process gets a timeout or is killed on cancel; its environment is cleared or built (clear_env: true) where it runs foreign code.nonemanual
RCR-194toolWhether a flagged pattern is really unsafe (the scanners are heuristics, not a parser).nonemanual
RCR-195toolSpecs are run in the package check() (crystal spec) and on the build farm; this catalogue reads the code, it does not run it.nonemanual

Known-vulnerable patterns

The classes of bug that become CVEs in programs like these, what they look like in Crystal, and the rule that holds the line. CWE numbers are the weakness class.

PatternClassWhy it hurtsSafe formRule
`cmd #{x}`, system("cmd #{x}"), Process.run("sh", ["-c", "... #{x}"]), Process.run(cmd, shell: true)command injection (CWE-78)the text after ;, $(...) or a newline in x runsProcess.run("prog", ["--flag", x]); validate x firstRCR-020
Pointer, to_unsafe, unsafe_as, .as(Void*), Box.unbox outside the bindingsmemory corruption (CWE-119, CWE-787)the compiler's checks stop at the pointerkeep raw memory in the FFI_ALLOW modules and hand out typed Crystal valuesRCR-010, RCR-011
HTTP::Client.get(url)no time limit, hangs (CWE-400)a server that never answers holds the processa client with connect_timeout and read_timeoutRCR-022
verify_mode = VerifyMode::NONE, Context::Client.insecurecertificate not checked (CWE-295)any machine on the path can pose as the serverthe default contextRCR-023
JSON.parse(body), YAML.parse(text) on input from outsideuntyped decode (CWE-502, CWE-20)any shape is accepted, a wrong type crashes later, YAML aliases can blow up memoryType.from_json(body) into a JSON::Serializable struct that rejects what it does not expectRCR-024
rescue Exception, bare rescue, expr rescue nilswallowed failure (CWE-755, CWE-390)a failed signature check or write looks like successrescue ex : File::Error | JSON::ParseException and a decision (fail closed or tell the user)RCR-025, RCR-026
File.write("/tmp/name", ...)predictable temporary file (CWE-377)another user plants a symlink firstFile.tempfileRCR-030
File.chmod(p, 0o777), "a+w"wrong permissions (CWE-732)any local user rewrites the file0o600, 0o644, 0o755RCR-031
Digest::MD5, Digest::SHA1weak hash (CWE-328)collisions are cheapDigest::SHA256RCR-032
Random.rand, Random.new for a tokenpredictable value (CWE-338)the generator state can be guessedRandom::Secure.hexRCR-033
password = "hunter2"hard-coded credential (CWE-798)it is in every copy of the sourceread it from the user's keyring or askRCR-034
shards install in a PKGBUILD, ~> 1.2 or a tag in shard.ymlunpinned dependency (CWE-1357, CWE-494)the build gets whatever the network serves todaycommit: pin, shard.lock, vendored source with a checksumRCR-040 to RCR-044
{{ run("gen.cr") }}, crystal eval at run time, dlopencode from a file or text chosen at run time (CWE-829)whoever writes the file runs codebuild-time generation in make, committed outputRCR-021
File.read("#{dir}/#{name}") with name from outsidepath traversal (CWE-22)../../etc/shadowcheck name against a pattern, then File.join and a prefix testRCR-190 (manual)

Baselines today

Counts found when the catalogue was written (61 .cr files: wopkg 35, hue 22, bindings 4; formatter crystal 1.22.2 and ameba 1.7.0 on the build box):

  • RCR-001, RCR-003, RCR-021, RCR-023, RCR-025, RCR-031, RCR-032, RCR-033, RCR-034, RCR-035, RCR-043, RCR-044: 0 findings.
  • RCR-002: B_PURPOSE, 0 (the 18 spec files are not asked).
  • RCR-007: B_AMEBA_EXCLUDED, 51 pairs of rule and file, over 29 files. A pair goes when the file is fixed. Rules in the list: Lint/NotNil, Lint/NotNilAfterNoBang, Lint/PercentArrays, Lint/ShadowingOuterLocalVar, Lint/SpecEqWithBoolOrNilLiteral, Lint/UnusedArgument, Lint/WhitespaceAroundMacroExpression, Naming/QueryBoolMethods, Performance/ChainedCallWithNoBang, Style/NegatedConditionsInUnless, Style/ParenthesesAroundCondition, Style/RedundantNilInControlExpression, Style/VerboseBlock. Three style rules are off for the whole tree with a reason in .ameba.yml (Naming/BlockParameterName, Style/PercentLiteralDelimiters, Metrics/CyclomaticComplexity).
  • RCR-010: B_UNSAFE, 2 files (hue/spec/perf_spec.cr: 4 findings, wopkg/src/wopkg/sandbox.cr: 1, LibC.getuid). The 7 allowlisted files hold the other 199 findings (the bindings 145, hue/src/hue/image.cr 13, core.cr 23, lib_wopkg.cr 18).
  • RCR-011: FFI_ALLOW, 9 entries (bindings directory, hue/src/hue/color.cr, hue/src/hue/image.cr, the cryswol generated links spec directory and the generated lib_* files of cryswol src/wol, src/wol/pkg and src/wol/realtime, wopkg/src/wopkg/core.cr, wopkg/src/wopkg/lib_wopkg.cr); 180 declarations, all inside it.
  • RCR-020: B_PROCESS_SHELL, 1 (wopkg/spec/completions_spec.cr runs bash -c on a fixed script).
  • RCR-022: B_HTTP_TIMEOUT, 2 (wopkg/src/wopkg/app.cr and aur.cr call HTTP::Client.get).
  • RCR-024: B_UNTYPED_PARSE, 15 untyped parses in 9 non-spec files (app.cr 5, aur.cr 2, core.cr 2, and one each in aur_guard.cr, aur_hook.cr, scan.cr, hue cli.cr, engine.cr, imports.cr); the specs add 17 more and are not asked.
  • RCR-026: B_CATCH_ALL, 18 catch-all rescues in 6 files (wopkg app.cr 10, aur_hook.cr 3, aur.cr 2, aur_guard.cr 1, sources.cr 1, hue theme.cr 1).
  • RCR-030: B_TMP, 1 file (wopkg/src/wopkg/sandbox.cr, 3 strings: the /tmp mount point and TMPDIR inside the sandbox, which is a fresh tmpfs); 8 more in specs, not asked.
  • RCR-032: B_WEAK_HASH, 0. RCR-033: B_RANDOM, 0. RCR-035: B_HOME, 0.
  • RCR-040 to RCR-042: 2 shard.yml files (wopkg, hue), 0 dependencies, 0 lock files needed. ALLOWED_SHARDS is empty and SHARD_CENSUS is 0: the Crystal code uses the standard library and libwopkg only. Since wave 3 (CR-5) the wolkit shard pins cryswol by commit with a committed shard.lock: ALLOWED_SHARDS is ["cryswol"] and SHARD_CENSUS is 1.
  • RCR-045: advisories.json is empty, 0 shard.lock files in the checkout; the toolchain record is crystal 1.22.2-1, gc 8.2.12-1, llvm-libs 23.1.4-1 (snapshot 2026-10-31).
  • RCR-046: B_CRYSTAL_FLOOR, 0 (all 6 shard.yml files ask for the pin); B_RECIPE_PIN, 0 (the 5 recipes that depend on crystal say crystal>=1.22.0).

Projects: wol app check

wol app check [dir] (crates/controller/src/app.rs) runs the same scanners over one project on a WolOS machine: every .cr file outside bin/, build/, lib/, src/gen/ and hidden folders, with the spec exemptions above, plus RCR-040, RCR-041, RCR-044, RCR-051 and RCR-052. It prints file:line: RCR-nnn why and exits 1 on any finding; with none it type-checks through make check (--rules: the files only; --json: the findings as JSON). There are no baselines for a project: a finding is fixed, not listed. The lexer and scanners are one copy, crates/controller/tests/support/crystal_scan.rs, which the controller library includes, so a rule changed for the repo changes for projects too.

Tools

  • tools/crystal/toolchain.json is the Crystal pin and the snapshot's toolchain record (RCR-046); tools/crystal/advisories.json the advisory list (RCR-045); tools/crystal/README.md says what to do when an advisory comes out or the pin moves.
  • tools/crystal/lint.sh runs crystal tool format --check and ameba (pinned with --up-to-version) over the tracked and new .cr files, fixtures left out. The same files are checked by RCR-004 and RCR-005 in cargo test.
  • .ameba.yml is the rule set. Edit it with the rule list in front of you (ameba --rules); never rely on a default.
  • Installing ameba: CI builds it from a pinned commit of its source (crystal build --no-debug -o ameba src/cli.cr, about 15 seconds, no shards; the step is in .github/workflows/ci.yml). Arch's ameba 1.7.0 package crashed at start with Crystal 1.22 on the build box ("Arithmetic overflow"), so RCR-005 and RCR-006 treat a binary that will not print --version as missing. Moving to a newer ameba: build it, run ameba --rules, decide each new rule in .ameba.yml, then change AMEBA_VERSION in rules_crystal.rs and tools/crystal/lint.sh and the commit in ci.yml together.

Adding a rule

  1. Add fn scan_name(s: &Src) -> Vec<Finding> to rules_crystal.rs, list it in FILE_SCANNERS, and add fn rcr_nnn_name() (next free id in the area).
  2. Add its row above (ID, Area, Rule, Test = the function name, Status).
  3. Add a bad-*.cr fixture with a # trips: RCR-nnn line (and keep good.cr clean). rcr_090 fails without it.
  4. If the code fails it today, list the files in a new B_* constant, add it to rcr_092 and name it under "Baselines today", and mark the row baseline.

rcr_091_doc_and_tests_agree fails when the document and the tests drift apart.

From the developer documentation, wonderlandos/wolos docs/RULES-crystal.md.