Skip to content

Commit c26f33a

Browse files
committed
Warn when missing semicolons
1 parent e3bf977 commit c26f33a

File tree

7 files changed

+176
-64
lines changed

7 files changed

+176
-64
lines changed

lib/live_view_native/swiftui/rules_parser.ex

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,22 @@ defmodule LiveViewNative.SwiftUI.RulesParser do
33

44
alias LiveViewNative.SwiftUI.RulesParser.Modifiers
55
alias LiveViewNative.SwiftUI.RulesParser.Parser
6+
require Logger
67

78
def parse(rules, opts \\ []) do
89
file = Keyword.get(opts, :file) || ""
910
module = Keyword.get(opts, :module) || ""
1011
line = Keyword.get(opts, :line) || 1
1112
variable_context = Keyword.get(opts, :variable_context, Elixir)
13+
expect_semicolons? = Keyword.get(opts, :expect_semicolons?, false)
1214

1315
context =
1416
opts
1517
|> Keyword.get(:context, %{})
1618
|> Map.put_new(:file, file)
1719
|> Map.put_new(:source_line, line)
1820
|> Map.put_new(:module, module)
21+
|> Map.put_new(:expect_semicolons?, expect_semicolons?)
1922
|> Map.put_new(
2023
:annotations,
2124
Application.get_env(:live_view_native_stylesheet, :annotations, false)
@@ -35,10 +38,12 @@ defmodule LiveViewNative.SwiftUI.RulesParser do
3538
|> Parser.error_from_result()
3639

3740
case result do
38-
{:ok, [output], _unconsumed = "", _context, _current_line_and_offset, _} ->
41+
{:ok, [output], warnings} ->
42+
log_warnings(warnings, file)
3943
output
4044

41-
{:ok, output, _unconsumed = "", _context, _current_line_and_offset, _} ->
45+
{:ok, output, warnings} ->
46+
log_warnings(warnings, file)
4247
output
4348

4449
{:error, message, _unconsumed, _context, {line, _}, _} ->
@@ -48,4 +53,10 @@ defmodule LiveViewNative.SwiftUI.RulesParser do
4853
line: line
4954
end
5055
end
56+
57+
def log_warnings(warnings, file) do
58+
for {message, {line, _}, _offset} <- warnings do
59+
IO.warn(message, line: line, file: file)
60+
end
61+
end
5162
end

lib/live_view_native/swiftui/rules_parser/modifiers.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ defmodule LiveViewNative.SwiftUI.RulesParser.Modifiers do
283283
ignore_whitespace()
284284
|> concat(modifier_name())
285285
|> concat(modifier_brackets.(nested: false))
286+
|> expect_semicolon_or_warn()
286287
|> post_traverse({PostProcessors, :to_function_call_ast, []}),
287288
export_combinator: true
288289
)

