Svelixir.Hash (svelixir v0.11.0)

Copy Markdown

Content hashing for manifest file entries.

For .ex/.exs, normalisation is Code.string_to_quoted!/2 |> Macro.to_string/1: the parser discards comments and the printer canonicalises whitespace, so reindentation and an added # credo:disable-for-next-line produce the SAME hash. That is the guarantee the whole design exists for.

Sourceror is the wrong tool and was measured: Sourceror.to_string/1 reproduces comments verbatim (they live in leading_comments/ trailing_comments metadata), and Sourceror.Comments.extract_comments/1 also fails, because removing a comment node removes its previous_eol_count and changes blank-line rendering. Hashing the AST term directly (:erlang.term_to_binary/1, :erlang.phash2/1, inspect/1) is likewise wrong: line: metadata SHIFTS when comments are added. The comment-insensitivity comes from Macro.to_string/1 discarding metadata, not from the parser.

The normalised string is a HASH INPUT ONLY

It is not guaranteed to be valid Elixir — a string literal containing both interpolation and a \r escape is rendered with a raw carriage return that will not re-parse — and Macro.to_string/1 is not idempotent. Never write it to disk, never re-parse it, never re-normalise it.

Known MVP limitations

Non-Elixir files stay comment-SENSITIVE: a # added by user in a compose.yaml DOES change its hash. Semantics-preserving literal rewrites collide for Elixir files (0x1F vs 31, ?a vs 97, 1_000_000 vs 1000000, heredoc vs escaped string). An empty file and a comments-only file share a hash.

Whitespace inside a multi-line sigil or heredoc is literal content, not layout — a sigil keeps its interior verbatim and a heredoc measures its interior against the closing delimiter — so reindenting a ~w(...) block, an ~H template or an indented code sample inside a @moduledoc DOES change the hash. mix format preserves those interiors; a manual or editor block reindent does not.

This module must NOT share a normaliser with Svelixir.Baseline, which requires the opposite semantics.

Summary

Functions

Returns "sha256:" followed by 64 lowercase hex characters.

Normalises file bytes for hashing, dispatching strictly on the extension.

Functions

content_hash(content, path)

@spec content_hash(binary(), Path.t()) :: binary()

Returns "sha256:" followed by 64 lowercase hex characters.

normalise(content, path)

@spec normalise(binary(), Path.t()) :: binary()

Normalises file bytes for hashing, dispatching strictly on the extension.