├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── TYPE_SYSTEM.md ├── mix │ └── tasks │ │ └── typelixir.ex ├── typelixir.ex └── typelixir │ ├── functions_extractor.ex │ ├── pattern_builder.ex │ ├── processor.ex │ ├── type_comparator.ex │ └── utils.ex ├── mix.exs ├── mix.lock └── test ├── test_helper.exs ├── typelixir ├── functions_extractor_test.exs ├── pattern_builder_test.exs ├── processor_test.exs └── type_comparator_test.exs └── typelixir_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | typelixir-*.tar 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mauricio Cassola 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typelixir 2 | 3 | The library proposes a type system that makes possible to perform static type-checking on a significant fragment of Elixir. 4 | 5 | An important feature of the type system is that it does not require any syntactic change to the language. Type information is provided by means of function signatures `@spec`. 6 | 7 | The approach is inspired by the so-called [gradual typing](https://en.wikipedia.org/wiki/Gradual_typing). 8 | 9 | The proposed type system is based on subtyping and is backwards compatible, as it allows the presence of non-typed code fragments. Represented as the `any` type. 10 | 11 | The code parts that are not statically type checked because of lack of typing information, will be type checked then at runtime. 12 | 13 | [Here](./lib/TYPE_SYSTEM.md) is the proposed type system and how to write the code to be statically type checked. 14 | 15 | ### Note 16 | 17 | The library is not extensive within the language. The scope of this work is to cover the expectations of a degree project for the [Facutlad de Ingeniería - UDELAR](https://www.fing.edu.uy/). 18 | 19 | Special thanks to our tutors Marcos Viera and Alberto Pardo. 20 | 21 | ## Documentation 22 | 23 | Documentation can be found at [https://hexdocs.pm/typelixir](https://hexdocs.pm/typelixir). 24 | 25 | ## Installation 26 | 27 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 28 | by adding `typelixir` to your list of dependencies in `mix.exs`: 29 | 30 | ```elixir 31 | def deps do 32 | [ 33 | {:typelixir, "~> 0.1.0"} 34 | ] 35 | end 36 | ``` 37 | 38 | After installing the dependency, you need to run: 39 | 40 | ```bash 41 | mix typelixir 42 | ``` 43 | 44 | ## License 45 | 46 | typelixir is licensed under the MIT license. 47 | 48 | See [LICENSE](./LICENSE) for the full license text. -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :typelixir, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:typelixir, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /lib/TYPE_SYSTEM.md: -------------------------------------------------------------------------------- 1 | # Typed Elixir 2 | 3 | ## Introduction 4 | 5 | Elixir possess some basic types such as `integer`, `float`, `string`, `boolean` and `atom` which are dynamically type-checked at runtime. 6 | 7 | The aim of this library is to introduce a type system in order to perform static typing on expressions of both basic types and structured types. Besides the basic types, our type system manipulates types for `lists`, `tuples`, `maps` and `functions`. We also include the type `any` as the supertype of all terms, and `none` as the empty type. 8 | 9 | Below there are some examples on how the library type checks the code (but they are not extensive to all cases, so if in doubt, give it a try!). 10 | 11 | ## Structured types 12 | 13 | Heterogeneous lists are not allowed. The following list generates a type error: 14 | 15 | ```elixir 16 | [1, "two", :three ] # wrong 17 | ``` 18 | 19 | Only homogeneous lists are allowed, like the following ones: 20 | 21 | ```elixir 22 | [1, 2, 3] # list of integer 23 | [[1, 2], [3, 4], [5, 6]] # list of integers lists 24 | ``` 25 | 26 | For maps, all keys must have the same type but each value can have its own type: 27 | 28 | ```elixir 29 | %{:age => 12, name: "John" } # map with atom keys 30 | %{1 => 12, name: => "John" } # wrong 31 | ``` 32 | 33 | For tuples, each element has its own type: 34 | 35 | ```elixir 36 | {12, "John"} # duple where the first element is an integer and the second a string 37 | ``` 38 | 39 | Last, integer type can be used as float because it is a subtype of it: 40 | 41 | ```elixir 42 | [1, 1.5, 2] # list of float 43 | %{1 => "one", 1.5 => "one dot five" } # map with float keys 44 | ``` 45 | 46 | ## Expressions 47 | 48 | For boolean expressions like `and`, `or` and `not` boolean types are expected: 49 | 50 | ```elixir 51 | true and false # false 52 | true and (0 < 1) # true 53 | true or 1 # wrong 54 | ``` 55 | 56 | In the case of comparison operators we are more flexible within Elixir's philosophy. We allow any value even from different types to be compared with each other. However, the return type is always boolean. Therefore: 57 | 58 | ```elixir 59 | ("hi" > 5.0) or false # true 60 | ("hi" > 5.0) * 3 # wrong 61 | ``` 62 | 63 | For `case` sentences, the expression must have the same type as the guards, and the return type of all guards must be the same: 64 | 65 | ```elixir 66 | case 1 > 0 do 67 | true -> 1 68 | false -> 1.5 69 | end # 1 70 | 71 | case 1 + 2 do 72 | 2 -> "This is wrong" 73 | 3 -> "This is right" 74 | end # "This is right" 75 | 76 | case 1 + 2 do 77 | "tres" -> "This is wrong" 78 | 3 -> "This is right" 79 | end # wrong 80 | 81 | case 1 + 2 do 82 | 1 -> :wrong 83 | 3 -> "This is right" 84 | end # wrong 85 | ``` 86 | 87 | The behaviour for `if` and `unless` is the same and for the `cond` sentence, a boolean condition is always expected on each guard. 88 | 89 | ## Function specifications 90 | 91 | The library uses the reserved word `@spec` for functions specs. 92 | 93 | It doesn't type-check functions defined with `when` conditions, they will be check at runtime as Elixir does. 94 | 95 | One of the main objectives in the design of our type system is to be backwards compatible to allow working with legacy code. In order to do so, we allow the existence of `untyped functions`. We can also see them as functions that doesn't have a `@spec` specification. 96 | 97 | In the following example we define a function that takes an integer and returns a float: 98 | 99 | ``` elixir 100 | @spec func1(integer) :: float 101 | def func1(x) do 102 | x * 42.0 103 | end 104 | ``` 105 | 106 | Function `func1` can be correctly applied to an integer: 107 | 108 | ```elixir 109 | func1(2) # 84.0 110 | ``` 111 | 112 | But other kind of applications will fail: 113 | 114 | ```elixir 115 | func1(2.0) # wrong 116 | func1("2") # wrong 117 | ``` 118 | 119 | We can also define functions using the `any` type to avoid the type-check: 120 | 121 | ```elixir 122 | @spec func2(any) :: boolean 123 | def func2(x) do 124 | x == x 125 | end 126 | ``` 127 | 128 | All types are subtypes of this one, so this function can be called with any value: 129 | 130 | ```elixir 131 | func2(1) # true 132 | func2("one") # true 133 | func2([1, 2, 3]) # true 134 | ``` 135 | 136 | If we want to specify a function with a list of integers as a parameter we write: 137 | 138 | ```elixir 139 | @spec func3([integer]) :: integer 140 | def func3([]) do 141 | 0 142 | end 143 | 144 | def func3([head|tail]) do 145 | 1 + func3(tail) 146 | end 147 | ``` 148 | 149 | This function can be called: 150 | 151 | ```elixir 152 | func3([]) # 0 153 | func3([1, 2, 3]) # 3 154 | 155 | func3(["1", "2", "3"]) # wrong 156 | func3([:one, :two, :three]) # wrong 157 | func3([1, :two, "three"]) # wrong 158 | ``` 159 | 160 | Note that the empty list can be used as a list of any type. 161 | 162 | Also, we can define a function applicable to all list types using the `any` type: 163 | 164 | ```elixir 165 | @spec func4([any]) :: integer 166 | def func4([]) do 167 | 0 168 | end 169 | 170 | def func4([head|tail]) do 171 | 1 + func4(tail) 172 | end 173 | ``` 174 | 175 | So now we can have `func4` calls like the following: 176 | 177 | ```elixir 178 | func4([]) # 0 179 | func4([1, 2, 3]) # 3 180 | func4(["1", "2", "3"]) # 3 181 | func4([:one, :two, :three]) # 3 182 | 183 | func4([1, :two, "three"]) # wrong 184 | ``` 185 | 186 | A map with more key-value pairs can be used instead of a map with less entries. The next function is applicable to maps that have at least one key-value pair, with atom keys and the first value has atom type: 187 | 188 | ```elixir 189 | @spec func5(%{atom => atom}) :: boolean 190 | def func5(map) do 191 | map[:key1] == :one 192 | end 193 | ``` 194 | 195 | So this function can be called with: 196 | 197 | ```elixir 198 | func5(%{:key1=>:three, :key2=>:three, :key3=>"three"}) # false 199 | func5(%{:key1=>:one, :key2=>:two, :key3=>"three"}) # true 200 | 201 | func5(%{"1"=>:one, "two"=>:two}) # wrong -> keys are not atoms 202 | func5(%{:key1=>:one, "two"=>:two, 3=>:three}) # wrong -> keys have different types 203 | func5(%{}) # wrong -> has less key-value pairs 204 | ``` 205 | 206 | If we want to specify a function that takes a map with any key type as a param, we can use the `none` type because, as usual, maps are `covariant` on its key and we have to use the lower type. We can also say that the first elem must have the `any` type to admit maps with any value types: 207 | 208 | ```elixir 209 | @spec func6(%{none => any}) :: boolean 210 | def func6(map) do 211 | map[:key1] == :one 212 | end 213 | ``` 214 | 215 | Some invocations to this function are: 216 | 217 | ```elixir 218 | func6(%{"one"=>:one, "two"=>2, "three"=>"three"}) # false 219 | func6(%{"one"=>1, "two"=>2, "three"=>3}) # false 220 | func6(%{:key1=>:one, :key2=>2, :key3=>"three"}) # true 221 | func6(%{:key1=>:one, :key2=>:two, :key3=>:three}) # false 222 | 223 | func6(%{1=>:one, :two=>2, "three"=>"three"}) # wrong -> keys have different types 224 | func6(%{}) # wrong -> has less key-value pairs 225 | ``` 226 | 227 | ### Return types 228 | 229 | If we don't want to specify the return type we can denote it as `any`: 230 | 231 | ```elixir 232 | @spec func8([any]) :: any 233 | def func8(list) do 234 | [head | tail] = list 235 | head 236 | end 237 | ``` 238 | 239 | This function can be called as: 240 | 241 | ```elixir 242 | func8(["one", "two", "three"]) # "one" 243 | func8([1, 2, 3]) # 1 244 | func8([:one, :two, :three]) # :one 245 | func8([[1,2,3], [4,5,6], [7,8,9]]) # [1,2,3] 246 | ``` 247 | 248 | As we did with parameters, we can specify that the return type is a list of any: 249 | 250 | ```elixir 251 | @spec func9([any]) :: [any] 252 | def func9(list) do 253 | [head | tail] = list 254 | tail 255 | end 256 | ``` 257 | 258 | Some examples of its usage are: 259 | 260 | ```elixir 261 | func9([1]) # [] 262 | func9([1,2]) # [2] 263 | func9([1.1, 2.0]) # [2.0] 264 | func9(["one", "two", "three"]) # ["two", "three"] 265 | func9([:one, :two]) # [:two] 266 | func9([{1,"one"}, {2,"two"}, {3,"three"}]) # [{2,"two"}, {3,"three"}] 267 | func9([%{1 => 3}, %{2 => "4"}, %{3 => :cinco}]) # [%{2 => "4"}, %{3 => :cinco}] 268 | ``` 269 | In the same way, this behaviour can be obtained for maps and tuples. 270 | 271 | ### Runtime errors 272 | 273 | Expressions with `any` type can be used anywhere so we could have: 274 | 275 | ```elixir 276 | func3(func9([0,1])) # 2 277 | func3(func9(['a', 'b'])) # runtime error 278 | ``` 279 | 280 | Statically both functions are correctly type-checked but dynamically the second one will fail. 281 | 282 | As we mentioned before, functions without typing specification have the same behaviour. For example, the following expression type-checks correctly: 283 | 284 | ```elixir 285 | id(8) + 10 # 18 286 | ``` 287 | 288 | But the following will fail at runtime: 289 | 290 | ```elixir 291 | "hello" <> Main.fact(9) # runtime error 292 | id(8) and true # runtime error 293 | ``` 294 | 295 | ## Closing thoughts 296 | 297 | We strongly believe that there's plenty of room for further research and improvement of the language in this area. 298 | 299 | The library is not extensive to all the language, we are missing some important operators such as `|>` or the mentioned `when`. 300 | 301 | It's a proof of concept, the scope of this work is just to cover the expectations of a degree project. 302 | -------------------------------------------------------------------------------- /lib/mix/tasks/typelixir.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Typelixir do 2 | use Mix.Task 3 | 4 | def run(_) do 5 | prepare() 6 | Typelixir.check(get_paths()) 7 | end 8 | 9 | defp prepare() do 10 | Mix.Task.run("compile", []) 11 | end 12 | 13 | defp get_paths() do 14 | paths = 15 | Mix.Project.config()[:elixirc_paths] 16 | |> Mix.Utils.extract_files([:ex]) 17 | 18 | IO.puts "\nTypelixir -> Compiling #{length(paths)} file#{if (length(paths) > 1), do: "s"} (.ex)\n" 19 | paths 20 | end 21 | end -------------------------------------------------------------------------------- /lib/typelixir.ex: -------------------------------------------------------------------------------- 1 | defmodule Typelixir do 2 | @moduledoc false 3 | 4 | require Typelixir.Utils 5 | alias Typelixir.{FunctionsExtractor, Processor} 6 | 7 | @env %{ 8 | state: :ok, 9 | type: nil, 10 | error_data: %{}, 11 | data: %{}, 12 | prefix: nil, 13 | vars: %{}, 14 | functions: %{} 15 | } 16 | 17 | def check(paths) do 18 | env_functions = pre_compile_files(paths) 19 | 20 | Typelixir.Utils.manage_results(env_functions[:results]) do 21 | Typelixir.Utils.manage_results(compile_files(paths, env_functions[:functions])) do 22 | IO.puts "#{IO.ANSI.green()}All type checks have passed\n" 23 | :ok 24 | end 25 | end 26 | end 27 | 28 | defp pre_compile_files(paths) do 29 | Enum.reduce(paths, %{results: [], functions: %{}}, fn path, acc -> 30 | result = FunctionsExtractor.extract_functions_file(path, %{@env | functions: acc[:functions]}) 31 | 32 | %{acc | 33 | functions: Map.merge(acc[:functions], result[:functions]), 34 | results: acc[:results] ++ [{"#{path}", result[:state], result[:data]}]} 35 | end) 36 | end 37 | 38 | defp compile_files(paths, env_functions) do 39 | Enum.reduce(paths, [], fn path, acc -> 40 | result = Processor.process_file(path, %{@env | functions: env_functions}) 41 | acc ++ [{"#{path}", result[:state], result[:data]}] 42 | end) 43 | end 44 | 45 | defp print_state({path, :error, error}) do 46 | IO.puts "#{IO.ANSI.red()}error:#{IO.ANSI.white()} #{elem(error, 1)} \n\s\s#{path}:#{elem(error, 0)}\n" 47 | end 48 | 49 | defp print_state(_), do: nil 50 | end 51 | -------------------------------------------------------------------------------- /lib/typelixir/functions_extractor.ex: -------------------------------------------------------------------------------- 1 | defmodule Typelixir.FunctionsExtractor do 2 | @moduledoc false 3 | 4 | alias Typelixir.{PatternBuilder, TypeComparator, Utils} 5 | 6 | # extends the given functions env map with the module name and the functions it defines 7 | 8 | def extract_functions_file(path, env) do 9 | ast = Code.string_to_quoted(File.read!(Path.absname(path))) 10 | {_ast, result} = Macro.prewalk(ast, env, &extract(&1, &2)) 11 | 12 | Utils.prepare_result_data(result) 13 | end 14 | 15 | # MODULES 16 | # --------------------------------------------------------------------------------------------------- 17 | 18 | # {:defmodule, _, MODULE} 19 | defp extract({:defmodule, [line: line], [{:__aliases__, meta, module_name}, [do: block]]}, env) do 20 | elem = {:defmodule, [line: line], [{:__aliases__, meta, module_name}, [do: {:__block__, [], []}]]} 21 | name = 22 | module_name 23 | |> Enum.map(fn name -> Atom.to_string(name) end) 24 | |> Enum.join(".") 25 | 26 | new_mod_name = if env[:prefix], do: env[:prefix] <> "." <> name, else: name 27 | new_functions = Map.put(env[:functions], new_mod_name, Map.new()) 28 | {_ast, result} = Macro.prewalk(block, %{env | functions: new_functions, prefix: new_mod_name}, &extract(&1, &2)) 29 | 30 | {elem, %{env | state: result[:state], error_data: result[:error_data], functions: Map.merge(env[:functions], result[:functions])}} 31 | end 32 | 33 | # FUNCTIONS 34 | # --------------------------------------------------------------------------------------------------- 35 | 36 | defp extract({:@, [line: line], [{:spec, _, [{:::, _, [{fn_name, _, type_of_args}, type_of_return]}]}]} = elem, env) do 37 | type_of_args = Enum.map(type_of_args || [], fn type -> PatternBuilder.type(type, %{}) end) 38 | 39 | case TypeComparator.has_type?(type_of_args, :error) do 40 | true -> {elem, %{env | state: :error, error_data: Map.put(env[:error_data], line, "Malformed type spec on #{fn_name}/#{length(type_of_args)} parameters")}} 41 | _ -> 42 | return_type = PatternBuilder.type(type_of_return, %{}) 43 | 44 | case TypeComparator.has_type?(return_type, :error) do 45 | true -> {elem, %{env | state: :error, error_data: Map.put(env[:error_data], line, "Malformed type spec on #{fn_name}/#{length(type_of_args)} return")}} 46 | _ -> 47 | fn_type = {return_type, type_of_args} 48 | fn_key = {fn_name, length(type_of_args)} 49 | 50 | case (env[:functions][env[:prefix]][fn_key]) do 51 | nil -> 52 | new_module_map = Map.put(env[:functions][env[:prefix]], {fn_name, length(type_of_args)}, fn_type) 53 | new_functions = Map.put(env[:functions], env[:prefix], new_module_map) 54 | 55 | {elem, %{env | functions: new_functions}} 56 | _ -> 57 | {elem, %{env | state: :error, error_data: Map.put(env[:error_data], line, "#{fn_name}/#{length(type_of_args)} already has a defined type")}} 58 | end 59 | end 60 | end 61 | end 62 | 63 | # BASE CASE 64 | # --------------------------------------------------------------------------------------------------- 65 | 66 | defp extract(elem, env), do: {elem, env} 67 | end 68 | -------------------------------------------------------------------------------- /lib/typelixir/pattern_builder.ex: -------------------------------------------------------------------------------- 1 | defmodule Typelixir.PatternBuilder do 2 | 3 | alias Typelixir.{TypeComparator} 4 | 5 | # --------------------------------------------------------------------------------------------------- 6 | # type -> returns the type of types defined on @spec 7 | # -> returns the type of any pattern 8 | 9 | def type({:list, _, [type]}, env), do: {:list, type(type, env)} 10 | 11 | def type({:tuple, _, [types_list]}, env), do: {:tuple, Enum.map(types_list, fn type -> type(type, env) end)} 12 | 13 | def type({:map, _, [key_type, value_type]}, env), do: {:map, {type(key_type, env), type(value_type, env)}} 14 | 15 | def type({:_, _, _}, _env), do: :any 16 | 17 | # @spec 18 | def type({type, _, _}, _env) when (type in [:string, :boolean, :integer, :float, :atom, :any, :none]), do: type 19 | 20 | # tuple more 2 elems 21 | def type({:{}, _, list}, env), do: {:tuple, Enum.map(list, fn t -> type(t, env) end)} 22 | 23 | # map 24 | def type({:%{}, _, []}, _env), do: {:map, {:any, :any}} 25 | 26 | def type({:%{}, _, list}, env) do 27 | keys_values = Enum.map(list, fn {key, elem} -> {type(key, env), type(elem, env)} end) 28 | {:map, { 29 | elem(Enum.reduce(keys_values, fn {k_acc, _}, {k_e, _} -> {TypeComparator.supremum(k_acc, k_e), :_} end), 0), 30 | Enum.map(keys_values, fn {_, v} -> v end) 31 | }} 32 | end 33 | 34 | def type({:|, _, [operand1, operand2]}, env), 35 | do: {:list, TypeComparator.supremum(type(operand1, env), type(operand2, env))} 36 | 37 | # binding 38 | def type({:=, _, [operand1, operand2]}, env), do: TypeComparator.supremum(type(operand1, env), type(operand2, env)) 39 | 40 | # variables 41 | def type({value, _, _}, env) do 42 | case env[:vars][value] do 43 | nil -> :any 44 | type -> type 45 | end 46 | end 47 | 48 | # list 49 | def type([], _env), do: {:list, :any} 50 | 51 | def type(value, env) when is_list(value), 52 | do: {:list, TypeComparator.supremum(Enum.map(value, fn t -> type(t, env) end))} 53 | 54 | # tuple 2 elems 55 | def type(value, env) when is_tuple(value), 56 | do: {:tuple, Enum.map(Tuple.to_list(value), fn t -> type(t, env) end)} 57 | 58 | # literals 59 | def type(value, _env) do 60 | cond do 61 | value === nil -> :atom 62 | is_boolean(value) -> :boolean 63 | is_bitstring(value) -> :string 64 | is_integer(value) -> :integer 65 | is_float(value) -> :float 66 | is_atom(value) -> :atom 67 | true -> :any 68 | end 69 | end 70 | 71 | # --------------------------------------------------------------------------------------------------- 72 | # vars -> returns a map with the vars of params and the corresponding types of 73 | # param_type_list, or {:error, "message"} 74 | 75 | def vars(params, param_type_list) do 76 | new_vars = 77 | Enum.zip(params, param_type_list) 78 | |> Enum.map(fn {var, type} -> get_vars(var, type) end) 79 | |> List.flatten() 80 | 81 | case new_vars[:error] do 82 | nil -> 83 | Enum.reduce_while(new_vars, %{}, fn {var, type}, acc -> 84 | t = Map.get(acc, var) 85 | cond do 86 | t === nil or t === type -> {:cont, Map.put(acc, var, type)} 87 | true -> {:halt, {:error, "Variable #{var} is already defined with type #{t}"}} 88 | end 89 | end) 90 | message -> {:error, message} 91 | end 92 | end 93 | 94 | defp get_vars(_, :any), do: [] 95 | 96 | defp get_vars({op, _, _}, type) when (op not in [:{}, :%{}, :=, :_, :|]), do: {op, type} 97 | 98 | defp get_vars({:_, _, _}, _type), do: [] 99 | 100 | defp get_vars({:=, _, [operand1, operand2]}, type), 101 | do: [get_vars(operand1, type), get_vars(operand2, type)] 102 | 103 | defp get_vars([], {:list, _type}), do: [] 104 | 105 | defp get_vars(op, {:list, type}) when is_list(op), do: Enum.map(op, fn x -> get_vars(x, type) end) 106 | 107 | defp get_vars({:|, _, [operand1, operand2]}, {:list, type}), 108 | do: [get_vars(operand1, type), get_vars(operand2, {:list, type})] 109 | 110 | defp get_vars(_, {:list, _}), do: {:error, "Parameters does not match type specification"} 111 | 112 | defp get_vars([], _), do: {:error, "Parameters does not match type specification"} 113 | 114 | defp get_vars({:|, _, _}, _), do: {:error, "Parameters does not match type specification"} 115 | 116 | defp get_vars({:%{}, _, op}, {:map, {_, value_types}}), do: Enum.zip(op, value_types) |> Enum.map(fn {{_, value}, value_type} -> get_vars(value, value_type) end) 117 | 118 | defp get_vars({:%{}, _, _}, _), do: {:error, "Parameters does not match type specification"} 119 | 120 | defp get_vars(_, {:map, {_, _}}), do: {:error, "Parameters does not match type specification"} 121 | 122 | defp get_vars({:{}, _, ops}, {:tuple, type_list}), do: get_vars_tuple(ops, type_list) 123 | 124 | defp get_vars(ops, {:tuple, type_list}) when is_tuple(ops), do: get_vars_tuple(Tuple.to_list(ops), type_list) 125 | 126 | defp get_vars({:{}, _, _}, _), do: {:error, "Parameters does not match type specification"} 127 | 128 | defp get_vars(_, {:tuple, _}), do: {:error, "Parameters does not match type specification"} 129 | 130 | defp get_vars(value, type) when (type in [:string, :boolean, :integer, :float, :atom, :any]) do 131 | cond do 132 | type === :any or 133 | (is_boolean(value) and type === :boolean) or 134 | (is_bitstring(value) and type === :string) or 135 | (is_integer(value) and (type === :integer or type === :float)) or 136 | (is_float(value) and type === :float) or 137 | (is_atom(value) and type === :atom) 138 | -> [] 139 | true -> {:error, "Parameters does not match type specification"} 140 | end 141 | end 142 | 143 | defp get_vars(_, _), do: {:error, "Parameters does not match type specification"} 144 | 145 | defp get_vars_tuple(ops, type_list) do 146 | if length(ops) === length(type_list), 147 | do: Enum.zip(ops, type_list) |> Enum.map(fn {var, type} -> get_vars(var, type) end), 148 | else: {:error, "The number of parameters in tuple does not match the number of types"} 149 | end 150 | end -------------------------------------------------------------------------------- /lib/typelixir/processor.ex: -------------------------------------------------------------------------------- 1 | defmodule Typelixir.Processor do 2 | @moduledoc false 3 | 4 | alias Typelixir.{PatternBuilder, TypeComparator, Utils} 5 | 6 | # FIRST 7 | # --------------------------------------------------------------------------------------------------- 8 | 9 | def process_file(path, env) do 10 | ast = Code.string_to_quoted(File.read!(Path.absname(path))) 11 | 12 | {_ast, result} = Macro.prewalk(ast, env, &process(&1, &2)) 13 | Utils.prepare_result_data(result) 14 | end 15 | 16 | # BASE CASES 17 | # --------------------------------------------------------------------------------------------------- 18 | 19 | # error 20 | defp process(elem, %{ 21 | state: :error, 22 | type: _, 23 | error_data: _, 24 | warnings: _, 25 | data: _, 26 | prefix: _, 27 | vars: _, 28 | functions: _ 29 | } = env), do: {elem, env} 30 | 31 | # block 32 | defp process({:__block__, _, _} = elem, env), do: {elem, env} 33 | 34 | # DEFMODULE 35 | # --------------------------------------------------------------------------------------------------- 36 | 37 | defp process({:defmodule, [line: line], [{:__aliases__, meta, module_name}, [do: block]]}, env) do 38 | elem = {:defmodule, [line: line], [{:__aliases__, meta, module_name}, [do: {:__block__, [], []}]]} 39 | 40 | name = 41 | module_name 42 | |> Enum.map(fn name -> Atom.to_string(name) end) 43 | |> Enum.join(".") 44 | new_mod_name = if env[:prefix], do: env[:prefix] <> "." <> name, else: name 45 | 46 | {_ast, result} = Macro.prewalk(block, %{env | vars: %{}, prefix: new_mod_name}, &process(&1, &2)) 47 | result = Utils.prepare_result_data(result) 48 | 49 | {elem, result} 50 | end 51 | 52 | # IMPORT 53 | # --------------------------------------------------------------------------------------------------- 54 | 55 | defp process({:import, [line: line], [{:__aliases__, _, module_name_ext}]}, env) do 56 | elem = {:import, [line: line], []} 57 | 58 | module_name = 59 | module_name_ext 60 | |> Enum.map(fn name -> Atom.to_string(name) end) 61 | |> Enum.join(".") 62 | 63 | case env[:functions][module_name] do 64 | nil -> {elem, env} 65 | _ -> {elem, %{env | functions: Map.put(env[:functions], env[:prefix], Map.merge(env[:functions][env[:prefix]], env[:functions][module_name]))}} 66 | end 67 | end 68 | 69 | # ALIAS 70 | # --------------------------------------------------------------------------------------------------- 71 | 72 | defp process({:alias, [line: line], [{:__aliases__, _, module_name_ext}]}, env) do 73 | elem = {:alias, [line: line], []} 74 | 75 | module_name = 76 | module_name_ext 77 | |> Enum.map(fn name -> Atom.to_string(name) end) 78 | |> Enum.join(".") 79 | 80 | case env[:functions][module_name] do 81 | nil -> {elem, env} 82 | _ -> {elem, %{env | functions: Map.put(env[:functions], Atom.to_string(Enum.at(module_name_ext, -1)), env[:functions][module_name])}} 83 | end 84 | end 85 | 86 | defp process({:alias, [line: line], [{:__aliases__, _, module_name_ext}, [as: {:__aliases__, _, as_module_name_ext}]]}, env) do 87 | elem = {:alias, [line: line], []} 88 | 89 | module_name = 90 | module_name_ext 91 | |> Enum.map(fn name -> Atom.to_string(name) end) 92 | |> Enum.join(".") 93 | 94 | as_module_name = 95 | as_module_name_ext 96 | |> Enum.map(fn name -> Atom.to_string(name) end) 97 | |> Enum.join(".") 98 | 99 | case env[:functions][module_name] do 100 | nil -> {elem, env} 101 | _ -> {elem, %{env | functions: Map.put(env[:functions], as_module_name, env[:functions][module_name])}} 102 | end 103 | end 104 | 105 | # FUNCTIONS DEF 106 | # --------------------------------------------------------------------------------------------------- 107 | 108 | defp process({:@, [line: line], [{:spec, _, [{:::, _, [{_fn_name, _, _type_of_args}, _type_of_return]}]}]}, env) do 109 | elem = {:@, [line: line], []} 110 | {elem, env} 111 | end 112 | 113 | defp process({defs, [line: line], [{function_name, _meta, params}, [do: block]]}, env) when (defs in [:def, :defp]) do 114 | elem = {defs, [line: line], []} 115 | 116 | params_length = length(params || []) 117 | fn_key = {function_name, params_length} 118 | 119 | case env[:functions][env[:prefix]][fn_key] do 120 | nil -> 121 | {_ast, result} = Macro.prewalk(block, %{env | vars: %{}}, &process(&1, &2)) 122 | result = Utils.prepare_result_data(result) 123 | 124 | {elem, result} 125 | _ -> 126 | return_type = env[:functions][env[:prefix]][fn_key] |> elem(0) 127 | param_type_list = env[:functions][env[:prefix]][fn_key] |> elem(1) 128 | params_vars = PatternBuilder.vars(params || [], param_type_list) 129 | 130 | case params_vars do 131 | {:error, msg} -> Utils.return_error(elem, env, {line, msg}) 132 | _ -> 133 | {_ast, result} = Macro.prewalk(block, %{env | vars: params_vars}, &process(&1, &2)) 134 | result = Utils.prepare_result_data(result) 135 | 136 | case result[:state] do 137 | :error -> {elem, result} 138 | _ -> 139 | case TypeComparator.supremum(result[:type], return_type) do 140 | :error -> Utils.return_error(elem, result, {line, "Body doesn't match function type on #{function_name}/#{params_length} declaration"}) 141 | _ -> {elem, result} 142 | end 143 | end 144 | end 145 | end 146 | end 147 | 148 | # FUNCTIONS CALL 149 | # --------------------------------------------------------------------------------------------------- 150 | 151 | defp process({{:., [line: line], [{:__aliases__, [line: line], mod_names}, fn_name]}, [line: line], args}, env) do 152 | elem = {{:., [line: line], [{:__aliases__, [line: line], []}, fn_name]}, [line: line], []} 153 | function_call_process(elem, line, mod_names, fn_name, args, env) 154 | end 155 | 156 | # BINDING 157 | # --------------------------------------------------------------------------------------------------- 158 | 159 | defp process({:=, [line: line], [pattern, expression]}, env) do 160 | elem = {:=, [line: line], []} 161 | 162 | {_ast, result} = Macro.prewalk(expression, env, &process(&1, &2)) 163 | result = Utils.prepare_result_data(result) 164 | 165 | case result[:state] do 166 | :error -> {elem, result} 167 | _ -> 168 | pattern = if is_list(pattern), do: pattern, else: [pattern] 169 | pattern_vars = PatternBuilder.vars(pattern, [result[:type]]) 170 | 171 | case pattern_vars do 172 | {:error, msg} -> Utils.return_error(elem, env, {line, msg}) 173 | _ -> Utils.return_merge_vars(elem, result, pattern_vars) 174 | end 175 | end 176 | end 177 | 178 | # NUMBER OPERATORS 179 | # --------------------------------------------------------------------------------------------------- 180 | 181 | defp process({operator, [line: line], [operand1, operand2]}, env) when (operator in [:*, :+, :-]) do 182 | elem = {operator, [line: line], []} 183 | binary_operator_process(elem, env, line, operator, operand1, operand2, :integer, false) 184 | end 185 | 186 | defp process({:/, [line: line], [operand1, operand2]}, env) do 187 | elem = {:/, [line: line], []} 188 | binary_operator_process(elem, env, line, :/, operand1, operand2, :float, false) 189 | end 190 | 191 | # neg 192 | defp process({:-, [line: line], [operand]}, env) do 193 | elem = {:-, [line: line], []} 194 | unary_operator_process(elem, env, line, :-, operand, :integer) 195 | end 196 | 197 | # BOOLEAN OPERATORS 198 | # --------------------------------------------------------------------------------------------------- 199 | 200 | defp process({operator, [line: line], [operand1, operand2]}, env) when (operator in [:and, :or]) do 201 | elem = {operator, [line: line], []} 202 | binary_operator_process(elem, env, line, operator, operand1, operand2, :boolean, false) 203 | end 204 | 205 | # not 206 | defp process({:not, [line: line], [operand]}, env) do 207 | elem = {:not, [line: line], []} 208 | unary_operator_process(elem, env, line, :not, operand, :boolean) 209 | end 210 | 211 | # COMPARISON OPERATORS 212 | # --------------------------------------------------------------------------------------------------- 213 | 214 | defp process({operator, [line: line], [operand1, operand2]}, env) when (operator in [:==, :!=, :>, :<, :<=, :>=, :===, :!==]) do 215 | elem = {operator, [line: line], []} 216 | binary_operator_process(elem, env, line, operator, operand1, operand2, :any, true) 217 | end 218 | 219 | # LIST OPERATORS 220 | # --------------------------------------------------------------------------------------------------- 221 | 222 | defp process({operator, [line: line], [operand1, operand2]}, env) when operator in [:++, :--] do 223 | elem = {operator, [line: line], []} 224 | binary_operator_process(elem, env, line, operator, operand1, operand2, {:list, :any}, false) 225 | end 226 | 227 | # STRING OPERATORS 228 | # --------------------------------------------------------------------------------------------------- 229 | 230 | defp process({:<>, [line: line], [operand1, operand2]}, env) do 231 | elem = {:<>, [line: line], []} 232 | binary_operator_process(elem, env, line, :<>, operand1, operand2, :string, false) 233 | end 234 | 235 | # IF/UNLESS 236 | # --------------------------------------------------------------------------------------------------- 237 | 238 | defp process({operator, [line: line], [condition, [do: do_block]]}, env) when operator in [:if, :unless] do 239 | elem = {operator, [line: line], []} 240 | 241 | {_ast, result_condition} = Macro.prewalk(condition, env, &process(&1, &2)) 242 | result_condition = Utils.prepare_result_data(result_condition) 243 | 244 | case result_condition[:state] do 245 | :error -> {elem, result_condition} 246 | _ -> 247 | case TypeComparator.supremum(result_condition[:type], :boolean) do 248 | :error -> Utils.return_error(elem, env, {line, "Type error on #{Atom.to_string(operator)} condition"}) 249 | _ -> 250 | {_ast, result_do_block} = Macro.prewalk(do_block, result_condition, &process(&1, &2)) 251 | result_do_block = Utils.prepare_result_data(result_do_block) 252 | 253 | case result_do_block[:state] do 254 | :error -> {elem, result_do_block} 255 | _ -> Utils.return_merge_vars(elem, %{env | type: result_do_block[:type]}, result_condition[:vars]) 256 | end 257 | end 258 | end 259 | end 260 | 261 | defp process({operator, [line: line], [condition, [do: do_block, else: else_block]]}, env) when operator in [:if, :unless] do 262 | elem = {operator, [line: line], []} 263 | 264 | {_ast, result_condition} = Macro.prewalk(condition, env, &process(&1, &2)) 265 | result_condition = Utils.prepare_result_data(result_condition) 266 | 267 | case result_condition[:state] do 268 | :error -> {elem, result_condition} 269 | _ -> 270 | case TypeComparator.supremum(result_condition[:type], :boolean) do 271 | :error -> Utils.return_error(elem, env, {line, "Type error on #{Atom.to_string(operator)} condition"}) 272 | _ -> 273 | {_ast, result_do_block} = Macro.prewalk(do_block, result_condition, &process(&1, &2)) 274 | result_do_block = Utils.prepare_result_data(result_do_block) 275 | 276 | case result_do_block[:state] do 277 | :error -> {elem, result_do_block} 278 | _ -> 279 | {_ast, result_else_block} = Macro.prewalk(else_block, result_condition, &process(&1, &2)) 280 | result_else_block = Utils.prepare_result_data(result_else_block) 281 | 282 | case result_else_block[:state] do 283 | :error -> {elem, result_else_block} 284 | _ -> 285 | case TypeComparator.supremum(result_do_block[:type], result_else_block[:type]) do 286 | :error -> Utils.return_error(elem, env, {line, "Type error on #{Atom.to_string(operator)} branches"}) 287 | type -> Utils.return_merge_vars(elem, %{env | type: type}, result_condition[:vars]) 288 | end 289 | end 290 | end 291 | end 292 | end 293 | end 294 | 295 | # COND 296 | # --------------------------------------------------------------------------------------------------- 297 | 298 | defp process({:cond, [line: line], [[do: branches]]}, env) do 299 | elem = {:cond, [line: line], []} 300 | 301 | Enum.reduce_while(branches, {elem, %{env | type: :any}}, 302 | fn {:->, [line: line], [[condition], do_block]}, {elem, acc_env} -> 303 | {_ast, result_condition} = Macro.prewalk(condition, acc_env, &process(&1, &2)) 304 | result_condition = Utils.prepare_result_data(result_condition) 305 | 306 | case result_condition[:state] do 307 | :error -> {:halt, {elem, result_condition}} 308 | _ -> 309 | case TypeComparator.supremum(result_condition[:type], :boolean) do 310 | :error -> {:halt, Utils.return_error(elem, acc_env, {line, "Type error on cond condition"})} 311 | _ -> 312 | {_ast, result_do_block} = Macro.prewalk(do_block, result_condition, &process(&1, &2)) 313 | result_do_block = Utils.prepare_result_data(result_do_block) 314 | 315 | case result_do_block[:state] do 316 | :error -> {:halt, {elem, result_do_block}} 317 | _ -> 318 | case TypeComparator.supremum(result_do_block[:type], acc_env[:type]) do 319 | :error -> {:halt, Utils.return_error(elem, acc_env, {line, "Type error on cond branches"})} 320 | type -> {:cont, {elem, %{acc_env | type: type}}} 321 | end 322 | end 323 | end 324 | end 325 | end) 326 | end 327 | 328 | # CASE 329 | # --------------------------------------------------------------------------------------------------- 330 | 331 | defp process({:case, [line: line], [condition, [do: branches]]}, env) do 332 | elem = {:case, [line: line], []} 333 | 334 | {_ast, result_condition} = Macro.prewalk(condition, env, &process(&1, &2)) 335 | result_condition = Utils.prepare_result_data(result_condition) 336 | 337 | case result_condition[:state] do 338 | :error -> {elem, result_condition} 339 | _ -> 340 | Enum.reduce_while(branches, {elem, %{env | vars: Map.merge(env[:vars], result_condition[:vars]), type: :any}}, 341 | fn {:->, [line: line], [[pattern], do_block]}, {elem, acc_env} -> 342 | pattern = if is_list(pattern), do: pattern, else: [pattern] 343 | pattern_vars = PatternBuilder.vars(pattern, [result_condition[:type]]) 344 | 345 | case pattern_vars do 346 | {:error, msg} -> {:halt, Utils.return_error(elem, env, {line, msg})} 347 | _ -> 348 | {_ast, result_do_block} = Macro.prewalk(do_block, %{acc_env | vars: Map.merge(acc_env[:vars], pattern_vars)}, &process(&1, &2)) 349 | result_do_block = Utils.prepare_result_data(result_do_block) 350 | 351 | case result_do_block[:state] do 352 | :error -> {:halt, {elem, result_do_block}} 353 | _ -> 354 | case TypeComparator.supremum(result_do_block[:type], acc_env[:type]) do 355 | :error -> {:halt, Utils.return_error(elem, acc_env, {line, "Type error on case branches"})} 356 | type -> {:cont, {elem, %{acc_env | type: type}}} 357 | end 358 | end 359 | end 360 | end) 361 | end 362 | end 363 | 364 | # LITERAL, VARIABLE, TUPLE, LIST, MAP 365 | # --------------------------------------------------------------------------------------------------- 366 | 367 | # tuple more 2 elems 368 | defp process({:{}, [line: line], list}, env) do 369 | elem = {:{}, [line: line], []} 370 | 371 | {types_list, result} = 372 | Enum.map(list, fn t -> elem(Macro.prewalk(t, env, &process(&1, &2)), 1) end) 373 | |> Enum.reduce_while({[], env}, fn result, {types_list, env_acc} -> 374 | result = Utils.prepare_result_data(result) 375 | 376 | case result[:state] do 377 | :error -> {:halt, {[], result}} 378 | _ -> {:cont, {types_list ++ [result[:type]], elem(Utils.return_merge_vars(elem, env_acc, result[:vars]), 1)}} 379 | end 380 | end) 381 | 382 | {{:{}, [line: line], []}, %{result | type: {:tuple, types_list}}} 383 | end 384 | 385 | # map 386 | defp process({:%{}, _, []} = elem, env), do: {elem, %{env | type: PatternBuilder.type(elem, env)}} 387 | 388 | defp process({:%{}, [line: line], list}, env) do 389 | elem = {:%{}, [line: line], []} 390 | 391 | keys = Enum.map(list, fn {k, _} -> k end) 392 | values = Enum.map(list, fn {_, v} -> v end) 393 | 394 | {type_key, result_key} = 395 | Enum.map(keys, fn t -> elem(Macro.prewalk(t, env, &process(&1, &2)), 1) end) 396 | |> Enum.reduce_while({:any, env}, fn result, {type_acc, env_acc} -> 397 | result = Utils.prepare_result_data(result) 398 | 399 | case result[:state] do 400 | :error -> {:halt, {:any, result}} 401 | _ -> 402 | case TypeComparator.supremum(result[:type], type_acc) do 403 | :error -> {:halt, Utils.return_error(elem, env, {line, "Malformed type map"})} 404 | type -> {:cont, {type, elem(Utils.return_merge_vars([], env_acc, result[:vars]), 1)}} 405 | end 406 | end 407 | end) 408 | 409 | case result_key[:state] do 410 | :error -> {elem, result_key} 411 | _ -> 412 | {types_values, result_value} = 413 | Enum.map(values, fn t -> elem(Macro.prewalk(t, env, &process(&1, &2)), 1) end) 414 | |> Enum.reduce_while({[], env}, fn result, {types_list, env_acc} -> 415 | result = Utils.prepare_result_data(result) 416 | 417 | case result[:state] do 418 | :error -> {:halt, {[], result}} 419 | _ -> {:cont, {types_list ++ [result[:type]], elem(Utils.return_merge_vars(elem, env_acc, result[:vars]), 1)}} 420 | end 421 | end) 422 | 423 | {elem, %{result_value | type: {:map, {type_key, types_values}}, vars: Map.merge(result_key[:vars], result_value[:vars])}} 424 | end 425 | end 426 | 427 | # map app 428 | defp process({{:., [line: line], [Access, :get]}, meta, [map, key]}, env) do 429 | elem = {{:., [line: line], [Access, :get]}, meta, []} 430 | map_app_process(elem, line, map, key, env) 431 | end 432 | 433 | defp process({{:., [line: line], [map, key]}, meta, []}, env) do 434 | elem = {{:., [line: line], []}, meta, []} 435 | map_app_process(elem, line, map, key, env) 436 | end 437 | 438 | # variables or local function 439 | defp process({value, [line: line], params}, env) do 440 | elem = {value, [line: line], []} 441 | case env[:vars][value] do 442 | nil -> 443 | if (env[:prefix] !== nil and is_list(params) and env[:functions][env[:prefix]][{value, length(params)}] !== nil) do 444 | function_call_process(elem, line, [String.to_atom(env[:prefix])], value, params, env) 445 | else 446 | {elem, %{env | type: :any}} 447 | end 448 | type -> {elem, %{env | type: type}} 449 | end 450 | end 451 | 452 | # list 453 | defp process([] = elem, env), do: {elem, %{env | type: PatternBuilder.type(elem, env)}} 454 | 455 | defp process([{:|, [line: line], [operand1, operand2]}], env) do 456 | elem = {:|, [line: line], []} 457 | binary_operator_process(elem, env, line, :|, operand1, operand2, {:list, :any}, false) 458 | end 459 | 460 | defp process(elem, env) when is_list(elem) do 461 | {type, result} = 462 | Enum.map(elem, fn t -> elem(Macro.prewalk(t, env, &process(&1, &2)), 1) end) 463 | |> Enum.reduce_while({:any, env}, fn result, {type_acc, env_acc} -> 464 | result = Utils.prepare_result_data(result) 465 | 466 | case result[:state] do 467 | :error -> {:halt, {:any, result}} 468 | _ -> 469 | case TypeComparator.supremum(result[:type], type_acc) do 470 | :error -> {:halt, Utils.return_error(elem, env, {"", "Malformed type list"})} # line? :( 471 | type -> {:cont, {type, elem(Utils.return_merge_vars([], env_acc, result[:vars]), 1)}} 472 | end 473 | end 474 | end) 475 | 476 | {[], %{result | type: {:list, type}}} 477 | end 478 | 479 | # tuple 2 elems 480 | defp process({elem1, elem2} = elem, env) when (elem1 !== :ok) do 481 | {types_list, result} = 482 | Enum.map([elem1, elem2], fn t -> elem(Macro.prewalk(t, env, &process(&1, &2)), 1) end) 483 | |> Enum.reduce_while({[], env}, fn result, {types_list, env_acc} -> 484 | result = Utils.prepare_result_data(result) 485 | 486 | case result[:state] do 487 | :error -> {:halt, {[], result}} 488 | _ -> {:cont, {types_list ++ [result[:type]], elem(Utils.return_merge_vars(elem, env_acc, result[:vars]), 1)}} 489 | end 490 | end) 491 | 492 | {{}, %{result | type: {:tuple, types_list}}} 493 | end 494 | 495 | # literals 496 | defp process(elem, env), do: {elem, %{env | type: PatternBuilder.type(elem, env)}} 497 | 498 | # OTHERS 499 | # --------------------------------------------------------------------------------------------------- 500 | 501 | defp function_call_process(elem, line, mod_names, fn_name, args, env) do 502 | mod_name = 503 | mod_names 504 | |> Enum.map(fn name -> Atom.to_string(name) end) 505 | |> Enum.join(".") 506 | spec_type = env[:functions][mod_name][{fn_name, length(args)}] 507 | 508 | if (spec_type) do 509 | {result_type, type_args} = spec_type 510 | 511 | args_check = Enum.reduce_while(Enum.zip(args, type_args), env, 512 | fn {arg, type}, acc_env -> 513 | {_ast, result} = Macro.prewalk(arg, acc_env, &process(&1, &2)) 514 | result = Utils.prepare_result_data(result) 515 | 516 | case TypeComparator.supremum(result[:type], type) do 517 | :error -> 518 | {:halt, %{acc_env | state: :error, error_data: Map.put(acc_env[:error_data], line, "Arguments does not match type specification on #{fn_name}/#{length(args)}")}} 519 | _ -> {:cont, Map.merge(acc_env, result)} 520 | end 521 | end) 522 | 523 | case args_check[:state] do 524 | :error -> {elem, args_check} 525 | _ -> {elem, %{args_check | type: result_type}} 526 | end 527 | else 528 | args_check = Enum.reduce(args, env, 529 | fn arg, acc_env -> 530 | {_ast, result} = Macro.prewalk(arg, acc_env, &process(&1, &2)) 531 | Map.merge(acc_env, Utils.prepare_result_data(result)) 532 | end) 533 | 534 | case args_check[:state] do 535 | :error -> {elem, args_check} 536 | _ -> {elem, %{args_check | type: :any}} 537 | end 538 | end 539 | end 540 | 541 | defp unary_operator_process(elem, env, line, operator, operand, max_type) do 542 | {_ast, result} = Macro.prewalk(operand, env, &process(&1, &2)) 543 | result = Utils.prepare_result_data(result) 544 | 545 | case result[:state] do 546 | :error -> {elem, result} 547 | _ -> 548 | supremum = TypeComparator.supremum(result[:type], max_type) 549 | case supremum do 550 | :error -> Utils.return_error(elem, env, {line, "Type error on #{Atom.to_string(operator)} operator"}) 551 | _ -> {elem, %{result | type: supremum}} 552 | end 553 | end 554 | end 555 | 556 | defp binary_operator_process(elem, env, line, operator, operand1, operand2, max_type, is_comparison) do 557 | {_ast, result_op1} = Macro.prewalk(operand1, env, &process(&1, &2)) 558 | result_op1 = Utils.prepare_result_data(result_op1) 559 | 560 | case result_op1[:state] do 561 | :error -> {elem, result_op1} 562 | _ -> 563 | {_ast, result_op2} = Macro.prewalk(operand2, env, &process(&1, &2)) 564 | result_op2 = Utils.prepare_result_data(result_op2) 565 | 566 | case result_op2[:state] do 567 | :error -> {elem, result_op2} 568 | _ -> 569 | cond do 570 | is_comparison -> Utils.return_merge_vars(elem, %{result_op1 | type: :boolean}, result_op2[:vars]) 571 | true -> 572 | type = 573 | cond do 574 | operator === :| and is_tuple(result_op2[:type]) -> TypeComparator.supremum({:list, result_op1[:type]}, result_op2[:type]) 575 | operator === :| -> {:list, TypeComparator.supremum(result_op1[:type], result_op2[:type])} 576 | true -> TypeComparator.supremum(result_op1[:type], result_op2[:type]) 577 | end 578 | 579 | cond do 580 | TypeComparator.has_type?(type, :error) === true -> Utils.return_error(elem, env, {line, "Type error on #{Atom.to_string(operator)} operator"}) 581 | true -> 582 | supremum = TypeComparator.supremum(type, max_type) 583 | case supremum do 584 | :error -> Utils.return_error(elem, env, {line, "Type error on #{Atom.to_string(operator)} operator"}) 585 | _ -> Utils.return_merge_vars(elem, %{result_op1 | type: supremum}, result_op2[:vars]) 586 | end 587 | end 588 | end 589 | end 590 | end 591 | end 592 | 593 | defp map_app_process(elem, line, map, key, env) do 594 | {_ast, result_map} = Macro.prewalk(map, env, &process(&1, &2)) 595 | result_map = Utils.prepare_result_data(result_map) 596 | 597 | case result_map[:state] do 598 | :error -> {elem, result_map} 599 | _ -> 600 | {_ast, result_key} = Macro.prewalk(key, env, &process(&1, &2)) 601 | result_key = Utils.prepare_result_data(result_key) 602 | 603 | case result_key[:state] do 604 | :error -> {elem, result_key} 605 | _ -> 606 | case result_map[:type] do 607 | {:map, {key_type, _value_types}} -> 608 | case TypeComparator.supremum(result_key[:type], key_type) do 609 | :error -> Utils.return_error(elem, env, {line, "Expected #{key_type} as key instead of #{result_key[:type]}"}) 610 | _ -> Utils.return_merge_vars(elem, %{result_map | type: :any}, result_key[:vars]) 611 | end 612 | _ -> Utils.return_error(elem, env, {line, "Not accessing to a map"}) 613 | end 614 | end 615 | end 616 | end 617 | end -------------------------------------------------------------------------------- /lib/typelixir/type_comparator.ex: -------------------------------------------------------------------------------- 1 | defmodule Typelixir.TypeComparator do 2 | # --------------------------------------------------------------------------------------------------- 3 | # supremum -> returns the supremum between type1 and type2 4 | 5 | def supremum(list_type) when is_list(list_type), do: Enum.reduce(list_type, fn acc, e -> supremum(acc, e) end) 6 | 7 | def supremum(type1, type2) when type1 === type2, do: type1 8 | 9 | def supremum(list_type1, list_type2) when is_list(list_type1) and is_list(list_type2), 10 | do: Enum.zip(list_type1, list_type2) |> Enum.map(fn {x, y} -> supremum(x, y) end) 11 | 12 | def supremum({:map, {key_type1, list_value_type1}}, {:map, {key_type2, list_value_type2}}), do: 13 | if (length(list_value_type1) >= length(list_value_type2)), do: 14 | {:map, {supremum(key_type1, key_type2), supremum(list_value_type1, list_value_type2)}}, else: :error 15 | 16 | def supremum({:tuple, list_type1}, {:tuple, list_type2}), do: 17 | if (length(list_type1) === length(list_type2)), do: {:tuple, supremum(list_type1, list_type2)}, else: :error 18 | 19 | def supremum({:list, type1}, {:list, type2}), do: {:list, supremum(type1, type2)} 20 | 21 | def supremum(:integer, :float), do: :float 22 | 23 | def supremum(:float, :integer), do: :float 24 | 25 | # -- downcast 26 | def supremum(:any, type), do: type 27 | 28 | def supremum(type, :any), do: type 29 | # -- 30 | 31 | def supremum(:none, type), do: type 32 | 33 | def supremum(type, :none), do: type 34 | 35 | def supremum(:error, _), do: :error 36 | 37 | def supremum(_, :error), do: :error 38 | 39 | def supremum(_, _), do: :error 40 | 41 | # --------------------------------------------------------------------------------------------------- 42 | # has_type? -> returns true if type1 contains type2 43 | 44 | def has_type?(list_type, type) when is_list(list_type) do 45 | Enum.map(list_type, fn t -> has_type?(t, type) end) |> Enum.member?(true) 46 | end 47 | 48 | def has_type?({:map, {key_type, list_value_type}}, type), 49 | do: has_type?(key_type, type) or has_type?(list_value_type, type) 50 | 51 | def has_type?({:tuple, list_type}, type), do: has_type?(list_type, type) 52 | 53 | def has_type?({:list, list_type}, type), do: has_type?(list_type, type) 54 | 55 | def has_type?(type1, type2) when type1 === type2, do: true 56 | 57 | def has_type?(_, _), do: false 58 | end -------------------------------------------------------------------------------- /lib/typelixir/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Typelixir.Utils do 2 | @moduledoc false 3 | 4 | defmacro manage_results(results, do: ok_block) do 5 | quote do 6 | case Enum.filter(unquote(results), fn {_, status, _} -> status === :error end) do 7 | [] -> unquote(ok_block) 8 | errors -> 9 | Enum.each(unquote(results), fn state -> print_state(state) end) 10 | {:error, Enum.map(errors, fn {path, _, error} -> "#{elem(error, 1)} in #{path}:#{elem(error, 0)}" end)} 11 | end 12 | end 13 | end 14 | 15 | def prepare_result_data(result) do 16 | case result[:state] do 17 | :error -> 18 | data_merged = Enum.reduce(Map.to_list(result[:error_data]), fn acc, e -> if elem(acc, 0) < elem(e, 0), do: acc, else: e end) 19 | %{result | data: data_merged} 20 | _ -> result 21 | end 22 | end 23 | 24 | def return_error(elem, env, {line, message}) do 25 | {elem, %{env | state: :error, error_data: Map.put(env[:error_data], line, message)}} 26 | end 27 | 28 | def return_merge_vars(elem, env, new_vars) do 29 | {elem, %{env | vars: Map.merge(env[:vars], new_vars)}} 30 | end 31 | end -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Typelixir.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :typelixir, 7 | version: "0.1.0", 8 | elixir: "~> 1.10.0", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | description: description(), 12 | package: package() 13 | ] 14 | end 15 | 16 | # Run "mix help compile.app" to learn about applications. 17 | def application do 18 | [ 19 | extra_applications: [:logger] 20 | ] 21 | end 22 | 23 | # Run "mix help deps" to learn about dependencies. 24 | defp deps do 25 | [{:ex_doc, ">= 0.0.0", only: :dev, runtime: false}] 26 | end 27 | 28 | defp package do 29 | [ 30 | licenses: ["MIT"], 31 | links: %{"GitHub" => "https://github.com/mcass19/typelixir"}, 32 | maintainers: ["Mauricio Cassola", "Agustín Talagorría"], 33 | name: :typelixir, 34 | ] 35 | end 36 | 37 | defp description do 38 | """ 39 | Library to compile Elixir statically. 40 | """ 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm"}, 3 | "ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm"}, 7 | } 8 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/typelixir/functions_extractor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Typelixir.FunctionsExtractorTest do 2 | use ExUnit.Case 3 | alias Typelixir.FunctionsExtractor 4 | 5 | describe "extract_functions_file" do 6 | @test_dir "test/tmp" 7 | 8 | @env %{ 9 | :functions => %{ 10 | "ModuleA.ModuleB" => %{ 11 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 12 | {:test2, 0} => {:any, []}, 13 | {:test3, 1} => {:any, [:integer]}, 14 | {:test3, 2} => {:string, [:integer, :string]} 15 | }, 16 | "ModuleThree" => %{ 17 | {:test, 2} => {:string, [:integer, :string]} 18 | } 19 | }, 20 | :prefix => nil, 21 | :state => :ok, 22 | :error_data => %{}, 23 | :data => %{}, 24 | :vars => %{}, 25 | } 26 | 27 | setup do 28 | File.mkdir(@test_dir) 29 | 30 | on_exit fn -> 31 | File.rm_rf @test_dir 32 | end 33 | end 34 | 35 | test "returns empty when there is no module or code defined on the file" do 36 | File.write("test/tmp/example.ex", "") 37 | assert FunctionsExtractor.extract_functions_file("#{@test_dir}/example.ex", @env) === @env 38 | end 39 | 40 | test "returns the module name with the functions defined" do 41 | File.write("test/tmp/example.ex", " 42 | defmodule Example do 43 | end 44 | ") 45 | assert FunctionsExtractor.extract_functions_file("#{@test_dir}/example.ex", @env) 46 | === %{ 47 | error_data: %{}, 48 | functions: %{ 49 | "Example" => %{}, 50 | "ModuleA.ModuleB" => %{{:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, {:test2, 0} => {:any, []}, {:test3, 1} => {:any, [:integer]}, {:test3, 2} => {:string, [:integer, :string]}}, 51 | "ModuleThree" => %{{:test, 2} => {:string, [:integer, :string]}} 52 | }, 53 | prefix: nil, 54 | state: :ok, 55 | data: %{}, 56 | vars: %{} 57 | } 58 | 59 | File.write("test/tmp/example.ex", " 60 | defmodule Example do 61 | @spec example(integer, boolean) :: float 62 | end 63 | defmodule Example2 do 64 | @spec example(integer, integer) :: boolean 65 | end 66 | ") 67 | assert FunctionsExtractor.extract_functions_file("#{@test_dir}/example.ex", @env) 68 | === %{ 69 | error_data: %{}, 70 | functions: %{ 71 | "Example" => %{{:example, 2} => {:float, [:integer, :boolean]}}, 72 | "Example2" => %{{:example, 2} => {:boolean, [:integer, :integer]}}, 73 | "ModuleA.ModuleB" => %{{:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, {:test2, 0} => {:any, []}, {:test3, 1} => {:any, [:integer]}, {:test3, 2} => {:string, [:integer, :string]}}, 74 | "ModuleThree" => %{{:test, 2} => {:string, [:integer, :string]}} 75 | }, 76 | prefix: nil, 77 | state: :ok, 78 | data: %{}, 79 | vars: %{} 80 | } 81 | 82 | File.write("test/tmp/example.ex", " 83 | defmodule Example do 84 | @spec example(integer, boolean) :: float 85 | @spec example2() :: integer 86 | @spec example3(integer) :: any 87 | @spec example4([integer], {float, string}, %{none => float}) :: {float, string} 88 | @spec example5 :: integer 89 | end 90 | ") 91 | assert FunctionsExtractor.extract_functions_file("#{@test_dir}/example.ex", @env) 92 | === %{ 93 | error_data: %{}, 94 | functions: %{ 95 | "Example" => %{{:example, 2} => {:float, [:integer, :boolean]}, {:example2, 0} => {:integer, []}, {:example3, 1} => {:any, [:integer]}, {:example4, 3} => {{:tuple, [:float, :string]}, [list: :integer, tuple: [:float, :string], map: {:none, [:float]}]}, {:example5, 0} => {:integer, []}}, 96 | "ModuleA.ModuleB" => %{{:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, {:test2, 0} => {:any, []}, {:test3, 1} => {:any, [:integer]}, {:test3, 2} => {:string, [:integer, :string]}}, 97 | "ModuleThree" => %{{:test, 2} => {:string, [:integer, :string]}} 98 | }, 99 | prefix: nil, 100 | state: :ok, 101 | data: %{}, 102 | vars: %{} 103 | } 104 | end 105 | 106 | test "returns error when there are repeated function specifications" do 107 | File.write("test/tmp/example.ex", " 108 | defmodule Example do 109 | @spec example(integer) :: float 110 | @spec example(boolean) :: float 111 | end 112 | ") 113 | assert FunctionsExtractor.extract_functions_file("#{@test_dir}/example.ex", @env) 114 | === %{ 115 | prefix: nil, 116 | error_data: %{4 => "example/1 already has a defined type"}, 117 | functions: %{ 118 | "ModuleA.ModuleB" => %{{:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, {:test2, 0} => {:any, []}, {:test3, 1} => {:any, [:integer]}, {:test3, 2} => {:string, [:integer, :string]}}, 119 | "ModuleThree" => %{{:test, 2} => {:string, [:integer, :string]}}, 120 | "Example" => %{{:example, 1} => {:float, [:integer]}} 121 | }, 122 | state: :error, 123 | data: {4, "example/1 already has a defined type"}, 124 | vars: %{} 125 | } 126 | 127 | File.write("test/tmp/example.ex", " 128 | defmodule Example do 129 | @spec example(integer) :: float 130 | defmodule Example2 do 131 | @spec example(integer) :: float 132 | @spec example2(boolean) :: float 133 | @spec example2(atom) :: float 134 | end 135 | end 136 | ") 137 | assert FunctionsExtractor.extract_functions_file("#{@test_dir}/example.ex", @env) 138 | === %{ 139 | prefix: nil, 140 | error_data: %{7 => "example2/1 already has a defined type"}, 141 | functions: %{ 142 | "ModuleA.ModuleB" => %{{:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, {:test2, 0} => {:any, []}, {:test3, 1} => {:any, [:integer]}, {:test3, 2} => {:string, [:integer, :string]}}, 143 | "ModuleThree" => %{{:test, 2} => {:string, [:integer, :string]}}, 144 | "Example" => %{{:example, 1} => {:float, [:integer]}}, 145 | "Example.Example2" => %{{:example2, 1} => {:float, [:boolean]}, {:example, 1} => {:float, [:integer]}} 146 | }, 147 | state: :error, 148 | data: {7, "example2/1 already has a defined type"}, 149 | vars: %{} 150 | } 151 | end 152 | 153 | test "returns error when there are malformed type specs" do 154 | File.write("test/tmp/example.ex", " 155 | defmodule Example do 156 | @spec example(%{integer => atom, boolean => integer}) :: float 157 | end 158 | ") 159 | assert FunctionsExtractor.extract_functions_file("#{@test_dir}/example.ex", @env) 160 | === %{ 161 | prefix: nil, 162 | error_data: %{3 => "Malformed type spec on example/1 parameters"}, 163 | functions: %{ 164 | "ModuleA.ModuleB" => %{{:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, {:test2, 0} => {:any, []}, {:test3, 1} => {:any, [:integer]}, {:test3, 2} => {:string, [:integer, :string]}}, 165 | "ModuleThree" => %{{:test, 2} => {:string, [:integer, :string]}}, 166 | "Example" => %{} 167 | }, 168 | state: :error, 169 | data: {3, "Malformed type spec on example/1 parameters"}, 170 | vars: %{} 171 | } 172 | 173 | File.write("test/tmp/example.ex", " 174 | defmodule Example do 175 | @spec example(float) :: [integer, string] 176 | end 177 | ") 178 | assert FunctionsExtractor.extract_functions_file("#{@test_dir}/example.ex", @env) 179 | === %{ 180 | prefix: nil, 181 | error_data: %{3 => "Malformed type spec on example/1 return"}, 182 | functions: %{ 183 | "ModuleA.ModuleB" => %{{:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, {:test2, 0} => {:any, []}, {:test3, 1} => {:any, [:integer]}, {:test3, 2} => {:string, [:integer, :string]}}, 184 | "ModuleThree" => %{{:test, 2} => {:string, [:integer, :string]}}, 185 | "Example" => %{} 186 | }, 187 | state: :error, 188 | data: {3, "Malformed type spec on example/1 return"}, 189 | vars: %{} 190 | } 191 | end 192 | end 193 | end -------------------------------------------------------------------------------- /test/typelixir/pattern_builder_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Typelixir.PatternBuilderTest do 2 | use ExUnit.Case 3 | import Typelixir.PatternBuilder 4 | 5 | describe "type" do 6 | @env %{ 7 | state: :ok, 8 | type: nil, 9 | error_data: %{}, 10 | warnings: %{}, 11 | data: %{}, 12 | prefix: :Module, 13 | vars: %{ 14 | a: :integer, 15 | b: :string, 16 | }, 17 | modules_functions: %{} 18 | } 19 | 20 | test "returns simple types defined on @spec" do 21 | assert type({:string, nil, nil}, @env) === :string 22 | assert type({:boolean, nil, nil}, @env) === :boolean 23 | assert type({:integer, nil, nil}, @env) === :integer 24 | assert type({:float, nil, nil}, @env) === :float 25 | assert type({:atom, nil, nil}, @env) === :atom 26 | assert type({:any, nil, nil}, @env) === :any 27 | assert type({:none, nil, nil}, @env) === :none 28 | assert type({:asd, nil, nil}, @env) === :any 29 | end 30 | 31 | test "returns types of tuples without elements defined with @spec" do 32 | assert type({:{}, nil, []}, @env) === {:tuple, []} 33 | end 34 | 35 | test "returns types of one element tuples defined with @spec" do 36 | assert type({:{}, nil, [{:integer, nil, nil}]}, @env) === {:tuple, [:integer]} 37 | end 38 | 39 | test "returns types of two elements tuples defined with @spec" do 40 | assert type({{:string, nil, nil}, {:integer, nil, nil}}, @env) === {:tuple, [:string, :integer]} 41 | end 42 | 43 | test "returns types of tuples with more than two elements defined with @spec" do 44 | assert type({:{}, nil, [{:string, nil, nil}, {:integer, nil, nil}, {:float, nil, nil}]}, @env) === {:tuple, [:string, :integer, :float]} 45 | assert type({:{}, nil, [{:string, nil, nil}, {:integer, nil, nil}, {:float, nil, nil}, {:atom, nil, nil}]}, @env) === {:tuple, [:string, :integer, :float, :atom]} 46 | end 47 | 48 | test "returns list types defined with @spec" do 49 | assert type([], @env) === {:list, :any} 50 | assert type([{:integer, nil, nil}], @env) === {:list, :integer} 51 | assert type([{:integer, nil, nil}, {:float, nil, nil}], @env) === {:list, :float} 52 | assert type([{:any, nil, nil}], @env) === {:list, :any} 53 | assert type([{:integer, nil, nil}, {:string, nil, nil}], @env) === {:list, :error} 54 | end 55 | 56 | test "returns map types defined with @spec" do 57 | assert type({:%{}, nil, []}, @env) === {:map, {:any, :any}} 58 | assert type({:%{}, nil, [{{:integer, nil, nil}, {:float, nil, nil}}]}, @env) === {:map, {:integer, [:float]}} 59 | assert type({:%{}, nil, [{{:integer, nil, nil}, {:float, nil, nil}}, {{:float, nil, nil}, {:float, nil, nil}}]}, @env) === {:map, {:float, [:float, :float]}} 60 | assert type({:%{}, nil, [{{:integer, nil, nil}, {:float, nil, nil}}, {{:string, nil, nil}, {:float, nil, nil}}]}, @env) === {:map, {:error, [:float, :float]}} 61 | assert type({:%{}, nil, [{{:integer, nil, nil}, {:integer, nil, nil}}, {{:integer, nil, nil}, {:float, nil, nil}}]}, @env) === {:map, {:integer, [:integer, :float]}} 62 | assert type({:%{}, nil, [{{:none, nil, nil}, {:any, nil, nil}}]}, @env) === {:map, {:none, [:any]}} 63 | end 64 | 65 | test "returns simple types" do 66 | assert type("hola", @env) === :string 67 | assert type(true, @env) === :boolean 68 | assert type(false, @env) === :boolean 69 | assert type(1, @env) === :integer 70 | assert type(1.1, @env) === :float 71 | assert type(:atom, @env) === :atom 72 | end 73 | 74 | test "returns tuple types" do 75 | assert type({:{}, nil, []}, @env) === {:tuple, []} 76 | assert type({:{}, nil, [1]}, @env) === {:tuple, [:integer]} 77 | assert type({1,2}, @env) === {:tuple, [:integer, :integer]} 78 | assert type({:{}, nil, [1, 2, 3]}, @env) === {:tuple, [:integer, :integer, :integer]} 79 | end 80 | 81 | test "returns list types" do 82 | assert type([], @env) === {:list, :any} 83 | assert type([1], @env) === {:list, :integer} 84 | assert type([1, 1.2], @env) === {:list, :float} 85 | assert type([1 | [1.2]], @env) === {:list, :float} 86 | assert type([1, "string"], @env) === {:list, :error} 87 | end 88 | 89 | test "returns map types" do 90 | assert type({:%{}, nil, []}, @env) === {:map, {:any, :any}} 91 | assert type({:%{}, nil, [a: 1, b: 1.2]}, @env) === {:map, {:atom, [:integer, :float]}} 92 | assert type({:%{}, nil, [{1, 1}, {2, 2.1}]}, @env) === {:map, {:integer, [:integer, :float]}} 93 | assert type({:%{}, nil, [{1, 1.1}, {1, 2}]}, @env) === {:map, {:integer, [:float, :integer]}} 94 | assert type({:%{}, nil, [{1, 1}, {2.1, 2.1}]}, @env) === {:map, {:float, [:integer, :float]}} 95 | assert type({:%{}, nil, [{1, 1}, {"string", 2.1}]}, @env) === {:map, {:error, [:integer, :float]}} 96 | end 97 | 98 | test "returns variable types" do 99 | assert type({:a, nil, []}, @env) === :integer 100 | assert type({:b, nil, []}, @env) === :string 101 | assert type({:k, nil, []}, @env) === :any 102 | assert type({:{}, nil, [{:a, nil, nil}]}, @env) === {:tuple, [:integer]} 103 | assert type([{:a, nil, []}], @env) === {:list, :integer} 104 | assert type({:%{}, nil, [{{:a, nil, []}, {:b, nil, []}}]}, @env) === {:map, {:integer, [:string]}} 105 | end 106 | 107 | test "returns wild types" do 108 | assert type({:_, nil, nil}, @env) === :any 109 | assert type({:{}, nil, [{:_, nil, nil}]}, @env) === {:tuple, [:any]} 110 | assert type([{:_, nil, nil}], @env) === {:list, :any} 111 | assert type([1, {:_, nil, nil}], @env) === {:list, :integer} 112 | assert type({:%{}, nil, [{{:_, nil, nil}, {:_, nil, nil}}]}, @env) === {:map, {:any, [:any]}} 113 | assert type({:%{}, nil, [{1, 1}, {{:_, nil, nil}, {:_, nil, nil}}]}, @env) === {:map, {:integer, [:integer, :any]}} 114 | end 115 | 116 | test "returns binding types" do 117 | assert type({:=, nil, [true, 1]}, @env) === :error 118 | assert type({:=, nil, [1, 1.2]}, @env) === :float 119 | end 120 | end 121 | 122 | describe "vars" do 123 | test "does not return variables when param list is empty" do 124 | assert vars([], []) === %{} 125 | end 126 | 127 | test "does not return variables when param list has only simple values" do 128 | assert vars([1, 1.2, true, :un_atom, "un string"], [:integer, :float, :boolean, :atom, :string]) === %{} 129 | assert vars([1], [:float]) === %{} 130 | assert vars([1], [:any]) === %{} 131 | assert vars([1], [:string]) === {:error, "Parameters does not match type specification"} 132 | end 133 | 134 | test "return variables from simple variable patterns" do 135 | assert vars([{:a, nil, nil}, {:b, nil, nil}, {:c, nil, nil}, {:d, nil, nil}, {:e, nil, nil}], [:integer, :float, :boolean, :atom, :string]) === %{a: :integer, b: :float, c: :boolean, d: :atom, e: :string} 136 | assert vars([{:a, nil, nil}, {:a, nil, nil}], [:integer, :float]) === {:error, "Variable a is already defined with type integer"} 137 | assert vars([{:a, nil, nil}], [:any]) === %{} 138 | end 139 | 140 | test "returns variables of composed types" do 141 | assert vars([{:a, nil, nil}], [{:tuple, [:integer]}]) === %{a: {:tuple, [:integer]}} 142 | assert vars([{:a, nil, nil}], [{:map, {:integer,[:integer]}}]) === %{a: {:map, {:integer,[:integer]}}} 143 | assert vars([{:a, nil, nil}], [{:list, :integer}]) === %{a: {:list, :integer}} 144 | end 145 | 146 | test "return variables from wild patterns" do 147 | assert vars([{:_, nil, nil}], [:integer]) === %{} 148 | end 149 | 150 | test "return variables from list patterns" do 151 | assert vars([[]], [{:list, :any}]) === %{} 152 | assert vars([[1,2,3]], [{:list, :integer}]) === %{} 153 | assert vars([[{:a, nil, nil}]], [{:list, :integer}]) === %{a: :integer} 154 | assert vars([[{:a, nil, nil}, {:b, nil, nil}]], [{:list, :integer}]) === %{a: :integer, b: :integer} 155 | assert vars([[{:a, nil, nil} | [{:b, nil, nil}]]], [{:list, :integer}]) === %{a: :integer, b: :integer} 156 | end 157 | 158 | test "return variables from map patterns" do 159 | assert vars([{:%{}, nil, []}], [{:map, {:any,[]}}]) === %{} 160 | assert vars([{:%{}, nil, [{1, {:a, nil, nil}}, {2, {:b, nil, nil}}]}], [{:map, {:integer, [:integer, :float]}}]) === %{a: :integer, b: :float} 161 | assert vars([{:%{}, nil, [{1, {:a, nil, nil}}, {2, {:b, nil, nil}}, {3, {:c, nil, nil}}]}], [{:map, {:integer, [:integer, :float]}}]) === %{a: :integer, b: :float} 162 | assert vars([{:%{}, nil, [{1, {:a, nil, nil}}, {2, {:b, nil, nil}}]}], [{:map, {:integer, [:integer, :float, :integer]}}]) === %{a: :integer, b: :float} 163 | end 164 | 165 | test "return variables from tuple patterns" do 166 | assert vars([{:{}, nil, []}], [{:tuple, []}]) === %{} 167 | assert vars([{:{}, nil, [{:a, nil, nil}]}], [{:tuple, [:integer]}]) === %{a: :integer} 168 | assert vars([{{:a, [line: 6], nil}, {:b, [line: 6], nil}}], [{:tuple, [:integer, :float]}]) === %{a: :integer, b: :float} 169 | assert vars([{:{}, nil, [{:a, nil, nil}, {:b, nil, nil}]}], [{:tuple, [:integer, :float, :string]}]) === {:error, "The number of parameters in tuple does not match the number of types"} 170 | assert vars([{:{}, nil, [{:a, nil, nil}, {:b, nil, nil}, {:c, nil, nil}]}], [{:tuple, [:integer, :float]}]) === {:error, "The number of parameters in tuple does not match the number of types"} 171 | end 172 | 173 | test "returns error when type and the pattern does not match" do 174 | assert vars([{:{}, nil, []}], [:integer]) === {:error, "Parameters does not match type specification"} 175 | assert vars([{:{}, nil, []}], [{:map, {:any,[]}}]) === {:error, "Parameters does not match type specification"} 176 | assert vars([{:{}, nil, []}], [{:list, :integer}]) === {:error, "Parameters does not match type specification"} 177 | 178 | assert vars([[]], [:integer]) === {:error, "Parameters does not match type specification"} 179 | assert vars([[]], [{:map, {:any,[]}}]) === {:error, "Parameters does not match type specification"} 180 | assert vars([[]], [{:tuple, []}]) === {:error, "Parameters does not match type specification"} 181 | 182 | assert vars([{:%{}, nil, []}], [:integer]) === {:error, "Parameters does not match type specification"} 183 | assert vars([{:%{}, nil, []}], [{:tuple, []}]) === {:error, "Parameters does not match type specification"} 184 | assert vars([{:%{}, nil, []}], [{:list, :integer}]) === {:error, "Parameters does not match type specification"} 185 | end 186 | end 187 | end -------------------------------------------------------------------------------- /test/typelixir/processor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Typelixir.ProcessorTest do 2 | use ExUnit.Case 3 | alias Typelixir.Processor 4 | 5 | describe "process_file" do 6 | @test_dir "test/tmp" 7 | 8 | @env %{ 9 | :functions => %{ 10 | "ModuleA.ModuleB" => %{ 11 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 12 | {:test2, 0} => {:any, []} 13 | }, 14 | "ModuleC" => %{ 15 | {:test, 2} => {:string, [:integer, :string]} 16 | }, 17 | "Example" => %{} 18 | }, 19 | :prefix => nil, 20 | :type => nil, 21 | :state => :ok, 22 | :error_data => %{}, 23 | :data => %{}, 24 | :vars => %{} 25 | } 26 | 27 | setup do 28 | File.mkdir(@test_dir) 29 | 30 | on_exit fn -> 31 | File.rm_rf @test_dir 32 | end 33 | end 34 | # NOTE: we don't care about the type the module returns 35 | 36 | test "modules definition" do 37 | File.write("test/tmp/example.ex", "") 38 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 39 | === %{ 40 | :functions => %{ 41 | "ModuleA.ModuleB" => %{ 42 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 43 | {:test2, 0} => {:any, []} 44 | }, 45 | "ModuleC" => %{ 46 | {:test, 2} => {:string, [:integer, :string]} 47 | }, 48 | "Example" => %{} 49 | }, 50 | :prefix => nil, 51 | :type => :atom, 52 | :state => :ok, 53 | :error_data => %{}, 54 | :data => %{}, 55 | :vars => %{} 56 | } 57 | 58 | File.write("test/tmp/example.ex", " 59 | defmodule Example do 60 | end 61 | ") 62 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 63 | === %{ 64 | :functions => %{ 65 | "ModuleA.ModuleB" => %{ 66 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 67 | {:test2, 0} => {:any, []} 68 | }, 69 | "ModuleC" => %{ 70 | {:test, 2} => {:string, [:integer, :string]} 71 | }, 72 | "Example" => %{} 73 | }, 74 | :prefix => "Example", 75 | :type => {:list, {:tuple, [:atom, :any]}}, 76 | :state => :ok, 77 | :error_data => %{}, 78 | :data => %{}, 79 | :vars => %{} 80 | } 81 | end 82 | 83 | test "import and alias" do 84 | File.write("test/tmp/example.ex", " 85 | defmodule Example do 86 | import ModuleC 87 | alias ModuleA.ModuleB 88 | alias ModuleD, as: D 89 | 90 | import UnknownModuleA 91 | alias UnknownModuleB 92 | end 93 | ") 94 | assert Processor.process_file("#{@test_dir}/example.ex", %{@env | functions: Map.put(@env[:functions], "ModuleD", %{{:test, 1} => {:integer, [:integer]}})}) 95 | === %{ 96 | :functions => %{ 97 | "ModuleA.ModuleB" => %{ 98 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 99 | {:test2, 0} => {:any, []} 100 | }, 101 | "ModuleC" => %{ 102 | {:test, 2} => {:string, [:integer, :string]} 103 | }, 104 | "Example" => %{ 105 | {:test, 2} => {:string, [:integer, :string]} 106 | }, 107 | "ModuleB" => %{ 108 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 109 | {:test2, 0} => {:any, []} 110 | }, 111 | "ModuleD" => %{ 112 | {:test, 1} => {:integer, [:integer]} 113 | }, 114 | "D" => %{ 115 | {:test, 1} => {:integer, [:integer]} 116 | }, 117 | }, 118 | :prefix => "Example", 119 | :type => {:list, {:tuple, [:atom, :any]}}, 120 | :state => :ok, 121 | :error_data => %{}, 122 | :data => %{}, 123 | :vars => %{} 124 | } 125 | end 126 | 127 | test "functions def" do 128 | # body 129 | File.write("test/tmp/example.ex", " 130 | defmodule Example do 131 | @spec test(string) :: integer 132 | defp test(x) do 133 | 10 134 | end 135 | 136 | def test(x) do 137 | length([]) 138 | end 139 | 140 | def test(x) do 141 | \"not pass\" 142 | end 143 | end 144 | ") 145 | 146 | assert Processor.process_file("#{@test_dir}/example.ex", %{@env | functions: Map.put(@env[:functions], "Example", %{{:test, 1} => {:integer, [:string]}})}) 147 | === %{ 148 | :functions => %{ 149 | "ModuleA.ModuleB" => %{ 150 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 151 | {:test2, 0} => {:any, []} 152 | }, 153 | "ModuleC" => %{ 154 | {:test, 2} => {:string, [:integer, :string]} 155 | }, 156 | "Example" => %{ 157 | {:test, 1} => {:integer, [:string]} 158 | } 159 | }, 160 | :prefix => "Example", 161 | :type => {:list, :any}, 162 | :state => :error, 163 | :error_data => %{12 => "Body doesn't match function type on test/1 declaration"}, 164 | :data => {12, "Body doesn't match function type on test/1 declaration"}, 165 | :vars => %{x: :string} 166 | } 167 | 168 | # params 169 | File.write("test/tmp/example.ex", " 170 | defmodule Example do 171 | @spec test(integer, string) :: integer 172 | def test([x], y), do: 10 173 | defp test(x, x), do: 11 174 | 175 | @spec test2({integer, boolean}) :: integer 176 | def test2([1, 2]), do: 10 177 | def test2({1, true, 3}), do: 11 178 | end 179 | ") 180 | 181 | assert Processor.process_file("#{@test_dir}/example.ex", %{@env | functions: Map.put(@env[:functions], "Example", %{ 182 | {:test, 2} => {:integer, [:integer, :string]}, 183 | {:test2, 1} => {:integer, [{:tuple, [:integer, :boolean]}]} 184 | })}) 185 | === %{ 186 | :functions => %{ 187 | "ModuleA.ModuleB" => %{ 188 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 189 | {:test2, 0} => {:any, []} 190 | }, 191 | "ModuleC" => %{ 192 | {:test, 2} => {:string, [:integer, :string]} 193 | }, 194 | "Example" => %{ 195 | {:test, 2} => {:integer, [:integer, :string]}, 196 | {:test2, 1} => {:integer, [{:tuple, [:integer, :boolean]}]} 197 | } 198 | }, 199 | :prefix => "Example", 200 | :type => {:list, :any}, 201 | :state => :error, 202 | :error_data => %{ 203 | 4 => "Parameters does not match type specification", 204 | 5 => "Variable x is already defined with type integer", 205 | 8 => "Parameters does not match type specification", 206 | 9 => "The number of parameters in tuple does not match the number of types" 207 | }, 208 | :data => {4, "Parameters does not match type specification"}, 209 | :vars => %{} 210 | } 211 | 212 | # empty params 213 | File.write("test/tmp/example.ex", " 214 | defmodule Example do 215 | @spec test(integer) :: integer 216 | defp test(x), do: 11 217 | 218 | @spec test2 :: integer 219 | def test2, do: 11 220 | end 221 | ") 222 | 223 | assert Processor.process_file("#{@test_dir}/example.ex", %{@env | functions: Map.put(@env[:functions], "Example", %{ 224 | {:test, 1} => {:integer, [:integer]}, 225 | {:test2, 0} => {:any, [:integer]} 226 | })}) 227 | === %{ 228 | :functions => %{ 229 | "Example" => %{{:test, 1} => {:integer, [:integer]}, {:test2, 0} => {:any, [:integer]}}, 230 | "ModuleA.ModuleB" => %{{:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, {:test2, 0} => {:any, []}}, 231 | "ModuleC" => %{{:test, 2} => {:string, [:integer, :string]}} 232 | }, 233 | :prefix => "Example", 234 | :type => {:list, {:tuple, [:atom, :any]}}, 235 | :state => :ok, 236 | :error_data => %{}, 237 | :data => %{}, 238 | :vars => %{} 239 | } 240 | end 241 | 242 | test "functions call" do 243 | File.write("test/tmp/example.ex", " 244 | defmodule Example do 245 | @spec test(integer) :: string 246 | def test(x), do: UnknownModule.test(x) 247 | def test(x), do: ModuleC.test(x, \"a\") 248 | def test(x), do: ModuleC.test(true, \"a\") 249 | def test(x), do: ModuleC.test(x, {1, 2}) 250 | 251 | def test2(x, y), do: test(1) 252 | def test2(x, y), do: test(true) 253 | def test2(x, y), do: test([1, 2]) 254 | end 255 | ") 256 | assert Processor.process_file("#{@test_dir}/example.ex", %{@env | functions: Map.put(@env[:functions], "Example", %{{:test, 1} => {:string, [:integer]}})}) 257 | === %{ 258 | :functions => %{ 259 | "ModuleA.ModuleB" => %{ 260 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 261 | {:test2, 0} => {:any, []} 262 | }, 263 | "ModuleC" => %{ 264 | {:test, 2} => {:string, [:integer, :string]} 265 | }, 266 | "Example" => %{ 267 | {:test, 1} => {:string, [:integer]} 268 | } 269 | }, 270 | :prefix => "Example", 271 | :type => {:list, :any}, 272 | :state => :error, 273 | :error_data => %{ 274 | 6 => "Arguments does not match type specification on test/2", 275 | 7 => "Arguments does not match type specification on test/2", 276 | 10 => "Arguments does not match type specification on test/1", 277 | 11 => "Arguments does not match type specification on test/1" 278 | }, 279 | :data => {6, "Arguments does not match type specification on test/2"}, 280 | :vars => %{} 281 | } 282 | end 283 | 284 | test "binding" do 285 | File.write("test/tmp/example.ex", " 286 | defmodule Example do 287 | a = 1 288 | b = UnknownModule.length(2) 289 | 2 = 2 290 | {y, z} = {[1, 2.4], false} 291 | [head | tail] = [2, 4.2] 292 | c = a + 3.2 293 | x = a 294 | a = true 295 | end 296 | ") 297 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 298 | === %{ 299 | :functions => %{ 300 | "ModuleA.ModuleB" => %{ 301 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 302 | {:test2, 0} => {:any, []} 303 | }, 304 | "ModuleC" => %{ 305 | {:test, 2} => {:string, [:integer, :string]} 306 | }, 307 | "Example" => %{ 308 | } 309 | }, 310 | :prefix => "Example", 311 | :type => {:list, {:tuple, [:atom, :any]}}, 312 | :state => :ok, 313 | :error_data => %{}, 314 | :data => %{}, 315 | :vars => %{a: :boolean, c: :float, x: :integer, z: :boolean, y: {:list, :float}, head: :float, tail: {:list, :float}} 316 | } 317 | end 318 | 319 | test "number operators" do 320 | File.write("test/tmp/example.ex", " 321 | defmodule Example do 322 | def test() do 323 | a = 1 + 2 324 | b = 2 + 3.4 325 | c = 1 / 2 326 | d = length([]) + 2 327 | e = 4 + \"5\" 328 | end 329 | end 330 | ") 331 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 332 | === %{ 333 | :functions => %{ 334 | "ModuleA.ModuleB" => %{ 335 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 336 | {:test2, 0} => {:any, []} 337 | }, 338 | "ModuleC" => %{ 339 | {:test, 2} => {:string, [:integer, :string]} 340 | }, 341 | "Example" => %{ 342 | } 343 | }, 344 | :prefix => "Example", 345 | data: {8, "Type error on + operator"}, 346 | error_data: %{8 => "Type error on + operator"}, 347 | state: :error, 348 | type: {:list, :any}, 349 | vars: %{c: :float, a: :integer, b: :float, d: :integer} 350 | } 351 | 352 | # neg 353 | File.write("test/tmp/example.ex", " 354 | defmodule Example do 355 | def test() do 356 | f = -1 357 | g = -1.2 358 | h = -\"3\" 359 | end 360 | end 361 | ") 362 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 363 | === %{ 364 | :functions => %{ 365 | "ModuleA.ModuleB" => %{ 366 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 367 | {:test2, 0} => {:any, []} 368 | }, 369 | "ModuleC" => %{ 370 | {:test, 2} => {:string, [:integer, :string]} 371 | }, 372 | "Example" => %{ 373 | } 374 | }, 375 | :prefix => "Example", 376 | data: {6, "Type error on - operator"}, 377 | error_data: %{6 => "Type error on - operator"}, 378 | state: :error, 379 | type: {:list, :any}, 380 | vars: %{f: :integer, g: :float} 381 | } 382 | end 383 | 384 | test "boolean operators" do 385 | File.write("test/tmp/example.ex", " 386 | defmodule Example do 387 | def test() do 388 | a = true and false 389 | b = 1 > 2 and is_list([]) 390 | e = 4 and true 391 | end 392 | end 393 | ") 394 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 395 | === %{ 396 | :functions => %{ 397 | "ModuleA.ModuleB" => %{ 398 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 399 | {:test2, 0} => {:any, []} 400 | }, 401 | "ModuleC" => %{ 402 | {:test, 2} => {:string, [:integer, :string]} 403 | }, 404 | "Example" => %{ 405 | } 406 | }, 407 | :prefix => "Example", 408 | data: {6, "Type error on and operator"}, 409 | error_data: %{6 => "Type error on and operator"}, 410 | state: :error, 411 | type: {:list, :any}, 412 | vars: %{a: :boolean, b: :boolean} 413 | } 414 | 415 | # not 416 | File.write("test/tmp/example.ex", " 417 | defmodule Example do 418 | def test() do 419 | f = not true 420 | g = not is_atom(:b) 421 | h = not 10 422 | end 423 | end 424 | ") 425 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 426 | === %{ 427 | :functions => %{ 428 | "ModuleA.ModuleB" => %{ 429 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 430 | {:test2, 0} => {:any, []} 431 | }, 432 | "ModuleC" => %{ 433 | {:test, 2} => {:string, [:integer, :string]} 434 | }, 435 | "Example" => %{ 436 | } 437 | }, 438 | :prefix => "Example", 439 | data: {6, "Type error on not operator"}, 440 | error_data: %{6 => "Type error on not operator"}, 441 | state: :error, 442 | type: {:list, :any}, 443 | vars: %{f: :boolean, g: :boolean} 444 | } 445 | end 446 | 447 | test "comparison operators" do 448 | File.write("test/tmp/example.ex", " 449 | defmodule Example do 450 | def test() do 451 | a = 1 > 2 452 | b = 3 > true 453 | e = \"a\" === :a 454 | end 455 | end 456 | ") 457 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 458 | === %{ 459 | :functions => %{ 460 | "ModuleA.ModuleB" => %{ 461 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 462 | {:test2, 0} => {:any, []} 463 | }, 464 | "ModuleC" => %{ 465 | {:test, 2} => {:string, [:integer, :string]} 466 | }, 467 | "Example" => %{ 468 | } 469 | }, 470 | :prefix => "Example", 471 | data: %{}, 472 | error_data: %{}, 473 | state: :ok, 474 | type: {:list, {:tuple, [:atom, :any]}}, 475 | vars: %{a: :boolean, b: :boolean, e: :boolean} 476 | } 477 | end 478 | 479 | test "list operators" do 480 | File.write("test/tmp/example.ex", " 481 | defmodule Example do 482 | def test() do 483 | a = [1] ++ [2] 484 | b = [1] ++ [2.5] 485 | c = UnknownModule.list(1, 2) -- [2, 5] 486 | d = 1 -- [2, 5] 487 | end 488 | end 489 | ") 490 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 491 | === %{ 492 | :functions => %{ 493 | "ModuleA.ModuleB" => %{ 494 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 495 | {:test2, 0} => {:any, []} 496 | }, 497 | "ModuleC" => %{ 498 | {:test, 2} => {:string, [:integer, :string]} 499 | }, 500 | "Example" => %{ 501 | } 502 | }, 503 | :prefix => "Example", 504 | data: {7, "Type error on -- operator"}, 505 | error_data: %{7 => "Type error on -- operator"}, 506 | state: :error, 507 | type: {:list, :any}, 508 | vars: %{a: {:list, :integer}, b: {:list, :float}, c: {:list, :integer}} 509 | } 510 | end 511 | 512 | test "string operators" do 513 | File.write("test/tmp/example.ex", " 514 | defmodule Example do 515 | def test() do 516 | a = \"a\" <> \"b\" 517 | c = UnknownModule.to_string(1) <> \"b\" 518 | d = \"1\" <> 10 519 | end 520 | end 521 | ") 522 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 523 | === %{ 524 | :functions => %{ 525 | "ModuleA.ModuleB" => %{ 526 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 527 | {:test2, 0} => {:any, []} 528 | }, 529 | "ModuleC" => %{ 530 | {:test, 2} => {:string, [:integer, :string]} 531 | }, 532 | "Example" => %{ 533 | } 534 | }, 535 | :prefix => "Example", 536 | data: {6, "Type error on <> operator"}, 537 | error_data: %{6 => "Type error on <> operator"}, 538 | state: :error, 539 | type: {:list, :any}, 540 | vars: %{a: :string, c: :string} 541 | } 542 | end 543 | 544 | test "if/unless" do 545 | # branches 546 | File.write("test/tmp/example.ex", " 547 | defmodule Example do 548 | def test() do 549 | if (true), do: 10 550 | if (true), do: 10, else: 10 551 | 552 | if (false) do 553 | true 554 | else 555 | 10 556 | end 557 | end 558 | end 559 | ") 560 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 561 | === %{ 562 | :functions => %{ 563 | "ModuleA.ModuleB" => %{ 564 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 565 | {:test2, 0} => {:any, []} 566 | }, 567 | "ModuleC" => %{ 568 | {:test, 2} => {:string, [:integer, :string]} 569 | }, 570 | "Example" => %{ 571 | } 572 | }, 573 | :prefix => "Example", 574 | data: {7, "Type error on if branches"}, 575 | error_data: %{7 => "Type error on if branches"}, 576 | state: :error, 577 | type: {:list, :any}, 578 | vars: %{} 579 | } 580 | 581 | # condition 582 | File.write("test/tmp/example.ex", " 583 | defmodule Example do 584 | def test() do 585 | unless (14), do: \"fail\" 586 | end 587 | end 588 | ") 589 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 590 | === %{ 591 | :functions => %{ 592 | "ModuleA.ModuleB" => %{ 593 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 594 | {:test2, 0} => {:any, []} 595 | }, 596 | "ModuleC" => %{ 597 | {:test, 2} => {:string, [:integer, :string]} 598 | }, 599 | "Example" => %{ 600 | } 601 | }, 602 | :prefix => "Example", 603 | data: {4, "Type error on unless condition"}, 604 | error_data: %{4 => "Type error on unless condition"}, 605 | state: :error, 606 | type: {:list, :any}, 607 | vars: %{} 608 | } 609 | end 610 | 611 | test "cond" do 612 | # branches 613 | File.write("test/tmp/example.ex", " 614 | defmodule Example do 615 | def test() do 616 | cond do 617 | 1 > 2 -> 10 618 | true -> 14 619 | end 620 | 621 | cond do 622 | 1 > 2 -> 10 623 | false -> 100 624 | true -> \"fail\" 625 | end 626 | end 627 | end 628 | ") 629 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 630 | === %{ 631 | :functions => %{ 632 | "ModuleA.ModuleB" => %{ 633 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 634 | {:test2, 0} => {:any, []} 635 | }, 636 | "ModuleC" => %{ 637 | {:test, 2} => {:string, [:integer, :string]} 638 | }, 639 | "Example" => %{ 640 | } 641 | }, 642 | :prefix => "Example", 643 | data: {12, "Type error on cond branches"}, 644 | error_data: %{12 => "Type error on cond branches"}, 645 | state: :error, 646 | type: {:list, :any}, 647 | vars: %{} 648 | } 649 | 650 | # condition 651 | File.write("test/tmp/example.ex", " 652 | defmodule Example do 653 | def test() do 654 | cond do 655 | 1 > 2 -> 40 656 | 1123 -> 14 657 | true -> 15 658 | end 659 | end 660 | end 661 | ") 662 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 663 | === %{ 664 | :functions => %{ 665 | "ModuleA.ModuleB" => %{ 666 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 667 | {:test2, 0} => {:any, []} 668 | }, 669 | "ModuleC" => %{ 670 | {:test, 2} => {:string, [:integer, :string]} 671 | }, 672 | "Example" => %{ 673 | } 674 | }, 675 | :prefix => "Example", 676 | data: {6, "Type error on cond condition"}, 677 | error_data: %{6 => "Type error on cond condition"}, 678 | state: :error, 679 | type: {:list, :any}, 680 | vars: %{} 681 | } 682 | end 683 | 684 | test "case" do 685 | File.write("test/tmp/example.ex", " 686 | defmodule Example do 687 | def test(x) do 688 | case x do 689 | {x, y} -> 10 690 | true -> 14 691 | [x, y] -> x + 10 692 | end 693 | 694 | case x do 695 | {x, y} -> 10 696 | true -> 14 697 | [x, y] -> \"fail\" 698 | end 699 | end 700 | end 701 | ") 702 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 703 | === %{ 704 | :functions => %{ 705 | "ModuleA.ModuleB" => %{ 706 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 707 | {:test2, 0} => {:any, []} 708 | }, 709 | "ModuleC" => %{ 710 | {:test, 2} => {:string, [:integer, :string]} 711 | }, 712 | "Example" => %{ 713 | } 714 | }, 715 | :prefix => "Example", 716 | data: {13, "Type error on case branches"}, 717 | error_data: %{13 => "Type error on case branches"}, 718 | state: :error, 719 | type: {:list, :any}, 720 | vars: %{} 721 | } 722 | end 723 | 724 | test "map application" do 725 | File.write("test/tmp/example.ex", " 726 | defmodule Example do 727 | def test(x) do 728 | a = %{1 => 2, 3 => 4} 729 | b = %{a: :value1, b: :value2} 730 | 731 | c = a[1] 732 | d = b.a 733 | e = a[2] + 4 734 | 735 | f = a[:key] 736 | end 737 | end 738 | ") 739 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 740 | === %{ 741 | :functions => %{ 742 | "ModuleA.ModuleB" => %{ 743 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 744 | {:test2, 0} => {:any, []} 745 | }, 746 | "ModuleC" => %{ 747 | {:test, 2} => {:string, [:integer, :string]} 748 | }, 749 | "Example" => %{ 750 | } 751 | }, 752 | :prefix => "Example", 753 | data: {11, "Expected integer as key instead of atom"}, 754 | error_data: %{11 => "Expected integer as key instead of atom"}, 755 | state: :error, 756 | type: {:list, :any}, 757 | vars: %{a: {:map, {:integer, [:integer, :integer]}}, b: {:map, {:atom, [:atom, :atom]}}, e: :integer} 758 | } 759 | 760 | File.write("test/tmp/example.ex", " 761 | defmodule Example do 762 | def test() do 763 | a = %{1 => 2, 3 => 4} 764 | b = c[2] 765 | end 766 | end 767 | ") 768 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 769 | === %{ 770 | :functions => %{ 771 | "ModuleA.ModuleB" => %{ 772 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 773 | {:test2, 0} => {:any, []} 774 | }, 775 | "ModuleC" => %{ 776 | {:test, 2} => {:string, [:integer, :string]} 777 | }, 778 | "Example" => %{ 779 | } 780 | }, 781 | :prefix => "Example", 782 | data: {5, "Not accessing to a map"}, 783 | error_data: %{5 => "Not accessing to a map"}, 784 | state: :error, 785 | type: {:list, :any}, 786 | vars: %{a: {:map, {:integer, [:integer, :integer]}}} 787 | } 788 | end 789 | 790 | test "malformed types" do 791 | # list 792 | File.write("test/tmp/example.ex", " 793 | defmodule Example do 794 | def test(x) do 795 | a = [1, 2] 796 | b = [1, length([])] 797 | c = [1, 40.0] 798 | d = [] 799 | e = [1, 2, :a] 800 | end 801 | end 802 | ") 803 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 804 | === %{ 805 | :functions => %{ 806 | "ModuleA.ModuleB" => %{ 807 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 808 | {:test2, 0} => {:any, []} 809 | }, 810 | "ModuleC" => %{ 811 | {:test, 2} => {:string, [:integer, :string]} 812 | }, 813 | "Example" => %{ 814 | } 815 | }, 816 | :prefix => "Example", 817 | data: {"", "Malformed type list"}, 818 | error_data: %{"" => "Malformed type list"}, 819 | state: :error, 820 | type: {:list, :any}, 821 | vars: %{a: {:list, :integer}, b: {:list, :integer}, c: {:list, :float}, d: {:list, :any}} 822 | } 823 | 824 | # map 825 | File.write("test/tmp/example.ex", " 826 | defmodule Example do 827 | def test(x) do 828 | a = %{} 829 | b = %{1 => \"a\"} 830 | c = %{40 => :value, 47.5 => :o_value, 30 => length{[1]}} 831 | d = %{1 => 2, \"2\" => 3} 832 | end 833 | end 834 | ") 835 | assert Processor.process_file("#{@test_dir}/example.ex", @env) 836 | === %{ 837 | :functions => %{ 838 | "ModuleA.ModuleB" => %{ 839 | {:test, 2} => {{:tuple, [{:list, :integer}, :string]}, [{:list, :integer}, :string]}, 840 | {:test2, 0} => {:any, []} 841 | }, 842 | "ModuleC" => %{ 843 | {:test, 2} => {:string, [:integer, :string]} 844 | }, 845 | "Example" => %{ 846 | } 847 | }, 848 | :prefix => "Example", 849 | data: {7, "Malformed type map"}, 850 | error_data: %{7 => "Malformed type map"}, 851 | state: :error, 852 | type: {:list, :any}, 853 | vars: %{a: {:map, {:any, :any}}, b: {:map, {:integer, [:string]}}, c: {:map, {:float, [:atom, :atom, :any]}}} 854 | } 855 | end 856 | end 857 | end -------------------------------------------------------------------------------- /test/typelixir/type_comparator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Typelixir.TypeComparatorTest do 2 | use ExUnit.Case 3 | alias Typelixir.TypeComparator 4 | 5 | describe "supremum" do 6 | test "returns the type when arguments are equal" do 7 | assert TypeComparator.supremum(:string, :string) === :string 8 | assert TypeComparator.supremum(:boolean, :boolean) === :boolean 9 | assert TypeComparator.supremum(:integer, :integer) === :integer 10 | assert TypeComparator.supremum(:float, :float) === :float 11 | assert TypeComparator.supremum(:atom, :atom) === :atom 12 | end 13 | 14 | test "returns error when types are not comparable" do 15 | assert TypeComparator.supremum(:string, :boolean) === :error 16 | assert TypeComparator.supremum(:boolean, :integer) === :error 17 | assert TypeComparator.supremum(:integer, :atom) === :error 18 | assert TypeComparator.supremum({:tuple, [:integer]}, :float) === :error 19 | assert TypeComparator.supremum({:list, :string}, {:tuple, [:integer]}) === :error 20 | end 21 | 22 | test "returns error when one of the type is already an error" do 23 | assert TypeComparator.supremum(:error, :boolean) === :error 24 | assert TypeComparator.supremum(:error, :none) === :error 25 | assert TypeComparator.supremum(:none, :error) === :error 26 | assert TypeComparator.supremum({:tuple, [:integer]}, :error) === :error 27 | assert TypeComparator.supremum({:tuple, [:integer]}, {:map, {:float, :error}}) === :error 28 | assert TypeComparator.supremum({:tuple, [:integer, :error]}, {:map, {:float, :string}}) === :error 29 | end 30 | 31 | test "returns float because is greater than integer" do 32 | assert TypeComparator.supremum(:integer, :float) === :float 33 | assert TypeComparator.supremum(:float, :integer) === :float 34 | end 35 | 36 | test "returns type because is greater than none" do 37 | assert TypeComparator.supremum(:none, :boolean) === :boolean 38 | assert TypeComparator.supremum(:integer, :none) === :integer 39 | assert TypeComparator.supremum(:none, {:list, :string}) === {:list, :string} 40 | assert TypeComparator.supremum({:tuple, [:integer]}, :none) === {:tuple, [:integer]} 41 | end 42 | 43 | # downcast 44 | test "returns type because is less than any but downcast is applied" do 45 | assert TypeComparator.supremum(:any, :boolean) === :boolean 46 | assert TypeComparator.supremum(:integer, :any) === :integer 47 | assert TypeComparator.supremum(:any, {:list, :string}) === {:list, :string} 48 | assert TypeComparator.supremum({:tuple, [:integer]}, :any) === {:tuple, [:integer]} 49 | end 50 | 51 | test "returns the supremum type between two maps" do 52 | assert TypeComparator.supremum({:map, {:integer, [:string]}}, {:map, {:integer, [:string]}}) === {:map, {:integer, [:string]}} 53 | assert TypeComparator.supremum({:map, {:integer, [:string]}}, {:map, {:float, [:string]}}) === {:map, {:float, [:string]}} 54 | assert TypeComparator.supremum({:map, {:integer, [{:list, :integer}, :boolean]}}, {:map, {:float, [{:list, :integer}, :boolean]}}) === {:map, {:float, [{:list, :integer}, :boolean]}} 55 | assert TypeComparator.supremum({:map, {:integer, [:string, :float]}}, {:map, {:float, [:string]}}) === {:map, {:float, [:string]}} 56 | 57 | assert TypeComparator.supremum({:map, {:none, [:any, :string]}}, {:map, {:integer, [:string]}}) === {:map, {:integer, [:string]}} 58 | 59 | assert TypeComparator.supremum({:map, {:integer, [:string]}}, {:map, {:float, [:atom]}}) === {:map, {:float, [:error]}} 60 | assert TypeComparator.supremum({:map, {:integer, [:string]}}, {:map, {:float, [:string, :atom]}}) === :error 61 | end 62 | 63 | test "returns the supremum type between two tuples" do 64 | assert TypeComparator.supremum({:tuple, []}, {:tuple, []}) === {:tuple, []} 65 | assert TypeComparator.supremum({:tuple, [:integer, :string]}, {:tuple, [:integer, :string]}) === {:tuple, [:integer, :string]} 66 | assert TypeComparator.supremum({:tuple, [:integer, :string]}, {:tuple, [:float, :string]}) === {:tuple, [:float, :string]} 67 | assert TypeComparator.supremum({:tuple, [:integer, {:list, :integer}, :boolean]}, {:tuple, [:float, {:list, :float}, :boolean]}) === {:tuple, [:float, {:list, :float}, :boolean]} 68 | 69 | assert TypeComparator.supremum({:tuple, [:any, :any]}, {:tuple, [:integer, :string]}) === {:tuple, [:integer, :string]} 70 | 71 | assert TypeComparator.supremum({:tuple, [:integer, :string]}, {:tuple, [:float, :atom]}) === {:tuple, [:float, :error]} 72 | assert TypeComparator.supremum({:tuple, [:integer]}, {:tuple, [:float, :atom]}) === :error 73 | end 74 | 75 | test "returns the supremum type between two lists" do 76 | assert TypeComparator.supremum({:list, :integer}, {:list, :integer}) === {:list, :integer} 77 | assert TypeComparator.supremum({:list, :integer}, {:list, :float}) === {:list, :float} 78 | assert TypeComparator.supremum({:list, {:list, :integer}}, {:list, {:list, :float}}) === {:list, {:list, :float}} 79 | 80 | assert TypeComparator.supremum({:list, :any}, {:list, :string}) === {:list, :string} 81 | assert TypeComparator.supremum({:list, {:list, :any}}, {:list, {:list, :integer}}) === {:list, {:list, :integer}} 82 | 83 | assert TypeComparator.supremum({:list, :integer}, {:list, :atom}) === {:list, :error} 84 | assert TypeComparator.supremum({:list, {:list, :integer}}, {:list, {:list, :atom}}) === {:list, {:list, :error}} 85 | end 86 | 87 | test "returns a list with the supremum types of two lists" do 88 | assert TypeComparator.supremum([], []) === [] 89 | assert TypeComparator.supremum([:integer, :string], [:integer, :string]) === [:integer, :string] 90 | assert TypeComparator.supremum([:integer, :string], [:float, :string]) === [:float, :string] 91 | assert TypeComparator.supremum([:integer, {:list, :integer}, :boolean], [:float, {:list, :float}, :boolean]) === [:float, {:list, :float}, :boolean] 92 | 93 | assert TypeComparator.supremum([:any, :any], [:integer, :string]) === [:integer, :string] 94 | 95 | assert TypeComparator.supremum([:integer, :float], [:string, :integer]) === [:error, :float] 96 | end 97 | 98 | test "return the supremum in a list" do 99 | assert TypeComparator.supremum([:integer]) === :integer 100 | assert TypeComparator.supremum([:integer, :float]) === :float 101 | 102 | assert TypeComparator.supremum([:any, :integer]) === :integer 103 | assert TypeComparator.supremum([:any, :any]) === :any 104 | 105 | assert TypeComparator.supremum([:atom, :boolean]) === :error 106 | assert TypeComparator.supremum([:any, :boolean, :atom]) === :error 107 | end 108 | end 109 | 110 | describe "has_type?" do 111 | test "returns true when argument is equal to type" do 112 | assert TypeComparator.has_type?(:string, :string) === true 113 | assert TypeComparator.has_type?(:boolean, :boolean) === true 114 | assert TypeComparator.has_type?(:integer, :integer) === true 115 | assert TypeComparator.has_type?(:float, :float) === true 116 | assert TypeComparator.has_type?(:atom, :atom) === true 117 | assert TypeComparator.has_type?(:none, :none) === true 118 | assert TypeComparator.has_type?(:any, :any) === true 119 | 120 | assert TypeComparator.has_type?(:float, :none) === false 121 | assert TypeComparator.has_type?(:integer, :float) === false 122 | assert TypeComparator.has_type?(:string, :boolean) === false 123 | assert TypeComparator.has_type?(:any, :float) === false 124 | end 125 | 126 | test "returns true when key or value types of map are equal to type" do 127 | assert TypeComparator.has_type?({:map, {:integer, [:string]}}, :integer) === true 128 | assert TypeComparator.has_type?({:map, {:integer, [{:list, :integer}]}}, :integer) === true 129 | assert TypeComparator.has_type?({:map, {:integer, [:any]}}, :integer) === true 130 | assert TypeComparator.has_type?({:map, {:none, [:integer]}}, :integer) === true 131 | 132 | assert TypeComparator.has_type?({:map, {:none, [:any]}}, :float) === false 133 | assert TypeComparator.has_type?({:map, {:integer, [:string]}}, :float) === false 134 | assert TypeComparator.has_type?({:map, {:integer, [:integer]}}, :boolean) === false 135 | assert TypeComparator.has_type?({:map, {:none, [:boolean]}}, :integer) === false 136 | end 137 | 138 | test "returns true when one of tuple types contains type" do 139 | assert TypeComparator.has_type?({:tuple, [:any, :integer]}, :integer) === true 140 | assert TypeComparator.has_type?({:tuple, [:integer, :string]}, :integer) === true 141 | assert TypeComparator.has_type?({:tuple, [:integer, {:list, :integer}, :boolean]}, :integer) === true 142 | assert TypeComparator.has_type?({:tuple, [:any, :integer]}, :integer) === true 143 | 144 | assert TypeComparator.has_type?({:tuple, []}, :float) === false 145 | assert TypeComparator.has_type?({:tuple, [:any, :any]}, :float) === false 146 | assert TypeComparator.has_type?({:tuple, [:boolean, :string]}, :integer) === false 147 | assert TypeComparator.has_type?({:tuple, [:any, :any]}, :float) === false 148 | end 149 | 150 | test "returns true when type of list contains type" do 151 | assert TypeComparator.has_type?({:list, :integer}, :integer) === true 152 | assert TypeComparator.has_type?({:list, {:list, :integer}}, :integer) === true 153 | assert TypeComparator.has_type?({:list, {:list, :any}}, :any) === true 154 | assert TypeComparator.has_type?({:list, {:list, :any}}, :any) === true 155 | 156 | assert TypeComparator.has_type?({:list, :any}, :float) === false 157 | assert TypeComparator.has_type?({:list, {:list, :any}}, :boolean) === false 158 | assert TypeComparator.has_type?({:list, :any}, :float) === false 159 | end 160 | 161 | test "returns true when one of the types on a list contains type" do 162 | assert TypeComparator.has_type?([:integer, :string], :integer) === true 163 | assert TypeComparator.has_type?([:integer, {:list, :float}, :boolean], :float) === true 164 | assert TypeComparator.has_type?([:integer, {:list, :float}, :none], :none) === true 165 | assert TypeComparator.has_type?([:integer, {:list, :float}, :any], :any) === true 166 | 167 | assert TypeComparator.has_type?([], :float) === false 168 | assert TypeComparator.has_type?([:any], :atom) === false 169 | assert TypeComparator.has_type?([:any, :string], :atom) === false 170 | assert TypeComparator.has_type?([:integer, :any, :string], :float) === false 171 | assert TypeComparator.has_type?([:any], :atom) === false 172 | end 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /test/typelixir_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TypelixirTest do 2 | # This test is not intended to cover the main functionality, it is only to 3 | # test that the functions on the module works well, and do the step 4 | # to the processor correctly. The big test cases will be on processor_test.exs 5 | 6 | use ExUnit.Case 7 | doctest Typelixir 8 | 9 | describe "check" do 10 | @test_dir "test/tmp" 11 | 12 | setup do 13 | File.mkdir(@test_dir) 14 | 15 | on_exit fn -> 16 | File.rm_rf @test_dir 17 | end 18 | end 19 | 20 | test "returns :ok when there is no module or code defined on the file" do 21 | File.write("test/tmp/example.ex", "") 22 | assert Typelixir.check(["#{@test_dir}/example.ex"]) === :ok 23 | 24 | File.write("test/tmp/example.ex", " 25 | defmodule Example do 26 | end 27 | ") 28 | assert Typelixir.check(["#{@test_dir}/example.ex"]) === :ok 29 | end 30 | 31 | test "returns :ok when a file is well compiled" do 32 | File.write("test/tmp/example.ex", " 33 | defmodule Example do 34 | a = 1 35 | end 36 | ") 37 | assert Typelixir.check(["#{@test_dir}/example.ex"]) === :ok 38 | end 39 | 40 | test "returns :ok when the modules are well compiled by Typelixir" do 41 | File.write("test/tmp/example.ex", " 42 | defmodule Example do 43 | @spec test(integer) :: [integer] 44 | def test(int) do 45 | [int] 46 | end 47 | end 48 | ") 49 | File.write("test/tmp/example2.ex", " 50 | defmodule Example2 do 51 | b = 3 52 | end 53 | ") 54 | File.write("test/tmp/example3.ex", " 55 | defmodule Example3 do 56 | c = [1, 2] ++ Example2.test(3) 57 | end 58 | ") 59 | assert Typelixir.check(["#{@test_dir}/example.ex", "#{@test_dir}/example2.ex", "#{@test_dir}/example3.ex"]) === :ok 60 | end 61 | 62 | test "returns :error when a file is not well compiled by Typelixir" do 63 | File.write("test/tmp/example.ex", " 64 | defmodule Example do 65 | @spec test(integer) :: [integer] 66 | def test(int) do 67 | [int] 68 | end 69 | end 70 | ") 71 | 72 | File.write("test/tmp/example2.ex", " 73 | defmodule Example2 do 74 | a = Example.test(true) 75 | end 76 | ") 77 | assert Typelixir.check(["#{@test_dir}/example.ex", "#{@test_dir}/example2.ex"]) 78 | === {:error, ["Arguments does not match type specification on test/1 in test/tmp/example2.ex:3"]} 79 | end 80 | end 81 | end 82 | --------------------------------------------------------------------------------