lib/live_view_native/swiftui/rules_parser/parser.ex

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ defmodule LiveViewNative.SwiftUI.RulesParser.Parser do
3535
choices ++
3636
if generate_error? do
3737
[
38-
error_parser
39-
|> put_error(
38+
put_error(
39+
error_parser,
4040
"Expected one of the following:\n" <>
4141
label_from_named_choices(named_choices) <> "\n",
4242
show_incorrect_text?: show_incorrect_text?
@@ -49,16 +49,19 @@ defmodule LiveViewNative.SwiftUI.RulesParser.Parser do
4949
end
5050

5151
def expect(combinator \\ empty(), combinator_2, opts) do
52-
error_parser = Keyword.get(opts, :error_parser, non_whitespace())
53-
error_message = Keyword.get(opts, :error_message)
54-
generate_error? = Keyword.get(opts, :generate_error?, true)
52+
opts =
53+
opts
54+
|> Keyword.update(:error_parser, non_whitespace(), &(&1 || empty()))
55+
|> Keyword.update(:error_message, "", & &1)
56+
|> Keyword.update(:generate_error?, true, & &1)
57+
|> Keyword.update(:warning, false, & &1)
5558

5659
combinator
5760
|> concat(
58-
if generate_error? do
61+
if opts[:generate_error?] do
5962
choice([
6063
combinator_2,
61-
put_error(error_parser, error_message, opts)
64+
put_error(opts[:error_parser], opts[:error_message], opts)
6265
])
6366
else
6467
combinator_2
@@ -150,8 +153,13 @@ defmodule LiveViewNative.SwiftUI.RulesParser.Parser do
150153
{message, position, byte_offset} = Error.context_to_error_message(context)
151154
{:error, message, rest, context, position, byte_offset}
152155

153-
result ->
154-
result
156+
{:ok, output, _unconsumed = "", %{context: %Context{} = context}, _, _} ->
157+
warnings =
158+
Enum.map(context.warnings, fn warning ->
159+
Error.context_to_error_message(context, warning)
160+
end)
161+
162+
{:ok, output, warnings}
155163
end
156164
end
157165
end

lib/live_view_native/swiftui/rules_parser/parser/context.ex

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ defmodule LiveViewNative.SwiftUI.RulesParser.Parser.Context do
88
source: "",
99
source_lines: [],
1010
errors: [],
11+
warnings: [],
1112
highlight_error: true,
1213
# Where in the code does the input start?
1314
# Useful for localizing errors when parsing sigil text
@@ -45,14 +46,14 @@ defmodule LiveViewNative.SwiftUI.RulesParser.Parser.Context do
4546
if is_frozen?(context) and not error.forced? do
4647
context
4748
else
48-
path = [:context, Access.key(:errors)]
49+
path =
50+
if error.is_warning? do
51+
[:context, Access.key(:warnings)]
52+
else
53+
[:context, Access.key(:errors)]
54+
end
4955

50-
{_, context} =
51-
get_and_update_in(context, path, fn
52-
existing_errors -> {[error | existing_errors], [error | existing_errors]}
53-
end)
54-
55-
context
56+
update_in(context, path, &[error | &1])
5657
end
5758
end
5859

lib/live_view_native/swiftui/rules_parser/parser/error.ex

Lines changed: 47 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,70 +9,64 @@ defmodule LiveViewNative.SwiftUI.RulesParser.Parser.Error do
99
:line,
1010
:byte_offset,
1111
:error_message,
12-
forced?: false
12+
forced?: false,
13+
is_warning?: false
1314
])
1415

1516
def put_error(
1617
rest,
1718
args,
1819
context,
19-
_,
20-
_,
21-
error_message,
22-
opts \\ []
23-
)
24-
25-
def put_error(
26-
rest,
27-
[] = arg,
28-
context,
29-
{line, _offset},
30-
byte_offset,
31-
error_message,
32-
opts
33-
) do
34-
# IO.inspect({[], rest, error_message}, label: "error[0]")
35-
36-
context =
37-
Context.put_new_error(context, rest, %__MODULE__{
38-
incorrect_text: "",
39-
line: line,
40-
byte_offset: byte_offset,
41-
error_message: error_message,
42-
show_incorrect_text?: Keyword.get(opts, :show_incorrect_text?, false),
43-
forced?: Keyword.get(opts, :force_error?, false)
44-
})
45-
46-
{rest, arg, context}
47-
end
48-
49-
def put_error(
50-
rest,
51-
[matched_text | _] = args,
52-
context,
5320
{line, _offset},
5421
byte_offset,
5522
error_message,
56-
opts
23+
opts \\ []
5724
) do
58-
# IO.inspect({matched_text, rest, error_message}, label: "error[0]")
25+
# IO.inspect({args, rest, error_message}, label: "error[0]")
26+
27+
error = %__MODULE__{
28+
incorrect_text: List.first(args, ""),
29+
line: line,
30+
byte_offset: byte_offset,
31+
error_message: error_message,
32+
show_incorrect_text?: Keyword.get(opts, :show_incorrect_text?, false),
33+
forced?: Keyword.get(opts, :force_error?, false),
34+
is_warning?: false
35+
}
5936

6037
context =
61-
Context.put_new_error(context, rest, %__MODULE__{
62-
incorrect_text: matched_text,
63-
line: line,
64-
byte_offset: byte_offset,
65-
error_message: error_message,
66-
show_incorrect_text?: Keyword.get(opts, :show_incorrect_text?, false),
67-
forced?: Keyword.get(opts, :force_error?, false)
68-
})
38+
case opts[:warning] do
39+
true ->
40+
# Always treat error as warning
41+
Context.put_new_error(context, rest, %{error | is_warning?: true})
42+
43+
false ->
44+
# Never treat error as warning
45+
Context.put_new_error(context, rest, error)
46+
47+
nil ->
48+
# Never treat error as warning
49+
Context.put_new_error(context, rest, error)
50+
51+
warning ->
52+
# The error is an optional warning
53+
# Only log the warning if value in `context[<key>]` is true
54+
if get_in(context, [Access.key(warning)]) == true do
55+
Context.put_new_error(context, rest, %{error | is_warning?: true})
56+
else
57+
context
58+
end
59+
end
6960

7061
{rest, args, context}
7162
end
7263

7364
def context_to_error_message(context) do
7465
[%__MODULE__{} = error | _] = Enum.reverse(context.errors)
66+
context_to_error_message(context, error)
67+
end
7568

69+
def context_to_error_message(context, %__MODULE__{} = error) do
7670
error_message = error.error_message
7771
line = error.line
7872
incorrect_text = error.incorrect_text
@@ -144,8 +138,15 @@ defmodule LiveViewNative.SwiftUI.RulesParser.Parser.Error do
144138
""
145139
end
146140

141+
header =
142+
if error.is_warning? do
143+
""
144+
else
145+
"Unsupported input:"
146+
end
147+
147148
message = """
148-
Unsupported input:
149+
#{header}
149150
#{line_spacer} |
150151
#{lines}
151152
#{line_spacer} |

lib/live_view_native/swiftui/rules_parser/tokens.ex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,19 @@ defmodule LiveViewNative.SwiftUI.RulesParser.Tokens do
113113
combinator |> ignore(optional(whitespace(min: 1)))
114114
end
115115

116+
def expect_semicolon_or_warn(combinator) do
117+
combinator
118+
|> concat(
119+
ignore(string(";"))
120+
|> expect(
121+
warning: :expect_semicolons?,
122+
error_message: "Expected a ‘;’",
123+
error_parser: empty(),
124+
show_incorrect_text?: false
125+
)
126+
)
127+
end
128+
116129
# @tuple_children [
117130
# parsec(:nested_attribute),
118131
# atom(),

test/live_view_native/swiftui/rules_parser_test.exs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
defmodule LiveViewNative.SwiftUI.RulesParserTest do
22
use ExUnit.Case
3+
import ExUnit.CaptureIO
34
alias LiveViewNative.SwiftUI.RulesParser
45

56
def parse(input, opts \\ []) do
67
RulesParser.parse(input,
78
file: Keyword.get(opts, :file),
89
module: Keyword.get(opts, :module),
910
line: Keyword.get(opts, :line),
11+
expect_semicolons?: Keyword.get(opts, :expect_semicolons?, false),
1012
context: %{
1113
annotations: Keyword.get(opts, :annotations, false)
1214
}
@@ -382,6 +384,7 @@ defmodule LiveViewNative.SwiftUI.RulesParserTest do
382384

383385
input = ~s{rotationEffect(gesture_state(:rotate, .rotation, defaultValue: .zero))}
384386
output = {:rotationEffect, [], [{:__gesture_state__, [], [:rotate, {:., [], [nil, :rotation]}, [defaultValue: {:., [], [nil, :zero]}]]}]}
387+
assert parse(input) == output
385388
end
386389
end
387390

@@ -454,6 +457,80 @@ defmodule LiveViewNative.SwiftUI.RulesParserTest do
454457
assert String.trim(error.description) == error_prefix
455458
end
456459

460+
test "warn when missing semicolons" do
461+
input = "blue() red()"
462+
463+
logs =
464+
capture_io(:stderr, fn ->
465+
assert parse(input, expect_semicolons?: true)
466+
end)
467+
468+
assert logs =~
469+
"""
470+
warning:#{" "}
471+
|
472+
1 | blue()‎ red()
473+
| ^
474+
|
475+
476+
Expected a ‘;’
477+
"""
478+
479+
assert logs =~
480+
"""
481+
warning:#{" "}
482+
|
483+
1 | blue() red()‎
484+
| ^
485+
|
486+
487+
Expected a ‘;’
488+
"""
489+
490+
input = """
491+
blue()
492+
red()
493+
green()
494+
"""
495+
496+
logs =
497+
capture_io(:stderr, fn ->
498+
assert parse(input, expect_semicolons?: true)
499+
end)
500+
501+
assert logs =~
502+
"""
503+
warning:#{" "}
504+
|
505+
1 | blue()‎
506+
| ^
507+
|
508+
509+
Expected a ‘;’
510+
"""
511+
assert logs =~
512+
"""
513+
warning:#{" "}
514+
|
515+
2 | red()‎
516+
| ^
517+
|
518+
519+
Expected a ‘;’
520+
"""
521+
522+
assert logs =~
523+
"""
524+
warning:#{" "}
525+
|
526+
3 | green()‎
527+
| ^
528+
|
529+
530+
Expected a ‘;’
531+
"""
532+
end
533+
457534
test "invalid modifier name" do
458535
input = "1(.red)"
459536

0 commit comments

Comments
 (0)