Skip to content

Commit c410872

Browse files
mkaputmhanberg
authored andcommitted
Merge branch 'main' into pr/db35a96-fn-newline-metadata
2 parents 3a82c57 + 708da64 commit c410872

14 files changed

Lines changed: 334 additions & 381 deletions

.formatter.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Used by "mix format"
22
[
33
plugins: [Styler],
4+
import_deps: [:stream_data],
45
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
56
]

.github/property_failures.exs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#! /usr/bin/env elixir
2+
3+
defmodule Main do
4+
@moduledoc false
5+
require Logger
6+
7+
@repo System.get_env("REPO", "elixir-tools/spitfire")
8+
def run do
9+
input =
10+
with :eof <- IO.read(:eof) do
11+
Logger.error("Stdin is empty")
12+
System.halt(2)
13+
end
14+
15+
results =
16+
case JSON.decode(input) do
17+
{:ok, results} ->
18+
results
19+
20+
{:error, reason} ->
21+
case reason do
22+
{_, offset, byte} ->
23+
Logger.error(inspect(byte))
24+
Logger.error(binary_slice(input, offset, byte_size(input)))
25+
26+
_ ->
27+
:ok
28+
end
29+
30+
Logger.error(inspect(reason))
31+
raise "Malformed JSON: " <> input
32+
end
33+
34+
failures =
35+
for failures <- get_in(results, ["tests", Access.all(), "failures"]), f <- failures do
36+
[match] = Regex.run(~r/.*Clause:.*\s+Generated:.*\n+(.*)/, f["message"], capture: :all_but_first)
37+
38+
JSON.decode!(String.trim(match))
39+
end
40+
41+
failures = Enum.uniq_by(failures, & &1["code"])
42+
43+
if failures == [] do
44+
IO.write(:stderr, "No failures!")
45+
end
46+
47+
for fail <- failures do
48+
{issues, 0} =
49+
gh(
50+
~w(issue list --label property-failure --json title) ++
51+
["--jq", ~s<. | map(. | select(.title == "#{fail["code"]}"))>]
52+
)
53+
54+
case JSON.decode!(issues) do
55+
[] ->
56+
IO.write(:stderr, "Making new issue for #{fail["code"]}\n")
57+
58+
gh(
59+
~w(issue create --label property-failure) ++
60+
[
61+
"--title",
62+
fail["code"],
63+
"--body",
64+
"""
65+
Type: #{fail["type"]}
66+
Context: #{fail["context"]}
67+
68+
Code:
69+
```elixir
70+
#{fail["code"]}
71+
```
72+
73+
Elixir AST:
74+
```elixir
75+
#{fail["elixir"]}
76+
```
77+
78+
Spitfire AST:
79+
```elixir
80+
#{fail["spitfire"] || "N/A"}
81+
```
82+
"""
83+
]
84+
)
85+
86+
_ ->
87+
IO.write(:stderr, "Issue already exists for #{fail["code"]}\n")
88+
end
89+
end
90+
end
91+
92+
defp gh(args) do
93+
System.cmd("gh", args ++ ~w(--repo #{@repo}))
94+
end
95+
end
96+
97+
Main.run()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Property Tests
2+
on:
3+
# schedule:
4+
# - cron: "0 0 * * *"
5+
workflow_dispatch:
6+
7+
permissions:
8+
contents: read
9+
issues: write
10+
11+
concurrency:
12+
group: property-test
13+
cancel-in-progress: false
14+
15+
jobs:
16+
proptest:
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: DeterminateSystems/nix-installer-action@main
22+
- uses: cachix/cachix-action@v14
23+
with:
24+
name: elixir-tools
25+
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
26+
27+
- uses: actions/cache@v4
28+
with:
29+
path: |
30+
deps
31+
_build
32+
key: ${{ runner.os }}-mix-${{ hashFiles('**/.mise.toml') }}-${{ hashFiles('**/mix.lock') }}
33+
restore-keys: |
34+
${{ runner.os }}-mix-${{ hashFiles('**/.mise.toml') }}-
35+
36+
- name: Install Dependencies
37+
run: nix develop --command bash -c "mix deps.get"
38+
39+
- name: Compile
40+
run: nix develop --command bash -c "MIX_ENV=test mix compile"
41+
42+
- name: Run property tests
43+
env:
44+
GH_TOKEN: ${{ github.token }}
45+
run: nix develop --command bash -c "MIX_ENV=test mix test.json --only property | .github/property_failures.exs"

.github/workflows/proptest.yaml

Lines changed: 0 additions & 74 deletions
This file was deleted.

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "0.3.6"
2+
".": "0.3.7"
33
}

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## [0.3.7](https://github.com/elixir-tools/spitfire/compare/v0.3.6...v0.3.7) (2026-02-19)
4+
5+
6+
### Bug Fixes
7+
8+
* allow list literals in struct type parsing ([#111](https://github.com/elixir-tools/spitfire/issues/111)) ([f2eabee](https://github.com/elixir-tools/spitfire/commit/f2eabee78f740f6109609724179fbea1a9537558))
9+
* parse assoc-map values outside key context ([#102](https://github.com/elixir-tools/spitfire/issues/102)) ([6751c15](https://github.com/elixir-tools/spitfire/commit/6751c15bd1a7aec63fa2d81ff81bc8a9b3905f3f))
10+
* parse nested struct/map targets in struct literals ([#105](https://github.com/elixir-tools/spitfire/issues/105)) ([3d1cc61](https://github.com/elixir-tools/spitfire/commit/3d1cc61120c36f2c686ab32bbfa307407991bc17))
11+
* support dot-call struct targets like %e.(){} ([#110](https://github.com/elixir-tools/spitfire/issues/110)) ([706f0b0](https://github.com/elixir-tools/spitfire/commit/706f0b01c1a6440be5ea3618ecedfab0f5452353))
12+
313
## [0.3.6](https://github.com/elixir-tools/spitfire/compare/v0.3.5...v0.3.6) (2026-02-16)
414

515

config/config.exs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,17 @@ if config_env() == :test do
1313
# END: parse_program
1414

1515
config :spitfire, trace: false
16+
17+
is_ci = "CI" |> System.get_env("false") |> String.to_existing_atom()
18+
19+
config :spitfire, is_ci: is_ci
20+
21+
max_runs =
22+
if is_ci do
23+
500_000
24+
else
25+
1000
26+
end
27+
28+
config :stream_data, max_runs: max_runs
1629
end

flake.nix

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
packages = with pkgs; [
1818
beam.packages.erlang_27.erlang
1919
beam.packages.erlang_27.elixir_1_18
20-
act
2120
];
2221
};
2322
};

lib/spitfire.ex

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,28 +1195,9 @@ defmodule Spitfire do
11951195
nl -> [newlines: nl]
11961196
end
11971197

1198-
# Check if we have a semicolon right after -> (possibly after eol)
1199-
# Check if there's a leading semicolon right after ->
1200-
# Handles both ";expr" and "\n;expr" patterns
1201-
has_leading_semicolon =
1202-
case peek_token(parser) do
1203-
:";" ->
1204-
true
1205-
1206-
:eol ->
1207-
# Peek at the next token after eol without consuming
1208-
# We need to manually check the token sequence
1209-
with {:eol, _} <- parser.current_token,
1210-
# Look at the tokens list to find what comes after eol
1211-
[{:";", _} | _] <- parser.tokens do
1212-
true
1213-
else
1214-
_ -> false
1215-
end
1216-
1217-
_ ->
1218-
false
1219-
end
1198+
# A semicolon immediately after `->` (with optional newlines in between)
1199+
# starts the clause body with an implicit nil expression.
1200+
has_leading_semicolon = peek_token_skip_eol(parser) == :";"
12201201

12211202
parser = eat_eoe_at(parser, 1)
12221203

@@ -1423,8 +1404,28 @@ defmodule Spitfire do
14231404
{rhs, parser}
14241405
end
14251406

1407+
parser =
1408+
if token == :"//" and not match?({:.., _, [_, _]}, lhs) do
1409+
put_error(
1410+
pre_parser,
1411+
{meta,
1412+
"the range step operator (//) must immediately follow the range definition operator (..), for example: 1..9//2. If you wanted to define a default argument, use (\\\\) instead. Syntax error before: '//'"}
1413+
)
1414+
else
1415+
parser
1416+
end
1417+
14261418
ast =
14271419
case token do
1420+
:"//" ->
1421+
case lhs do
1422+
{:.., lhs_meta, [left, middle]} ->
1423+
{:..//, lhs_meta, [left, middle, rhs]}
1424+
1425+
_ ->
1426+
{token, newlines ++ meta, [lhs, rhs]}
1427+
end
1428+
14281429
:"not in" ->
14291430
{:not, meta, [{:in, meta, [lhs, rhs]}]}
14301431

@@ -2941,7 +2942,7 @@ defmodule Spitfire do
29412942
if MapSet.member?(@terminals_with_comma, peek_token(parser)) or
29422943
peek_token(parser) == :";" or
29432944
peek in [:stab_op, :do, :end, :block_identifier] or
2944-
(is_binary_op?(peek) and peek != :dual_op) do
2945+
(is_binary_op?(peek) and peek not in [:dual_op, :ternary_op]) do
29452946
{{:..., current_meta(parser), []}, parser}
29462947
else
29472948
meta = current_meta(parser)

mix.exs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule Spitfire.MixProject do
77
[
88
app: :spitfire,
99
description: "Error resilient parser for Elixir",
10-
version: "0.3.6",
10+
version: "0.3.7",
1111
elixir: "~> 1.15",
1212
start_permanent: Mix.env() == :prod,
1313
deps: deps(),
@@ -35,7 +35,8 @@ defmodule Spitfire.MixProject do
3535
{:styler, "~> 0.11", only: [:dev, :test]},
3636
{:credo, "~> 1.7", only: :dev},
3737
{:dialyxir, "~> 1.0", only: :dev},
38-
{:stream_data, "~> 1.0", only: [:dev, :test]}
38+
{:stream_data, "~> 1.0", only: [:dev, :test]},
39+
{:ex_unit_json, "~> 0.4.1", only: [:dev, :test]}
3940
]
4041
end
4142

0 commit comments

Comments
 (0)