Skip to content

Commit

Permalink
Our first plugin!
Browse files Browse the repository at this point in the history
This is the lexical credo plugin, for a time, the way to get your
credo issues into your editor. It makes use of the new plugin
architecture, and is largely copied from Scott's earlier PR.

I found that it works surprisingly well for being so simple.
scohen committed Jun 13, 2023
1 parent db87e1d commit e32f161
Showing 10 changed files with 221 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .credo.exs
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@
},
plugins: [],
requires: [],
strict: false,
strict: true,
parse_timeout: 5000,
color: true,
checks: [
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ defmodule Lexical.LanguageServer.MixProject do
[
{:ex_doc, "~> 0.29.1", only: :dev, runtime: false},
{:credo, "~> 1.7", only: [:dev, :test]},
{:lexical_credo, path: "projects/lexical_credo", only: [:dev, :test]},
Mix.Dialyzer.dependency()
]
end
4 changes: 4 additions & 0 deletions projects/lexical_credo/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
26 changes: 26 additions & 0 deletions projects/lexical_credo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
lexical_credo-*.tar

# Temporary files, for example, from tests.
/tmp/
21 changes: 21 additions & 0 deletions projects/lexical_credo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# LexicalCredo

**TODO: Add description**

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `lexical_credo` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:lexical_credo, "~> 0.1.0"}
]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/lexical_credo>.

100 changes: 100 additions & 0 deletions projects/lexical_credo/lib/lexical_credo.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
defmodule LexicalCredo do
alias Lexical.Document
alias Lexical.Plugin.Diagnostic
alias Lexical.Project

use Diagnostic, name: :lexical_credo
require Logger

def init do
with {:ok, _} <- Application.ensure_all_started(:credo) do
:ok
end
end

def document do
%Document{}
end

def handle(%Document{} = doc) do
doc_contents = Document.to_string(doc)

execution_args = [
"--mute-exit-status",
"--read-from-stdin",
Document.Path.absolute_from_uri(doc.uri)
]

execution = Credo.Execution.build(execution_args)

with_stdin(
doc_contents,
fn ->
Credo.CLI.Output.Shell.suppress_output(fn ->
Credo.Execution.run(execution)
end)
end
)

diagnostics =
execution
|> Credo.Execution.get_issues()
|> Enum.map(&to_diagnostic/1)

{:ok, diagnostics}
end

def handle(%Project{}) do
results =
Credo.Execution.build()
|> Credo.Execution.run()
|> Credo.Execution.get_issues()
|> Enum.map(&to_diagnostic/1)

{:ok, results}
end

def with_stdin(stdin_contents, function) when is_function(function, 0) do
{:ok, stdio} = StringIO.open(stdin_contents)
caller = self()

spawn(fn ->
Process.group_leader(self(), stdio)
result = function.()
send(caller, {:result, result})
end)

receive do
{:result, result} ->
{:ok, result}
end
end

defp to_diagnostic(%Credo.Issue{} = issue) do
file_path = Document.Path.ensure_uri(issue.filename)

Diagnostic.Result.new(
file_path,
location(issue),
issue.message,
priority_to_severity(issue),
"Credo"
)
end

defp priority_to_severity(%Credo.Issue{priority: priority}) do
case Credo.Priority.to_atom(priority) do
:higher -> :error
:high -> :warning
:normal -> :information
_ -> :hint
end
end

defp location(%Credo.Issue{} = issue) do
case {issue.line_no, issue.column} do
{line, nil} -> line
location -> location
end
end
end
30 changes: 30 additions & 0 deletions projects/lexical_credo/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule LexicalCredo.MixProject do
use Mix.Project

def project do
[
app: :lexical_credo,
version: "0.1.0",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger],
env: [lexical_plugin: true]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:lexical_plugin, path: "../lexical_plugin"},
{:credo, "> 0.0.0", optional: true},
{:jason, "> 0.0.0", optional: true}
]
end
end
6 changes: 6 additions & 0 deletions projects/lexical_credo/mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
%{
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
}
31 changes: 31 additions & 0 deletions projects/lexical_credo/test/lexical_credo_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule LexicalCredoTest do
alias Lexical.Plugin.Diagnostic.Result
alias Lexical.Document

import LexicalCredo
use ExUnit.Case

def doc(contents) do
Document.new("file:///file.ex", contents, 1)
end

test "reports errors on documents" do
has_inspect =
"""
defmodule Bad do
def test do
IO.inspect("hello")
end
end
"""
|> doc()
|> handle()

assert {:ok, [%Result{} = result]} = has_inspect
assert result.position == {3, 5}
assert result.message == "There should be no calls to IO.inspect/1."
assert String.ends_with?(result.uri, "/file.ex")
assert result.severity == :error
assert result.source == "Credo"
end
end
1 change: 1 addition & 0 deletions projects/lexical_credo/test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()

0 comments on commit e32f161

Please sign in to comment.