Packages
mob_new
0.3.12
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.45
0.1.44
0.1.43
0.1.42
0.1.40
0.1.33
0.1.30
0.1.29
0.1.28
0.1.27
0.1.26
0.1.25
0.1.24
0.1.23
0.1.21
0.1.20
0.1.19
0.1.18
0.1.17
0.1.16
0.1.15
0.1.14
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Project generator for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mob_new/templates/lint.ex
defmodule MobNew.Templates.Lint do
@moduledoc """
Structural lints for generator-rendered native source files.
EEx templates can produce output that string-match generator tests
miss — duplicate imports, missing braces, orphan `<%=` tags. Real
compilation would catch them but is expensive (needs Android SDK
for Kotlin, Xcode for Swift, etc.). These lints are the cheap
middle layer: language-agnostic structural checks that run in
sub-second time on every `mix test`.
Each check returns a list of `issue` maps; the empty list means
no problems. Aggregate functions (`check_kotlin/1`, `check_c/1`,
`check_swift/1`) run all checks relevant to a language and return
the combined issue list.
Use:
content = File.read!("path/to/MobBridge.kt")
assert MobNew.Templates.Lint.check_kotlin(content) == []
Or for finer-grained reporting:
content
|> MobNew.Templates.Lint.check_kotlin()
|> Enum.each(&IO.puts(&1.message))
Limits: structural only. A typo that produces still-balanced
braces (`x_funtion` instead of `x_function`) won't be caught.
See `mix mob.lint_templates` (planned) for a fuller battery that
also invokes `zig cc -fsyntax-only` on rendered C.
"""
@type issue :: %{
required(:kind) => atom(),
required(:message) => String.t()
}
# ── Public per-language aggregate checks ─────────────────────────────────
@doc """
Runs all Kotlin-relevant checks on the given content. Returns
combined issue list (empty = clean).
"""
@spec check_kotlin(String.t()) :: [issue()]
def check_kotlin(content) do
[
&balanced_braces/1,
&balanced_parens/1,
&balanced_brackets/1,
&no_eex_leaks/1,
&unique_kotlin_imports/1
]
|> Enum.flat_map(& &1.(content))
end
@doc """
Runs all C-relevant checks on the given content. Returns
combined issue list.
"""
@spec check_c(String.t()) :: [issue()]
def check_c(content) do
[
&balanced_braces/1,
&balanced_parens/1,
&no_eex_leaks/1
]
|> Enum.flat_map(& &1.(content))
end
@doc """
Runs all Swift-relevant checks on the given content.
"""
@spec check_swift(String.t()) :: [issue()]
def check_swift(content) do
[
&balanced_braces/1,
&balanced_parens/1,
&no_eex_leaks/1,
&unique_swift_imports/1
]
|> Enum.flat_map(& &1.(content))
end
# ── Individual checks (each is `:ok` or `[issue, ...]`) ──────────────────
@doc """
Asserts equal counts of `{` and `}`. Naive — strings containing
literal braces would skew the count, but template C/Kotlin/Swift
output rarely has JSON or `printf("{...}")` strings.
"""
@spec balanced_braces(String.t()) :: [issue()]
def balanced_braces(content), do: balanced(content, ?{, ?}, :balanced_braces)
@doc "Asserts equal counts of `(` and `)`."
@spec balanced_parens(String.t()) :: [issue()]
def balanced_parens(content), do: balanced(content, ?(, ?), :balanced_parens)
@doc "Asserts equal counts of `[` and `]`."
@spec balanced_brackets(String.t()) :: [issue()]
def balanced_brackets(content), do: balanced(content, ?[, ?], :balanced_brackets)
@doc """
Catches `<%=` / `<%` / `%>` left in rendered output. Indicates a
malformed template tag or a render-pipeline misconfiguration that
emitted the literal text instead of evaluating it.
"""
@spec no_eex_leaks(String.t()) :: [issue()]
def no_eex_leaks(content) do
[{"<%=", :eex_open_eq}, {"<% ", :eex_open}, {" %>", :eex_close}]
|> Enum.filter(fn {pattern, _} -> String.contains?(content, pattern) end)
|> Enum.map(fn {pattern, kind} ->
%{
kind: kind,
message:
"Rendered output still contains `#{pattern}` — likely a malformed EEx tag in the template"
}
end)
end
@doc """
Asserts every `^import ` line in Kotlin output is unique. kotlinc
rejects duplicate imports with "Conflicting import" and the build
fails. This was the bug class that hit 0.3.2 → 0.3.4 twice.
"""
@spec unique_kotlin_imports(String.t()) :: [issue()]
def unique_kotlin_imports(content) do
duplicate_imports(content, "import ", :duplicate_kotlin_import)
end
@doc """
Asserts every `^import ` line in Swift output is unique. swiftc
is more forgiving than kotlinc (warns rather than errors on
duplicates) but Apple's archive-validation pass surfaces them.
"""
@spec unique_swift_imports(String.t()) :: [issue()]
def unique_swift_imports(content) do
duplicate_imports(content, "import ", :duplicate_swift_import)
end
@doc """
For Kotlin/C native bridges: every `external fun nativeFoo(...)`
declared in the Kotlin file must have a matching
`Java_..._MobBridge_nativeFoo` JNI thunk in the C file. Catches
the "added the Kotlin extern but forgot the C side" regression
(or vice versa). Pass both file contents.
The Java-class infix between `Java_` and `_native...` is
package-dependent; we just check the suffix matches.
"""
@spec external_fun_jni_consistency(String.t(), String.t()) :: [issue()]
def external_fun_jni_consistency(kotlin_content, c_content) do
# `@JvmStatic` is optional — Kotlin's `object MobBridge` makes every
# `external fun` callable from JNI; `@JvmStatic` only affects Java's
# caller-side syntax, not the JNI dispatch surface.
kotlin_externs =
~r/^\s*(?:@JvmStatic\s+)?external\s+fun\s+(native\w+)/m
|> Regex.scan(kotlin_content, capture: :all_but_first)
|> List.flatten()
|> MapSet.new()
c_thunks =
~r/Java_[\w_]+_MobBridge_(native\w+)\(/
|> Regex.scan(c_content, capture: :all_but_first)
|> List.flatten()
|> MapSet.new()
missing_in_c =
kotlin_externs |> MapSet.difference(c_thunks) |> MapSet.to_list() |> Enum.sort()
missing_in_kt =
c_thunks |> MapSet.difference(kotlin_externs) |> MapSet.to_list() |> Enum.sort()
[
for name <- missing_in_c do
%{
kind: :missing_jni_thunk,
message: "Kotlin declares `external fun #{name}` but C has no matching JNI thunk"
}
end,
for name <- missing_in_kt do
%{
kind: :missing_kotlin_extern,
message:
"C exports `Java_..._MobBridge_#{name}` but Kotlin has no matching `external fun`"
}
end
]
|> List.flatten()
end
# ── helpers ──────────────────────────────────────────────────────────────
defp balanced(content, open, close, kind) do
chars = String.to_charlist(content)
opens = Enum.count(chars, &(&1 == open))
closes = Enum.count(chars, &(&1 == close))
if opens == closes do
[]
else
[
%{
kind: kind,
message:
"Unbalanced #{[open]}#{[close]}: #{opens} `#{[open]}` vs #{closes} `#{[close]}` — " <>
"likely a missing #{[close]} (or extra #{[open]}). Check recent template edits."
}
]
end
end
defp duplicate_imports(content, prefix, kind) do
duplicates =
content
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.filter(&String.starts_with?(&1, prefix))
|> Enum.frequencies()
|> Enum.filter(fn {_, count} -> count > 1 end)
|> Enum.map(fn {imp, count} -> {imp, count} end)
for {imp, count} <- duplicates do
%{
kind: kind,
message: "Duplicate import: `#{imp}` appears #{count} times"
}
end
end
end