From 2cc3e91e789b3a063685bfed355db90c8084ca29 Mon Sep 17 00:00:00 2001 From: Andreas Solleder Date: Thu, 4 Jun 2026 00:03:15 +0200 Subject: [PATCH] fix(samples): de-overfit no_implicit_null_initial behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #30's action comment flagged inflector.ex detect_casing/1 (a pure cond-dispatch with no nil-initialization) as a CRITICAL no_implicit_null_initial block. Unlike the file-invariant phantoms fixed in #31, this was a genuine per-block outlier — caused by the same overfitting root cause: the behavior had ONE good/bad sample pair but a ~105-dimension scalar vector, so it matched dense cond/case blocks by chance. Added 3 good/bad Elixir sample pairs (pricing, session, router), each contrasting nil-init-then-branch (bad) against direct assignment via case/with/function results (good), and relearned the scalar vector via sample_report --apply-scalars (457 written, 148 deadzoned). Result: inflector.ex no_implicit_null_initial finding 0; the behavior still separates its samples cleanly (bad anti-aligns, good does not), so no real detections were lost. 884 tests, credo --strict clean. --- .../no_implicit_null_initial/bad/pricing.ex | 63 ++++++ .../no_implicit_null_initial/bad/router.ex | 50 +++++ .../no_implicit_null_initial/bad/session.ex | 57 ++++++ .../no_implicit_null_initial/good/pricing.ex | 35 ++++ .../no_implicit_null_initial/good/router.ex | 35 ++++ .../no_implicit_null_initial/good/session.ex | 39 ++++ priv/combined_metrics/type_and_value.yml | 185 ++++++++++-------- 7 files changed, 381 insertions(+), 83 deletions(-) create mode 100644 priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/bad/pricing.ex create mode 100644 priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/bad/router.ex create mode 100644 priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/bad/session.ex create mode 100644 priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/good/pricing.ex create mode 100644 priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/good/router.ex create mode 100644 priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/good/session.ex diff --git a/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/bad/pricing.ex b/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/bad/pricing.ex new file mode 100644 index 0000000..dc30e33 --- /dev/null +++ b/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/bad/pricing.ex @@ -0,0 +1,63 @@ +defmodule Pricing.Bad do + @moduledoc """ + Price resolution — BAD: values initialized to nil then assigned in branches. + """ + + def quote(cart, customer) do + base = nil + discount = nil + shipping = nil + + base = subtotal(cart) + + if customer.tier == :gold do + discount = div(base, 10) + end + + if discount == nil do + discount = 0 + end + + if customer.tier == :gold do + shipping = 0 + else + shipping = 499 + end + + %{base: base, discount: discount, shipping: shipping, total: base - discount + shipping} + end + + def discount_for(customer, base) do + discount = nil + + if customer.tier == :gold do + discount = div(base, 10) + end + + if customer.tier == :silver do + discount = div(base, 20) + end + + if discount == nil do + discount = 0 + end + + discount + end + + def coupon_value(coupon, base) do + value = nil + + if coupon != nil do + value = div(base * coupon.value, 100) + end + + if value == nil do + value = 0 + end + + value + end + + defp subtotal(cart), do: Enum.reduce(cart, 0, &(&1.price * &1.qty + &2)) +end diff --git a/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/bad/router.ex b/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/bad/router.ex new file mode 100644 index 0000000..4f20d41 --- /dev/null +++ b/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/bad/router.ex @@ -0,0 +1,50 @@ +defmodule Router.Bad do + @moduledoc """ + Request routing — BAD: handler starts nil and is set in branch after branch. + """ + + def dispatch(request) do + handler = nil + + if request.method == :get and request.path == "/health" do + handler = &health/1 + end + + if request.method == :get and request.path == "/users" do + handler = &list_users/1 + end + + if request.method == :post and request.path == "/users" do + handler = &create_user/1 + end + + if handler == nil do + handler = ¬_found/1 + end + + handler.(request) + end + + def status_for(result) do + status = nil + + if match?({:ok, _}, result) do + status = 200 + end + + if result == {:error, :not_found} do + status = 404 + end + + if status == nil do + status = 500 + end + + status + end + + defp health(_request), do: {:ok, %{status: "up"}} + defp list_users(_request), do: {:ok, []} + defp create_user(_request), do: {:ok, %{id: 1}} + defp not_found(_request), do: {:error, :not_found} +end diff --git a/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/bad/session.ex b/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/bad/session.ex new file mode 100644 index 0000000..7d38a76 --- /dev/null +++ b/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/bad/session.ex @@ -0,0 +1,57 @@ +defmodule Session.Bad do + @moduledoc """ + Session building — BAD: nil placeholders filled in across nested branches. + """ + + def build(conn, store) do + user = nil + token = nil + prefs = nil + + token = conn.cookies["session"] + + if token != nil do + user = store.lookup(token) + + if user != nil do + prefs = store.preferences(user.id) + end + end + + if user == nil do + {:error, :unauthenticated} + else + {:ok, %{user: user, token: token, preferences: prefs}} + end + end + + def current_role(session) do + role = nil + + if session != nil do + if session.user != nil do + role = session.user.role + end + end + + if role == nil do + role = :guest + end + + role + end + + def expires_at(session, now) do + ttl = nil + + if current_role(session) == :admin do + ttl = 3_600 + end + + if ttl == nil do + ttl = 1_800 + end + + now + ttl + end +end diff --git a/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/good/pricing.ex b/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/good/pricing.ex new file mode 100644 index 0000000..15295c7 --- /dev/null +++ b/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/good/pricing.ex @@ -0,0 +1,35 @@ +defmodule Pricing.Good do + @moduledoc """ + Price resolution — GOOD: each value comes straight from an expression. + """ + + def quote(cart, customer) do + base = subtotal(cart) + discount = discount_for(customer, base) + shipping = shipping_for(cart, customer) + + %{base: base, discount: discount, shipping: shipping, total: base - discount + shipping} + end + + def discount_for(customer, base) do + case customer.tier do + :gold -> div(base, 10) + :silver -> div(base, 20) + _ -> 0 + end + end + + def shipping_for(cart, customer) do + cond do + customer.tier == :gold -> 0 + subtotal(cart) >= 5_000 -> 0 + true -> 499 + end + end + + def coupon_value(nil, _base), do: 0 + def coupon_value(%{type: :percent, value: value}, base), do: div(base * value, 100) + def coupon_value(%{type: :flat, value: value}, _base), do: value + + defp subtotal(cart), do: Enum.reduce(cart, 0, &(&1.price * &1.qty + &2)) +end diff --git a/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/good/router.ex b/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/good/router.ex new file mode 100644 index 0000000..b0391ab --- /dev/null +++ b/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/good/router.ex @@ -0,0 +1,35 @@ +defmodule Router.Good do + @moduledoc """ + Request routing — GOOD: the handler is the value of a single case expression. + """ + + def dispatch(request) do + handler = + case {request.method, request.path} do + {:get, "/health"} -> &health/1 + {:get, "/users"} -> &list_users/1 + {:post, "/users"} -> &create_user/1 + {_, _} -> ¬_found/1 + end + + handler.(request) + end + + def status_for(result) do + case result do + {:ok, _} -> 200 + {:error, :not_found} -> 404 + {:error, :invalid} -> 422 + {:error, _} -> 500 + end + end + + def content_type(request) do + Map.get(request.headers, "accept", "application/json") + end + + defp health(_request), do: {:ok, %{status: "up"}} + defp list_users(_request), do: {:ok, []} + defp create_user(_request), do: {:ok, %{id: 1}} + defp not_found(_request), do: {:error, :not_found} +end diff --git a/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/good/session.ex b/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/good/session.ex new file mode 100644 index 0000000..de9d79e --- /dev/null +++ b/priv/combined_metrics/samples/type_and_value/no_implicit_null_initial/good/session.ex @@ -0,0 +1,39 @@ +defmodule Session.Good do + @moduledoc """ + Session building — GOOD: values bound once from with/case results. + """ + + def build(conn, store) do + with {:ok, token} <- read_token(conn), + {:ok, user} <- store.lookup(token), + {:ok, prefs} <- store.preferences(user.id) do + {:ok, %{user: user, token: token, preferences: prefs}} + else + :no_token -> {:error, :unauthenticated} + {:error, reason} -> {:error, reason} + end + end + + def current_role(session) do + case session do + %{user: %{role: role}} -> role + _ -> :guest + end + end + + def expires_at(session, now) do + ttl = ttl_for(current_role(session)) + now + ttl + end + + defp ttl_for(:admin), do: 3_600 + defp ttl_for(:guest), do: 300 + defp ttl_for(_), do: 1_800 + + defp read_token(conn) do + case conn.cookies["session"] do + nil -> :no_token + token -> {:ok, token} + end + end +end diff --git a/priv/combined_metrics/type_and_value.yml b/priv/combined_metrics/type_and_value.yml index b973721..28610f7 100644 --- a/priv/combined_metrics/type_and_value.yml +++ b/priv/combined_metrics/type_and_value.yml @@ -346,110 +346,129 @@ no_empty_string_initial: no_implicit_null_initial: _doc: "Initialising a variable to `nil`/`null` and assigning it later in a branch signals missing structure." - _log_baseline: -3.2593 + _log_baseline: -13.1236 branching: - mean_branch_count: 0.0293 - mean_branching_density: 0.0871 - mean_non_blank_count: -0.0578 + mean_branch_count: -0.1283 + mean_branching_density: -0.0936 + mean_non_blank_count: -0.0648 brevity: - mean_sample_size: 0.0132 + mean_sample_size: 0.0225 casing_entropy: - mean_entropy: 0.0660 - mean_other_count: 0.1247 - mean_screaming_snake_density: 0.0448 - mean_snake_case_count: -0.0534 + mean_entropy: 0.0847 + mean_other_count: 0.0314 + mean_pascal_case_count: 0.0127 + mean_screaming_snake_density: 0.0314 + mean_snake_case_count: -0.0283 compression: - mean_raw_bytes: -0.0246 - mean_redundancy: -0.0100 - mean_unique_line_ratio: -0.0159 - mean_zlib_bytes: -0.0071 - mean_zlib_ratio: -0.0174 + mean_raw_bytes: -0.0234 + mean_redundancy: -0.0233 + mean_unique_line_ratio: 0.0227 + mean_zlib_bytes: 0.0080 + mean_zlib_ratio: -0.0344 entropy: - mean_char_entropy: 0.0071 - mean_char_max_entropy: 0.0063 - mean_token_entropy: -0.0025 - mean_token_max_entropy: 0.0029 - mean_token_normalized: -0.0055 - mean_vocab_size: 0.0132 + mean_char_entropy: 0.0139 + mean_char_max_entropy: 0.0040 + mean_char_normalized: 0.0100 + mean_token_entropy: 0.0056 + mean_token_max_entropy: 0.0056 + mean_total_tokens: 0.0056 + mean_vocab_size: 0.0225 function_metrics: - mean_avg_function_lines: -0.1325 - mean_avg_param_count: -0.0029 - mean_function_count: 0.0440 - mean_max_function_lines: -0.1618 + mean_avg_function_lines: -0.2242 + mean_avg_param_count: -0.0125 + mean_function_count: 0.0764 + mean_max_function_lines: -0.1680 halstead: - mean_N1_total_operators: 0.0268 - mean_N2_total_operands: -0.0416 - mean_difficulty: 0.0085 - mean_effort: 0.0109 - mean_estimated_bugs: 0.0024 - mean_n1_unique_operators: 0.0393 - mean_n2_unique_operands: -0.0108 - mean_time_to_implement_seconds: 0.0109 - mean_vocabulary: 0.0038 - mean_volume: 0.0024 + mean_N1_total_operators: 0.0239 + mean_N2_total_operands: -0.0246 + mean_difficulty: -0.0424 + mean_effort: -0.0243 + mean_estimated_bugs: 0.0094 + mean_length: 0.0058 + mean_n1_unique_operators: 0.0075 + mean_n2_unique_operands: 0.0236 + mean_time_to_implement_seconds: -0.0243 + mean_vocabulary: 0.0177 + mean_volume: 0.0094 heaps: - mean_beta: -0.0079 - mean_k: 0.0418 - mean_r_squared: -0.0034 + mean_beta: 0.0100 + mean_k: -0.0117 + mean_r_squared: 0.0031 identifier_length_variance: - mean_mean: 0.0185 - mean_std_dev: 0.0177 - mean_variance: 0.0354 + mean_max: 0.0041 + mean_mean: 0.0047 + mean_std_dev: 0.0317 + mean_variance: 0.0565 indentation: - mean_blank_line_ratio: -0.1146 - mean_mean_depth: 0.0089 - mean_variance: 0.1759 + mean_blank_line_ratio: -0.1084 + mean_max_depth: 0.0239 + mean_mean_depth: -0.0095 + mean_variance: 0.1274 line_patterns: - mean_blank_line_ratio: -0.1146 - mean_string_literal_ratio: -0.0022 - mean_unique_line_ratio: -0.0135 + mean_blank_line_ratio: -0.1084 + mean_string_literal_ratio: 0.0176 + mean_unique_line_ratio: 0.0202 magic_number_density: - mean_density: -0.0066 - mean_string_literal_ratio: -0.0022 + mean_density: 0.0224 + mean_magic_number_count: 0.0188 + mean_string_literal_ratio: 0.0176 near_duplicate_blocks_file: - mean_block_count: -0.1493 - mean_sub_block_count: 0.0422 + mean_block_count: -0.1528 + mean_near_dup_block_d0: -0.1314 + mean_near_dup_block_d2: 0.3255 + mean_near_dup_block_d3: 0.3255 + mean_near_dup_block_d4: 0.3255 + mean_near_dup_block_d5: 0.3255 + mean_near_dup_block_d6: 0.3255 + mean_near_dup_block_d8: -0.1628 + mean_sub_block_count: 0.0893 ngram: - mean_bigram_hapax_fraction: -0.0181 - mean_bigram_repeated_unique: 0.0178 - mean_bigram_repetition_rate: 0.0335 - mean_bigram_unique: -0.0287 - mean_trigram_hapax_fraction: -0.0209 - mean_trigram_repeated_unique: 0.0959 - mean_trigram_repetition_rate: 0.0860 - mean_trigram_unique: -0.0245 + mean_bigram_repeated_unique: 0.0120 + mean_bigram_repetition_rate: -0.0034 + mean_bigram_total: 0.0057 + mean_bigram_unique: 0.0077 + mean_trigram_hapax_fraction: -0.0012 + mean_trigram_repeated_unique: 0.0236 + mean_trigram_repetition_rate: 0.0051 + mean_trigram_total: 0.0057 + mean_trigram_unique: 0.0040 punctuation_density: - mean_arrow_density: -1.4388 - mean_bracket_nonalpha_prefix_count: 0.0807 - mean_bracket_nonalpha_suffix_count: 0.1035 - mean_colon_suffix_density: 0.1166 - mean_exclamation_density: 2.0000 - mean_id_nonalpha_suffix_density: 0.0685 + mean_arrow_density: -0.8920 + mean_bracket_nonalpha_prefix_count: 0.0710 + mean_bracket_nonalpha_suffix_count: 0.1396 + mean_colon_suffix_density: 0.1056 + mean_dot_count: -0.0539 + mean_exclamation_density: 1.4975 + mean_id_nonalpha_suffix_density: 0.0760 + mean_question_mark_density: 2.0000 readability: - mean_avg_line_length: 0.0358 - mean_avg_sub_words_per_id: 0.0035 - mean_avg_tokens_per_line: 0.0586 - mean_flesch_adapted: -0.0082 - mean_fog_adapted: 0.0549 - mean_total_lines: -0.0578 + mean_avg_line_length: 0.0468 + mean_avg_sub_words_per_id: 0.0057 + mean_avg_tokens_per_line: 0.0743 + mean_flesch_adapted: -0.0097 + mean_fog_adapted: 0.0725 + mean_total_lines: -0.0648 separator_counts: - mean_hyphen_count: 0.4264 - mean_slash_count: 0.0885 - mean_underscore_count: -0.0054 + mean_dot_count: -0.0539 + mean_hyphen_count: 0.7825 + mean_slash_count: 0.0616 + mean_underscore_count: 0.0583 symbol_density: - mean_density: 0.0549 - mean_distinct_symbol_types: 0.0342 - mean_symbol_count: 0.0303 + mean_density: 0.0714 + mean_distinct_symbol_types: 0.0201 + mean_symbol_count: 0.0419 vocabulary: - mean_mattr: 0.0540 - mean_raw_ttr: 0.0466 - mean_total_identifiers: -0.0466 + mean_mattr: 0.0476 + mean_raw_ttr: 0.0462 + mean_total_identifiers: -0.0174 + mean_unique_identifiers: 0.0259 vowel_density: - mean_total_chars: -0.0281 + mean_total_chars: -0.0127 zipf: - mean_exponent: -0.0074 - mean_r_squared: 0.0047 - mean_vocab_size: 0.0132 + mean_exponent: -0.0090 + mean_r_squared: 0.0066 + mean_total_tokens: 0.0056 + mean_vocab_size: 0.0225 no_magic_value_assigned: _doc: "Literal strings and numbers assigned to variables should be named constants, not inline values."