Skip to content

Commit

Permalink
Compile quoted after eval and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
scottming committed Aug 13, 2023
1 parent c58ba23 commit adc5a4f
Show file tree
Hide file tree
Showing 2 changed files with 242 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ defmodule Lexical.RemoteControl.Build.Document.Compilers.HEEx do
"""
alias Lexical.Document
alias Lexical.Plugin.V1.Diagnostic.Result
alias Lexical.RemoteControl.Build
alias Lexical.RemoteControl.Build.Document.Compiler
alias Lexical.RemoteControl.Build.Error
alias Lexical.RemoteControl.Build.Document.Compilers
require Logger

@behaviour Compiler
Expand All @@ -20,8 +21,14 @@ defmodule Lexical.RemoteControl.Build.Document.Compilers.HEEx do

def compile(%Document{} = document) do
with {:ok, _quoted} <- heex_to_quoted(document),
{:ok, _string} <- eval(document) do
{:ok, []}
{:ok, eex_quoted_ast} <- eval(document) do
compile_quoted(document, eex_quoted_ast)
end
end

defp compile_quoted(%Document{} = document, quoted) do
with {:error, errors} <- Compilers.Quoted.compile(document, quoted, "HEEx") do
{:error, reject_undefined_assigns(errors)}
end
end

Expand Down Expand Up @@ -56,25 +63,79 @@ defmodule Lexical.RemoteControl.Build.Document.Compilers.HEEx do
|> Document.to_string()
|> EEx.compile_string(file: document.path)

result =
if Elixir.Features.with_diagnostics?() do
eval_quoted_with_diagnostics(quoted_ast, document.path)
else
eval_quoted(quoted_ast, document.path)
end

case result do
{:ok, quoted_ast} ->
{:ok, quoted_ast}

{:exception, exception, stack} ->
converted =
document
|> Build.Error.error_to_diagnostic(exception, stack, quoted_ast)
|> Map.put(:source, "HEEx")

{:error, [converted]}

{{:ok, quoted_ast}, _} ->
# Ignore warnings for now
# because they will be handled by `compile_quoted/2`
# like: unused variables
{:ok, quoted_ast}

{{:exception, exception, stack, quoted_ast}, all_errors_and_warnings} ->
converted = Build.Error.error_to_diagnostic(document, exception, stack, quoted_ast)
maybe_diagnostics = Build.Error.diagnostics_from_mix(document, all_errors_and_warnings)

diagnostics =
[converted | maybe_diagnostics]
|> Enum.reverse()
|> Build.Error.refine_diagnostics()
|> Enum.map(&Map.replace!(&1, :source, "HEEx"))

{:error, diagnostics}
end
end

defp eval_quoted_with_diagnostics(quoted_ast, path) do
# Using apply to prevent a compile warning on elixir < 1.15
# credo:disable-for-next-line
apply(Code, :with_diagnostics, [fn -> eval_quoted(quoted_ast, path) end])
end

def eval_quoted(quoted_ast, path) do
try do
{result, _} = Code.eval_quoted(quoted_ast, [assigns: %{}], file: document.path)
{:ok, result}
{_, _} = Code.eval_quoted(quoted_ast, [assigns: %{}], file: path)
{:ok, quoted_ast}
rescue
exception ->
{filled_exception, stack} = Exception.blame(:error, exception, __STACKTRACE__)
{:exception, filled_exception, stack, quoted_ast}
end
end

error =
[Error.error_to_diagnostic(document, exception, stack, quoted_ast)]
|> Error.refine_diagnostics()
defp reject_undefined_assigns(errors) do
# NOTE: Ignoring error for assigns makes sense,
# because we don't want such a error report,
# for example: `<%= @name %>`
Enum.reject(errors, fn %Result{message: message} ->
message =~ ~s[undefined variable "assigns"]
end)
end

{:error, error}
end
defp error_to_result(%Document{} = document, %EEx.SyntaxError{} = error) do
position = {error.line, error.column}

Result.new(document.uri, position, error.message, :error, "HEEx")
end

defp error_to_result(document, %error_struct{} = error)
when error_struct in [
EEx.SystaxError,
TokenMissingError,
Phoenix.LiveView.Tokenizer.ParseError
] do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
defmodule Lexical.RemoteControl.Build.Document.Compilers.HeexTest do
alias Lexical.Document
alias Lexical.Plugin.V1.Diagnostic.Result
alias Lexical.Project
alias Lexical.RemoteControl
alias Lexical.RemoteControl.Api.Messages
alias Lexical.RemoteControl.Build
alias Lexical.RemoteControl.Build.CaptureServer
alias Lexical.RemoteControl.Build.Document.Compilers
alias Lexical.RemoteControl.ModuleMappings
alias Lexical.RemoteControl.ProjectNodeSupervisor

import Lexical.Test.Fixtures
import Messages
import Lexical.Test.CodeSigil

use ExUnit.Case

def with_capture_server(_) do
start_supervised!(CaptureServer)
start_supervised!(ModuleMappings)
:ok
end

def with_liveview_project(_) do
fixture_dir = Path.join(fixtures_path(), "live_demo")
project = Project.new("file://#{fixture_dir}")

{:ok, _} = start_supervised({ProjectNodeSupervisor, project})
{:ok, _, _} = RemoteControl.start_link(project, self())
Build.schedule_compile(project, true)

assert_receive project_compiled(status: :success), 10_000

{:ok, %{project: project}}
end

defp compile(project, document) do
RemoteControl.call(project, Compilers.HEEx, :compile, [document])
end

def document_with_content(content) do
Document.new("file:///file.heex", content, 0)
end

setup_all [:with_liveview_project, :with_capture_server]

describe "compile/1" do
test "handles valid EEx content", %{project: project} do
document = document_with_content(~q[
<%= "thing" %>
])

assert {:ok, []} = compile(project, document)
end

test "handles EEx syntax error", %{project: project} do
document = document_with_content(~q[
<%= IO.
])
assert {:error, [%Result{} = result]} = compile(project, document)

assert result.message =~ "'%>'"
assert result.source == "HEEx"
end

test "handles unused error", %{project: project} do
document = document_with_content(~q[
<div>
<%= something = 1 %>
</div>
])

assert {:ok, [%Result{} = result]} = compile(project, document)

assert result.message == "variable \"something\" is unused"
assert result.position == {2, 7}
assert result.severity == :warning
assert result.source == "HEEx"
assert result.uri == "file:///file.heex"
end

test "handles undefinied function", %{project: project} do
document = document_with_content(~q[
<%= IO.uts("thing") %>
])

assert {:error, [%Result{} = result]} = compile(project, document)
assert result.message =~ "function IO.uts/1 is undefined or private"
assert result.position == {1, 8}
assert result.severity == :error
assert result.source == "HEEx"
assert result.uri =~ "file:///file.heex"
end

@tag :with_diagnostics

test "handles undefinied variable", %{project: project} do
document = document_with_content(~q[
<%= thing %>
])

assert {:error, [%Result{} = result]} = compile(project, document)

assert result.message =~ "undefined variable \"thing\""
assert result.position == {1, 5}
assert result.severity == :error
assert result.source == "HEEx"
assert result.uri =~ "file:///file.heex"
end

test "ignore undefinied assigns", %{project: project} do
document = document_with_content(~q[
<div><%= @thing %></div>
])

assert {:error, []} = compile(project, document)
end

test "handles valid HEEx content", %{project: project} do
document = document_with_content(~q[
<div>thing</div>
])
assert {:ok, []} = compile(project, document)
end

test "handles unclosed tags", %{project: project} do
document = document_with_content(~q[
<div>thing
])
assert {:error, [%Result{} = result]} = compile(project, document)

assert result.message =~
"end of template reached without closing tag for <div>\n |\n1 | <div>thing\n | ^"

assert result.position == {1, 1}
assert result.severity == :error
assert result.source == "HEEx"
assert result.uri =~ "file:///file.heex"
end

test "handles invalid HEEx syntax", %{project: project} do
document = document_with_content(~q[
<span id=@id}></span>
])

assert {:error, [%Result{} = result]} = compile(project, document)

assert result.message =~ "invalid attribute value after `=`. "
assert result.position == {1, 10}
assert result.severity == :error
assert result.source == "HEEx"
assert result.uri =~ "file:///file.heex"
end
end

describe "function components" do
@tag :skip
test "handles undefined function component", %{project: project} do
path = "lib/simple_live.html.heex"
content = ~q[
<.greets_world name={@name} />
]
document = Document.new(path, content, 0)

assert {:error, [error]} = compile(project, document)
assert error.message =~ "undefined function \"greets_world\""
end
end
end

0 comments on commit adc5a4f

Please sign in to comment.