├── .formatter.exs ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── csv.ex ├── csv │ ├── LICENSE │ ├── README.md │ ├── defaults.ex │ └── encoding │ │ ├── encode.ex │ │ └── encoder.ex ├── licensir.ex ├── licensir │ ├── file_analyzer.ex │ ├── guesser.ex │ ├── license.ex │ ├── naming_variants.ex │ └── scanner.ex ├── mix │ └── tasks │ │ └── licenses.ex ├── table_rex.ex └── table_rex │ ├── LICENSE │ ├── README.md │ ├── cell.ex │ ├── column.ex │ ├── renderer.ex │ ├── renderer │ ├── text.ex │ └── text │ │ └── meta.ex │ └── table.ex ├── mix.exs ├── mix.lock ├── priv └── licenses │ ├── Apache2_text.txt │ ├── Apache2_text.variant-2.txt │ ├── Apache2_url.txt │ ├── BSD-3.txt │ ├── BSD-3.variant-2.txt │ ├── CC0-1.0.txt │ ├── GPLv2.txt │ ├── GPLv3.txt │ ├── ISC.txt │ ├── ISC.variant-2.txt │ ├── LGPL.txt │ ├── LicensirMockLicense.txt │ ├── MIT.txt │ ├── MIT.variant-2.txt │ ├── MIT.variant-3.txt │ └── MPL2.txt └── test ├── fixtures └── deps │ ├── dep_license_undefined │ ├── hex_metadata.config │ └── mix.exs │ ├── dep_of_dep │ └── mix.exs │ ├── dep_one_license │ ├── LICENSE │ ├── hex_metadata.config │ └── mix.exs │ ├── dep_one_unrecognized_license_file │ ├── LICENSE │ └── mix.exs │ ├── dep_two_conflicting_licenses │ ├── LICENSE │ ├── hex_metadata.config │ └── mix.exs │ ├── dep_two_licenses │ ├── hex_metadata.config │ └── mix.exs │ ├── dep_two_variants_same_license │ ├── hex_metadata.config │ └── mix.exs │ └── dep_with_dep │ └── mix.exs ├── licensir ├── guesser_test.exs ├── naming_variants_test.exs └── scanner_test.exs ├── mix └── tasks │ └── licenses_test.exs ├── support ├── case.ex └── test_app.ex └── test_helper.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 third-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 | licensir-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | 3 | elixir: 4 | - '1.7.4' 5 | - '1.8.1' 6 | - '1.9.0' 7 | 8 | otp_release: 9 | - '20.3' 10 | - '21.3' 11 | 12 | env: 13 | - MIX_ENV=test 14 | 15 | script: mix coveralls.travis 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Unnawut Leepaisalsuwanna 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 | **Notice: This repository is now archived.** Thank you for over 192,000 downloads since December 2017. While I think Elixir is an awesome ecosystem, I no longer have the chance to be developing using Elixir on a regular basis. :'( 2 | 3 | ----- 4 | 5 | # Licensir 6 | 7 | [![Build Status](https://travis-ci.org/unnawut/licensir.svg?branch=master)](https://travis-ci.org/unnawut/licensir) 8 | [![Coverage Status](https://coveralls.io/repos/github/unnawut/licensir/badge.svg?branch=master)](https://coveralls.io/github/unnawut/licensir?branch=master) 9 | [![Module Version](https://img.shields.io/hexpm/v/licensir.svg)](https://hex.pm/packages/licensir) 10 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/licensir/) 11 | [![Total Download](https://img.shields.io/hexpm/dt/licensir.svg)](https://hex.pm/packages/licensir) 12 | [![License](https://img.shields.io/hexpm/l/licensir.svg)](https://github.com/unnawut/licensir/blob/master/LICENSE.md) 13 | [![Last Updated](https://img.shields.io/github/last-commit/unnawut/licensir.svg)](https://github.com/unnawut/licensir/commits/master) 14 | 15 | An Elixir mix task that list the license(s) of all installed packages in your project. 16 | 17 | ## Installation 18 | 19 | The package can be installed by adding `:licensir` to your list of dependencies in `mix.exs`: 20 | 21 | ```elixir 22 | def deps do 23 | [ 24 | {:licensir, "~> 0.7", only: :dev, runtime: false} 25 | ] 26 | end 27 | ``` 28 | 29 | This mix task in most cases only needs to be run on a development machine and independent from the runtime applications, hence the `only: dev, runtime: false` options. 30 | 31 | #### Install locally 32 | 33 | If you do not wish to include this tool as part of your dependencies, you may also install it locally by running: 34 | 35 | ```elixir 36 | $ mix archive.install hex licensir 0.7.0 37 | ``` 38 | 39 | Now you can access this tool from any path on your local machine that has access to `mix`. 40 | 41 | ## Usage 42 | 43 | Run `mix licenses` to get the list of packages and their licenses: 44 | 45 | ```shell 46 | $ mix licenses 47 | +---------------------+---------+--------------------------------------------------------+ 48 | | Package | Version | License | 49 | +---------------------+---------+--------------------------------------------------------+ 50 | | certifi | | BSD | 51 | | earmark | 1.3.2 | Apache 2.0 | 52 | | ex_doc | 0.20.2 | Apache 2.0 | 53 | | excoveralls | | Unsure (found: MIT, Unrecognized license file content) | 54 | | hackney | | Apache 2.0 | 55 | | idna | | Unsure (found: BSD, MIT) | 56 | | jason | | Apache 2.0 | 57 | | makeup | 0.8.0 | Unsure (found: BSD, Unrecognized license file content) | 58 | | makeup_elixir | 0.13.0 | BSD | 59 | | metrics | | BSD | 60 | | mimerl | | MIT | 61 | | nimble_parsec | 0.5.0 | Apache 2.0 | 62 | | ssl_verify_fun | | MIT | 63 | | table_rex | 2.0.0 | MIT | 64 | | unicode_util_compat | | Unsure (found: Apache 2.0, BSD) | 65 | +---------------------+---------+--------------------------------------------------------+ 66 | ``` 67 | 68 | Run `mix licenses --csv` to output in csv format: 69 | 70 | ```csv 71 | Package,Version,License 72 | certifi,,BSD 73 | earmark,1.3.2,Apache 2.0 74 | ex_doc,0.20.2,Apache 2.0 75 | excoveralls,,"Unsure (found: MIT, Unrecognized license file content)" 76 | hackney,,Apache 2.0 77 | idna,,"Unsure (found: BSD, MIT)" 78 | jason,,Apache 2.0 79 | makeup,0.8.0,"Unsure (found: BSD, Unrecognized license file content)" 80 | makeup_elixir,0.13.0,BSD 81 | metrics,,BSD 82 | mimerl,,MIT 83 | nimble_parsec,0.5.0,Apache 2.0 84 | ssl_verify_fun,,MIT 85 | unicode_util_compat,,"Unsure (found: Apache 2.0, BSD)" 86 | ``` 87 | 88 | ### Flags 89 | * `--top-level-only` - Only fetch license information from top level dependencies (e.g. packages that are directly listed in your application's `mix.exs`). Excludes transitive dependencies. 90 | 91 | ## Usage as a library 92 | 93 | You may call the function `Licensir.Scanner.scan()` from your Elixir application to get a list of license data per dependency. 94 | 95 | ```elixir 96 | iex> Licensir.Scanner.scan([]) 97 | [ 98 | %Licensir.License{ 99 | app: :jason, 100 | dep: %Mix.Dep{ 101 | app: :jason, 102 | deps: ... 103 | }, 104 | file: "Apache 2", 105 | hex_metadata: ["Apache 2.0"], 106 | license: "Apache 2.0", 107 | mix: nil, 108 | name: "jason", 109 | version: nil 110 | }, 111 | %Licensir.License{...}, 112 | ... 113 | ] 114 | ``` 115 | 116 | ## Copyright and License 117 | 118 | Copyright (c) 2017, Unnawut Leepaisalsuwanna. 119 | 120 | This library is released under the MIT License. See the [LICENSE.md](./LICENSE.md) file 121 | for further details. 122 | 123 | This project contains 3rd party work as follow: 124 | 125 | - ASCII table rendering: a [partial copy](https://github.com/unnawut/licensir/tree/master/lib/table_rex) of [djm/table_rex](https://github.com/djm/table_rex). 126 | - CSV rendering: a [partial copy](https://github.com/unnawut/licensir/tree/master/lib/csv) of [beatrichartz/csv](https://github.com/beatrichartz/csv). 127 | -------------------------------------------------------------------------------- /lib/csv.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.CSV do 2 | use CSV.Defaults 3 | 4 | alias CSV.Encoding.Encoder 5 | 6 | @moduledoc ~S""" 7 | RFC 4180 compliant CSV parsing and encoding for Elixir. Allows to specify 8 | other separators, so it could also be named: TSV, but it isn't. 9 | """ 10 | 11 | @doc """ 12 | Encode a table stream into a stream of RFC 4180 compliant CSV lines for 13 | writing to a file or other IO. 14 | 15 | ## Options 16 | 17 | These are the options: 18 | 19 | * `:separator` – The separator token to use, defaults to `?,`. 20 | Must be a codepoint (syntax: ? + (your separator)). 21 | * `:delimiter` – The delimiter token to use, defaults to `\\r\\n`. 22 | Must be a string. 23 | 24 | ## Examples 25 | 26 | Convert a stream of rows with cells into a stream of lines: 27 | 28 | iex> [~w(a b), ~w(c d)] 29 | iex> |> CSV.encode 30 | iex> |> Enum.take(2) 31 | [\"a,b\\r\\n\", \"c,d\\r\\n\"] 32 | 33 | Convert a stream of rows with cells with escape sequences into a stream of 34 | lines: 35 | 36 | iex> [[\"a\\nb\", \"\\tc\"], [\"de\", \"\\tf\\\"\"]] 37 | iex> |> CSV.encode(separator: ?\\t, delimiter: \"\\n\") 38 | iex> |> Enum.take(2) 39 | [\"\\\"a\\\\nb\\\"\\t\\\"\\\\tc\\\"\\n\", \"de\\t\\\"\\\\tf\\\"\\\"\\\"\\n\"] 40 | """ 41 | 42 | def encode(stream, options \\ []) do 43 | Encoder.encode(stream, options) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/csv/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Beat 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 | 23 | -------------------------------------------------------------------------------- /lib/csv/README.md: -------------------------------------------------------------------------------- 1 | This directory contains a copy of [beatrichartz/csv](https://github.com/beatrichartz/csv). 2 | 3 | A hard copy is used instead of a dependency so that `mix archive.install ...`, 4 | which does not recognize the archive's defined dependencies, is supported. 5 | -------------------------------------------------------------------------------- /lib/csv/defaults.ex: -------------------------------------------------------------------------------- 1 | defmodule CSV.Defaults do 2 | @moduledoc ~S""" 3 | The module defaults of CSV. 4 | """ 5 | 6 | defmacro __using__(_) do 7 | quote do 8 | @separator ?, 9 | @newline ?\n 10 | @carriage_return ?\r 11 | @delimiter <<@carriage_return::utf8>> <> <<@newline::utf8>> 12 | @double_quote ?" 13 | @escape_max_lines 1000 14 | @replacement nil 15 | end 16 | end 17 | 18 | @doc """ 19 | The default worker / work ratio. 20 | """ 21 | def worker_work_ratio do 22 | 5 23 | end 24 | 25 | @doc """ 26 | The default number of workers used. 27 | """ 28 | def num_workers do 29 | :erlang.system_info(:schedulers) * 3 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/csv/encoding/encode.ex: -------------------------------------------------------------------------------- 1 | defprotocol CSV.Encode do 2 | @fallback_to_any true 3 | @moduledoc """ 4 | Implement encoding for your data types. 5 | """ 6 | 7 | @doc """ 8 | The encode function to implement, gets passed the data and env as a keyword 9 | list containing the currently used separator and delimiter. 10 | """ 11 | def encode(data, env \\ []) 12 | end 13 | 14 | defimpl CSV.Encode, for: Any do 15 | @doc """ 16 | Default encoding implementation, uses the string protocol and feeds into the 17 | string encode implementation 18 | """ 19 | 20 | def encode(data, env \\ []) do 21 | to_string(data) |> CSV.Encode.encode(env) 22 | end 23 | end 24 | 25 | defimpl CSV.Encode, for: BitString do 26 | use CSV.Defaults 27 | 28 | @doc """ 29 | Standard string encoding implementation, escaping cells with double quotes 30 | where necessary. 31 | """ 32 | 33 | def encode(data, env \\ []) do 34 | separator = env |> Keyword.get(:separator, @separator) 35 | delimiter = env |> Keyword.get(:delimiter, @delimiter) 36 | 37 | cond do 38 | String.contains?(data, [ 39 | <>, 40 | delimiter, 41 | <<@carriage_return::utf8>>, 42 | <<@newline::utf8>>, 43 | <<@double_quote::utf8>> 44 | ]) -> 45 | <<@double_quote::utf8>> <> 46 | (data 47 | |> escape 48 | |> String.replace( 49 | <<@double_quote::utf8>>, 50 | <<@double_quote::utf8>> <> <<@double_quote::utf8>> 51 | )) <> <<@double_quote::utf8>> 52 | 53 | true -> 54 | data |> escape 55 | end 56 | end 57 | 58 | defp escape(cell) do 59 | cell 60 | |> String.replace(<<@newline::utf8>>, "\\n") 61 | |> String.replace(<<@carriage_return::utf8>>, "\\r") 62 | |> String.replace("\t", "\\t") 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/csv/encoding/encoder.ex: -------------------------------------------------------------------------------- 1 | defmodule CSV.Encoding.Encoder do 2 | use CSV.Defaults 3 | 4 | @moduledoc ~S""" 5 | The Encoder CSV module takes a table stream and transforms it into RFC 4180 6 | compliant stream of lines for writing to a CSV File or other IO. 7 | """ 8 | 9 | @doc """ 10 | Encode a table stream into a stream of RFC 4180 compliant CSV lines for 11 | writing to a file or other IO. 12 | 13 | ## Options 14 | 15 | These are the options: 16 | 17 | * `:separator` – The separator token to use, defaults to `?,`. 18 | Must be a codepoint (syntax: ? + your separator token). 19 | * `:delimiter` – The delimiter token to use, defaults to `\"\\r\\n\"`. 20 | * `:headers` – When set to `true`, uses the keys of the first map as 21 | the first element in the stream. All subsequent elements are the values 22 | of the maps. When set to a list, will use the given list as the first 23 | element in the stream and order all subsequent elements using that list. 24 | When set to `false` (default), will use the raw inputs as elements. 25 | When set to anything but `false`, all elements in the input stream are 26 | assumed to be maps. 27 | 28 | ## Examples 29 | 30 | Convert a stream of rows with cells into a stream of lines: 31 | 32 | iex> [~w(a b), ~w(c d)] 33 | iex> |> CSV.Encoding.Encoder.encode 34 | iex> |> Enum.take(2) 35 | [\"a,b\\r\\n\", \"c,d\\r\\n\"] 36 | 37 | Convert a stream of maps into a stream of lines: 38 | 39 | iex> [%{"a" => 1, "b" => 2}, %{"a" => 3, "b" => 4}] 40 | iex> |> CSV.Encoding.Encoder.encode(headers: true) 41 | iex> |> Enum.to_list() 42 | [\"a,b\\r\\n\", \"1,2\\r\\n\", \"3,4\\r\\n\"] 43 | 44 | Convert a stream of rows with cells with escape sequences into a stream of 45 | lines: 46 | 47 | iex> [[\"a\\nb\", \"\\tc\"], [\"de\", \"\\tf\\\"\"]] 48 | iex> |> CSV.Encoding.Encoder.encode(separator: ?\\t, delimiter: \"\\n\") 49 | iex> |> Enum.take(2) 50 | [\"\\\"a\\\\nb\\\"\\t\\\"\\\\tc\\\"\\n\", \"de\\t\\\"\\\\tf\\\"\\\"\\\"\\n\"] 51 | """ 52 | 53 | def encode(stream, options \\ []) do 54 | headers = options |> Keyword.get(:headers, false) 55 | 56 | encode_stream(stream, headers, options) 57 | end 58 | 59 | defp encode_stream(stream, false, options) do 60 | stream 61 | |> Stream.transform(0, fn row, acc -> 62 | {[encode_row(row, options)], acc + 1} 63 | end) 64 | end 65 | 66 | defp encode_stream(stream, headers, options) do 67 | stream 68 | |> Stream.transform(0, fn 69 | row, 0 -> 70 | {[ 71 | encode_row(get_headers(row, headers), options), 72 | encode_row(get_values(row, headers), options) 73 | ], 1} 74 | 75 | row, acc -> 76 | {[encode_row(get_values(row, headers), options)], acc + 1} 77 | end) 78 | end 79 | 80 | defp get_headers(row, true), do: Map.keys(row) 81 | defp get_headers(_row, headers), do: headers 82 | 83 | defp get_values(row, true), do: Map.values(row) 84 | defp get_values(row, headers), do: headers |> Enum.map(&Map.get(row, &1)) 85 | 86 | defp encode_row(row, options) do 87 | separator = options |> Keyword.get(:separator, @separator) 88 | delimiter = options |> Keyword.get(:delimiter, @delimiter) 89 | 90 | encoded = 91 | row 92 | |> Enum.map(&encode_cell(&1, separator, delimiter)) 93 | |> Enum.join(<>) 94 | 95 | encoded <> delimiter 96 | end 97 | 98 | defp encode_cell(cell, separator, delimiter) do 99 | CSV.Encode.encode(cell, separator: separator, delimiter: delimiter) 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/licensir.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir do 2 | end 3 | -------------------------------------------------------------------------------- /lib/licensir/file_analyzer.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.FileAnalyzer do 2 | # The file names to check for licenses 3 | @license_files ["LICENSE", "LICENSE.md", "LICENSE.txt"] 4 | 5 | # The files that contain the actual text for each license 6 | @files [ 7 | apache2: ["Apache2_text.txt", "Apache2_text.variant-2.txt", "Apache2_url.txt"], 8 | bsd: ["BSD-3.txt", "BSD-3.variant-2.txt"], 9 | cc0: ["CC0-1.0.txt"], 10 | gpl_v2: ["GPLv2.txt"], 11 | gpl_v3: ["GPLv3.txt"], 12 | isc: ["ISC.txt", "ISC.variant-2.txt"], 13 | lgpl: ["LGPL.txt"], 14 | mit: ["MIT.txt", "MIT.variant-2.txt", "MIT.variant-3.txt"], 15 | mpl2: ["MPL2.txt"], 16 | licensir_mock_license: ["LicensirMockLicense.txt"] 17 | ] 18 | 19 | def analyze(dir_path) do 20 | Enum.find_value(@license_files, fn file_name -> 21 | dir_path 22 | |> Path.join(file_name) 23 | |> File.read() 24 | |> case do 25 | {:ok, content} -> analyze_content(content) 26 | {:error, _} -> nil 27 | end 28 | end) 29 | end 30 | 31 | # Returns the first license that matches 32 | defp analyze_content(content) do 33 | Enum.find_value(@files, fn {license, license_files} -> 34 | found = 35 | Enum.find(license_files, fn license_file -> 36 | license = 37 | :licensir 38 | |> :code.priv_dir() 39 | |> Path.join("licenses") 40 | |> Path.join(license_file) 41 | |> File.read!() 42 | 43 | # Returns true only if the content is a superset of the license text 44 | clean(content) =~ clean(license) 45 | end) 46 | 47 | if found, do: license, else: nil 48 | end) || :unrecognized_license_file 49 | end 50 | 51 | defp clean(content), do: String.replace(content, ~r/\v/, "") 52 | end 53 | -------------------------------------------------------------------------------- /lib/licensir/guesser.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.Guesser do 2 | @moduledoc """ 3 | A module that determines a dependency's license based on different sources gathered. 4 | """ 5 | alias Licensir.{License, NamingVariants} 6 | 7 | @doc """ 8 | Guess the license based on the available license data. 9 | """ 10 | def guess(licenses) when is_list(licenses), do: Enum.map(licenses, &guess/1) 11 | 12 | def guess(%License{} = license) do 13 | hex_metadata_licenses = NamingVariants.normalize(license.hex_metadata) 14 | file_licenses = NamingVariants.normalize(license.file) 15 | 16 | conclusion = guess(hex_metadata_licenses, file_licenses) 17 | Map.put(license, :license, conclusion) 18 | end 19 | 20 | defp guess([], nil), do: "Undefined" 21 | defp guess(nil, nil), do: "Undefined" 22 | defp guess(nil, file), do: file 23 | defp guess(hex, nil) when length(hex) > 0, do: Enum.join(hex, ", ") 24 | defp guess(hex, file) when length(hex) == 1 and hd(hex) == file, do: file 25 | 26 | defp guess(hex, file) do 27 | "Unsure (found: " <> Enum.join(hex, ", ") <> ", " <> file <> ")" 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/licensir/license.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.License do 2 | @moduledoc """ 3 | Stores license information for a dependency. 4 | """ 5 | 6 | @doc """ 7 | The struct that keeps information about a dependency's license. 8 | 9 | It contains: 10 | * `app` - the depedency's name as an atom 11 | * `version` - the version of the dependency being used 12 | * `license` - the best guess of the license 13 | * `certainty` - the certainty that the guessed license is correct on a scale of 0.0 to 1.0 14 | * `license_mix` - the license defined in the dependency's `mix.exs` file 15 | * `license_file` - the license defined in the dependency's `LICENSE` or `LICENSE.md` file 16 | """ 17 | defstruct app: nil, 18 | name: "", 19 | version: nil, 20 | dep: nil, 21 | license: nil, 22 | certainty: 0.0, 23 | mix: nil, 24 | hex_metadata: nil, 25 | file: nil 26 | 27 | @type t :: %__MODULE__{ 28 | app: atom(), 29 | name: String.t(), 30 | version: String.t() | nil, 31 | dep: Mix.Dep.t(), 32 | license: String.t() | nil, 33 | certainty: float(), 34 | mix: list(String.t()) | nil, 35 | hex_metadata: list(String.t()) | nil, 36 | file: String.t() | nil 37 | } 38 | end 39 | -------------------------------------------------------------------------------- /lib/licensir/naming_variants.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.NamingVariants do 2 | @moduledoc """ 3 | Consolidate different naming variants of the same license to a single name. 4 | """ 5 | @variants %{ 6 | # Apache 2.0 7 | "Apache 2" => "Apache 2.0", 8 | "Apache v2.0" => "Apache 2.0", 9 | "Apache 2 (see the file LICENSE for details)" => "Apache 2.0", 10 | "Apache-2.0" => "Apache 2.0" 11 | } 12 | 13 | @doc """ 14 | Turns all variants of the names into a single one. 15 | 16 | Duplicates are removed if found in the list of variants. 17 | """ 18 | @spec normalize(String.t() | [String.t()] | nil) :: String.t() | [String.t()] | nil 19 | def normalize(nil), do: nil 20 | 21 | def normalize(name) when is_binary(name) do 22 | @variants[name] || name 23 | end 24 | 25 | def normalize(names), do: names |> Enum.map(&normalize/1) |> Enum.uniq() 26 | end 27 | -------------------------------------------------------------------------------- /lib/licensir/scanner.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.Scanner do 2 | @moduledoc """ 3 | Scans the project's dependencies for their license information. 4 | """ 5 | alias Licensir.{License, FileAnalyzer, Guesser} 6 | 7 | @human_names %{ 8 | apache2: "Apache 2", 9 | bsd: "BSD", 10 | cc0: "CC0-1.0", 11 | gpl_v2: "GPLv2", 12 | gpl_v3: "GPLv3", 13 | isc: "ISC", 14 | lgpl: "LGPL", 15 | mit: "MIT", 16 | mpl2: "MPL2", 17 | licensir_mock_license: "Licensir Mock License", 18 | unrecognized_license_file: "Unrecognized license file content" 19 | } 20 | 21 | @doc """ 22 | Scans all dependencies, formats into a list, and print out the result. 23 | """ 24 | @spec scan(keyword()) :: list(License.t()) 25 | def scan(opts) do 26 | # Make sure the dependencies are loaded 27 | Mix.Project.get!() 28 | 29 | deps() 30 | |> to_struct() 31 | |> filter_top_level(opts) 32 | |> search_hex_metadata() 33 | |> search_file() 34 | |> Guesser.guess() 35 | end 36 | 37 | @spec deps() :: list(Mix.Dep.t()) 38 | defp deps() do 39 | func = loaded_deps_func_name() 40 | apply(Mix.Dep, func, [[]]) 41 | end 42 | 43 | defp loaded_deps_func_name() do 44 | if Keyword.has_key?(Mix.Dep.__info__(:functions), :load_on_environment) do 45 | :load_on_environment 46 | else 47 | :loaded 48 | end 49 | end 50 | 51 | defp to_struct(deps) when is_list(deps), do: Enum.map(deps, &to_struct/1) 52 | 53 | defp to_struct(%Mix.Dep{} = dep) do 54 | %License{ 55 | app: dep.app, 56 | name: Atom.to_string(dep.app), 57 | version: get_version(dep), 58 | dep: dep 59 | } 60 | end 61 | 62 | defp filter_top_level(deps, opts) do 63 | if Keyword.get(opts, :top_level_only) do 64 | Enum.filter(deps, &(&1.dep.top_level)) 65 | else 66 | deps 67 | end 68 | end 69 | 70 | defp get_version(%Mix.Dep{status: {:ok, version}}), do: version 71 | defp get_version(_), do: nil 72 | 73 | # 74 | # Search in hex_metadata.config 75 | # 76 | 77 | defp search_hex_metadata(licenses) when is_list(licenses), do: Enum.map(licenses, &search_hex_metadata/1) 78 | 79 | defp search_hex_metadata(%License{} = license) do 80 | Map.put(license, :hex_metadata, search_hex_metadata(license.dep)) 81 | end 82 | 83 | defp search_hex_metadata(%Mix.Dep{} = dep) do 84 | Mix.Dep.in_dependency(dep, fn _ -> 85 | "hex_metadata.config" 86 | |> :file.consult() 87 | |> case do 88 | {:ok, metadata} -> metadata 89 | {:error, _} -> [] 90 | end 91 | |> List.keyfind("licenses", 0) 92 | |> case do 93 | {_, licenses} -> licenses 94 | _ -> nil 95 | end 96 | end) 97 | end 98 | 99 | # 100 | # Search in LICENSE file 101 | # 102 | 103 | defp search_file(licenses) when is_list(licenses), do: Enum.map(licenses, &search_file/1) 104 | 105 | defp search_file(%License{} = license) do 106 | Map.put(license, :file, search_file(license.dep)) 107 | end 108 | 109 | defp search_file(%Mix.Dep{} = dep) do 110 | license_atom = 111 | Mix.Dep.in_dependency(dep, fn _ -> 112 | case File.cwd() do 113 | {:ok, dir_path} -> FileAnalyzer.analyze(dir_path) 114 | _ -> nil 115 | end 116 | end) 117 | 118 | Map.get(@human_names, license_atom) 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/mix/tasks/licenses.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Licenses do 2 | @moduledoc """ 3 | Lists the licenses defined by each dependecy. 4 | 5 | Notice: The output from this tool is not a legal advice. Use the information at your own risk. 6 | 7 | ## Arguments 8 | mix licenses # Prints a list containing the licenses defined in each dependency 9 | """ 10 | use Mix.Task 11 | 12 | @shortdoc "Lists each dependency's licenses" 13 | @recursive true 14 | @switches [ 15 | top_level_only: :boolean, 16 | csv: :boolean 17 | ] 18 | 19 | def run(argv) do 20 | {opts, _argv} = OptionParser.parse!(argv, switches: @switches) 21 | 22 | Licensir.Scanner.scan(opts) 23 | |> Enum.sort_by(fn license -> license.name end) 24 | |> Enum.map(&to_row/1) 25 | |> render(opts) 26 | end 27 | 28 | defp to_row(map) do 29 | [map.name, map.version, map.license] 30 | end 31 | 32 | defp render(rows, opts) do 33 | cond do 34 | Keyword.get(opts, :csv) -> render_csv(rows) 35 | true -> render_ascii_table(rows) 36 | end 37 | end 38 | 39 | defp render_ascii_table(rows) do 40 | _ = Mix.Shell.IO.info([:yellow, "Notice: This is not a legal advice. Use the information below at your own risk."]) 41 | 42 | rows 43 | |> Licensir.TableRex.quick_render!(["Package", "Version", "License"]) 44 | |> IO.puts() 45 | end 46 | 47 | defp render_csv(rows) do 48 | rows 49 | |> List.insert_at(0, ["Package", "Version", "License"]) 50 | |> Licensir.CSV.encode() 51 | |> Enum.each(&IO.write/1) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/table_rex.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.TableRex do 2 | @moduledoc """ 3 | TableRex generates configurable, text-based tables for display 4 | """ 5 | alias Licensir.TableRex.Renderer 6 | alias Licensir.TableRex.Table 7 | 8 | @doc """ 9 | A shortcut function to render with a one-liner. 10 | Sacrifices all customisation for those that just want sane defaults. 11 | Returns `{:ok, rendered_string}` on success and `{:error, reason}` on failure. 12 | """ 13 | @spec quick_render(list, list, String.t() | nil) :: Renderer.render_return() 14 | def quick_render(rows, header \\ [], title \\ nil) when is_list(rows) and is_list(header) do 15 | Table.new(rows, header, title) 16 | |> Table.render() 17 | end 18 | 19 | @doc """ 20 | A shortcut function to render with a one-liner. 21 | Sacrifices all customisation for those that just want sane defaults. 22 | Returns the `rendered_string` on success and raises `RuntimeError` on failure. 23 | """ 24 | @spec quick_render!(list, list, String.t() | nil) :: String.t() | no_return 25 | def quick_render!(rows, header \\ [], title \\ nil) when is_list(rows) and is_list(header) do 26 | case quick_render(rows, header, title) do 27 | {:ok, rendered} -> rendered 28 | {:error, reason} -> raise TableRex.Error, message: reason 29 | end 30 | end 31 | end 32 | 33 | defmodule TableRex.Error do 34 | defexception [:message] 35 | end 36 | -------------------------------------------------------------------------------- /lib/table_rex/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Darian Moody 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/table_rex/README.md: -------------------------------------------------------------------------------- 1 | This directory contains a copy of [djm/table_rex](https://github.com/djm/table_rex). 2 | 3 | A hard copy is used instead of a dependency so that `mix archive.install ...`, 4 | which does not recognize the archive's defined dependencies, is supported. 5 | -------------------------------------------------------------------------------- /lib/table_rex/cell.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.TableRex.Cell do 2 | @moduledoc """ 3 | Defines a struct that represents a single table cell, and helper functions. 4 | 5 | A cell stores both the original data _and_ the string-rendered version, 6 | this decision was taken as a tradeoff: this way uses more memory to store 7 | the table structure but the renderers gain the ability to get direct access 8 | to the string-coerced data rather than having to risk repeated coercion or 9 | handle their own storage of the computer values. 10 | 11 | Fields: 12 | 13 | * `raw_value`: The un-coerced original value 14 | 15 | * `rendered_value`: The stringified value for rendering 16 | 17 | * `wrapped_lines`: A list of 1 or more string values representing 18 | the line(s) within the cell to be rendered 19 | 20 | * `align`: 21 | * `:left`: left align text in the cell. 22 | * `:center`: center text in the cell. 23 | * `:right`: right align text in the cell. 24 | * `nil`: align text in cell according to column alignment. 25 | 26 | * `color`: the ANSI color of the cell. 27 | 28 | If creating a Cell manually: raw_value is the only required key to 29 | enable that Cell to work well with the rest of TableRex. It should 30 | be set to a piece of data that can be rendered to string. 31 | """ 32 | alias Licensir.TableRex.Cell 33 | 34 | defstruct raw_value: nil, rendered_value: "", align: nil, color: nil, wrapped_lines: [""] 35 | 36 | @type t :: %__MODULE__{} 37 | 38 | @doc """ 39 | Converts the passed value to be a normalised %Cell{} struct. 40 | 41 | If a non %Cell{} value is passed, this function returns a new 42 | %Cell{} struct with: 43 | 44 | * the `rendered_value` key set to the stringified binary of the 45 | value passed in. 46 | * the `raw_value` key set to original data passed in. 47 | * any other options passed are applied over the normal struct 48 | defaults, which allows overriding alignment & color. 49 | 50 | If a %Cell{} is passed in with no `rendered_value` key, then the 51 | `raw_value` key's value is rendered and saved against it, otherwise 52 | the Cell is passed through untouched. This is so that advanced use 53 | cases which require direct Cell creation and manipulation are not 54 | hindered. 55 | """ 56 | @spec to_cell(Cell.t()) :: Cell.t() 57 | def to_cell(%Cell{rendered_value: rendered_value} = cell) 58 | when is_binary(rendered_value) and rendered_value != "" do 59 | %Cell{cell | wrapped_lines: wrapped_lines(rendered_value)} 60 | end 61 | 62 | def to_cell(%Cell{raw_value: raw_value} = cell) do 63 | rendered_value = to_string(raw_value) 64 | %Cell{cell | rendered_value: rendered_value, wrapped_lines: wrapped_lines(rendered_value)} 65 | end 66 | 67 | @spec to_cell(any, list) :: Cell.t() 68 | def to_cell(value, opts \\ []) 69 | 70 | def to_cell(list, opts) when is_list(list) do 71 | if List.improper?(list) do 72 | list 73 | |> to_string() 74 | |> to_cell(opts) 75 | else 76 | list 77 | |> Enum.join("\n") 78 | |> to_cell(opts) 79 | end 80 | end 81 | 82 | def to_cell(value, opts) do 83 | opts = Enum.into(opts, %{}) 84 | 85 | rendered_value = to_string(value) 86 | 87 | %Cell{ 88 | raw_value: value, 89 | rendered_value: rendered_value, 90 | wrapped_lines: wrapped_lines(rendered_value) 91 | } 92 | |> Map.merge(opts) 93 | end 94 | 95 | @spec height(Cell.t()) :: integer 96 | def height(%Cell{wrapped_lines: lines}), do: length(lines) 97 | 98 | defp wrapped_lines(value) when is_binary(value) do 99 | String.split(value, "\n") 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/table_rex/column.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.TableRex.Column do 2 | @moduledoc """ 3 | Defines a struct that represents a column's metadata 4 | 5 | The align field can be one of :left, :center or :right. 6 | """ 7 | 8 | defstruct align: :left, padding: 1, color: nil 9 | 10 | @type t :: %__MODULE__{} 11 | end 12 | -------------------------------------------------------------------------------- /lib/table_rex/renderer.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.TableRex.Renderer do 2 | @moduledoc """ 3 | An Elixir behaviour that defines the API Renderers should conform to, allowing 4 | for display output in a variety of formats. 5 | """ 6 | 7 | @typedoc "Return value of the render function." 8 | @type render_return :: {:ok, String.t()} | {:error, String.t()} 9 | 10 | @doc "Returns a Map of the options and their default values required by the renderer." 11 | @callback default_options() :: map 12 | 13 | @doc "Renders a passed %TableRex.Table{} struct into a string." 14 | @callback render(table :: %Licensir.TableRex.Table{}, opts :: list) :: render_return 15 | end 16 | -------------------------------------------------------------------------------- /lib/table_rex/renderer/text.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.TableRex.Renderer.Text do 2 | @moduledoc """ 3 | Renderer module which handles outputting ASCII-style tables for display. 4 | """ 5 | alias Licensir.TableRex.Cell 6 | alias Licensir.TableRex.Table 7 | alias Licensir.TableRex.Renderer.Text.Meta 8 | 9 | @behaviour Licensir.TableRex.Renderer 10 | 11 | # horizontal_styles: [:all, :header, :frame:, :off] 12 | # vertical_styles: [:all, :frame, :off] 13 | 14 | # Which horizontal/vertical styles render a specific separator. 15 | @render_horizontal_frame_styles [:all, :frame, :header] 16 | @render_vertical_frame_styles [:all, :frame] 17 | @render_column_separators_styles [:all] 18 | @render_row_separators_styles [:all] 19 | 20 | @doc """ 21 | Provides a level of sane defaults for the Text rendering module. 22 | """ 23 | def default_options do 24 | %{ 25 | horizontal_style: :header, 26 | vertical_style: :all, 27 | horizontal_symbol: "-", 28 | vertical_symbol: "|", 29 | intersection_symbol: "+", 30 | top_frame_symbol: "-", 31 | title_separator_symbol: "-", 32 | header_separator_symbol: "-", 33 | bottom_frame_symbol: "-" 34 | } 35 | end 36 | 37 | @doc """ 38 | Implementation of the TableRex.Renderer behaviour. 39 | 40 | Available styling options. 41 | 42 | `horizontal_styles` controls horizontal separators and can be one of: 43 | 44 | * `:all`: display separators between and around every row. 45 | * `:header`: display outer and header horizontal separators only. 46 | * `:frame`: display outer horizontal separators only. 47 | * `:off`: display no horizontal separators. 48 | 49 | `vertical_styles` controls vertical separators and can be one of: 50 | 51 | * `:all`: display between and around every column. 52 | * `:frame`: display outer vertical separators only. 53 | * `:off`: display no vertical separators. 54 | """ 55 | def render(table = %Table{}, opts) do 56 | {col_widths, row_heights} = max_dimensions(table) 57 | 58 | # Calculations that would otherwise be carried out multiple times are done once and their 59 | # results are stored in the %Meta{} struct which is then passed through the pipeline. 60 | render_horizontal_frame? = opts[:horizontal_style] in @render_horizontal_frame_styles 61 | render_vertical_frame? = opts[:vertical_style] in @render_vertical_frame_styles 62 | render_column_separators? = opts[:vertical_style] in @render_column_separators_styles 63 | render_row_separators? = opts[:horizontal_style] in @render_row_separators_styles 64 | table_width = table_width(col_widths, vertical_frame?: render_vertical_frame?) 65 | intersections = intersections(table_width, col_widths, vertical_style: opts[:vertical_style]) 66 | 67 | meta = %Meta{ 68 | col_widths: col_widths, 69 | row_heights: row_heights, 70 | table_width: table_width, 71 | intersections: intersections, 72 | render_horizontal_frame?: render_horizontal_frame?, 73 | render_vertical_frame?: render_vertical_frame?, 74 | render_column_separators?: render_column_separators?, 75 | render_row_separators?: render_row_separators? 76 | } 77 | 78 | rendered = 79 | {table, meta, opts, []} 80 | |> render_top_frame 81 | |> render_title 82 | |> render_title_separator 83 | |> render_header 84 | |> render_header_separator 85 | |> render_rows 86 | |> render_bottom_frame 87 | |> render_to_string 88 | 89 | {:ok, rendered} 90 | end 91 | 92 | defp render_top_frame({table, %Meta{render_horizontal_frame?: false} = meta, opts, rendered}) do 93 | {table, meta, opts, rendered} 94 | end 95 | 96 | defp render_top_frame({%Table{title: title} = table, meta, opts, rendered}) 97 | when is_binary(title) do 98 | intersections = if meta.render_vertical_frame?, do: [0, meta.table_width - 1], else: [] 99 | 100 | line = 101 | render_line( 102 | meta.table_width, 103 | intersections, 104 | opts[:top_frame_symbol], 105 | opts[:intersection_symbol] 106 | ) 107 | 108 | {table, meta, opts, [line | rendered]} 109 | end 110 | 111 | defp render_top_frame({table, meta, opts, rendered}) do 112 | line = 113 | render_line( 114 | meta.table_width, 115 | meta.intersections, 116 | opts[:top_frame_symbol], 117 | opts[:intersection_symbol] 118 | ) 119 | 120 | {table, meta, opts, [line | rendered]} 121 | end 122 | 123 | defp render_title({%Table{title: nil} = table, meta, opts, rendered}) do 124 | {table, meta, opts, rendered} 125 | end 126 | 127 | defp render_title({%Table{title: title} = table, meta, opts, rendered}) do 128 | inner_width = Meta.inner_width(meta) 129 | line = do_render_cell(title, inner_width) 130 | 131 | line = 132 | if meta.render_vertical_frame? do 133 | line |> frame_with(opts[:vertical_symbol]) 134 | else 135 | line 136 | end 137 | 138 | {table, meta, opts, [line | rendered]} 139 | end 140 | 141 | defp render_title_separator({%Table{title: nil} = table, meta, opts, rendered}) do 142 | {table, meta, opts, rendered} 143 | end 144 | 145 | defp render_title_separator( 146 | {table, meta, %{horizontal_style: horizontal_style} = opts, rendered} 147 | ) 148 | when horizontal_style in [:all, :header] do 149 | line = 150 | render_line( 151 | meta.table_width, 152 | meta.intersections, 153 | opts[:title_separator_symbol], 154 | opts[:intersection_symbol] 155 | ) 156 | 157 | {table, meta, opts, [line | rendered]} 158 | end 159 | 160 | defp render_title_separator({table, %Meta{render_vertical_frame?: true} = meta, opts, rendered}) do 161 | line = render_line(meta.table_width, [0, meta.table_width - 1], " ", opts[:vertical_symbol]) 162 | {table, meta, opts, [line | rendered]} 163 | end 164 | 165 | defp render_title_separator( 166 | {table, %Meta{render_vertical_frame?: false} = meta, opts, rendered} 167 | ) do 168 | {table, meta, opts, ["" | rendered]} 169 | end 170 | 171 | defp render_header({%Table{header_row: []} = table, meta, opts, rendered}) do 172 | {table, meta, opts, rendered} 173 | end 174 | 175 | defp render_header({%Table{header_row: header_row} = table, meta, opts, rendered}) do 176 | separator = if meta.render_column_separators?, do: opts[:vertical_symbol], else: " " 177 | line = render_cell_row(table, meta, header_row, separator, opts[:vertical_symbol]) 178 | 179 | {table, meta, opts, [line | rendered]} 180 | end 181 | 182 | defp render_header_separator({%Table{header_row: []} = table, meta, opts, rendered}) do 183 | {table, meta, opts, rendered} 184 | end 185 | 186 | defp render_header_separator( 187 | {table, meta, %{horizontal_style: horizontal_style} = opts, rendered} 188 | ) 189 | when horizontal_style in [:all, :header] do 190 | line = 191 | render_line( 192 | meta.table_width, 193 | meta.intersections, 194 | opts[:header_separator_symbol], 195 | opts[:intersection_symbol] 196 | ) 197 | 198 | {table, meta, opts, [line | rendered]} 199 | end 200 | 201 | defp render_header_separator( 202 | {table, %Meta{render_vertical_frame?: true} = meta, opts, rendered} 203 | ) do 204 | line = render_line(meta.table_width, [0, meta.table_width - 1], " ", opts[:vertical_symbol]) 205 | {table, meta, opts, [line | rendered]} 206 | end 207 | 208 | defp render_header_separator( 209 | {table, %Meta{render_vertical_frame?: false} = meta, opts, rendered} 210 | ) do 211 | {table, meta, opts, ["" | rendered]} 212 | end 213 | 214 | defp render_rows({%Table{rows: rows} = table, meta, opts, rendered}) do 215 | cell_separator = if meta.render_column_separators?, do: opts[:vertical_symbol], else: " " 216 | 217 | lines = 218 | Enum.map(rows, &render_cell_row(table, meta, &1, cell_separator, opts[:vertical_symbol])) 219 | 220 | lines = 221 | if meta.render_row_separators? do 222 | row_separator = 223 | render_line( 224 | meta.table_width, 225 | meta.intersections, 226 | opts[:horizontal_symbol], 227 | opts[:intersection_symbol] 228 | ) 229 | 230 | Enum.intersperse(lines, row_separator) 231 | else 232 | lines 233 | end 234 | 235 | rendered = lines ++ rendered 236 | {table, meta, opts, rendered} 237 | end 238 | 239 | defp vertically_framed(lines, %{render_vertical_frame?: true}, symbol) do 240 | Enum.map(lines, &frame_with(&1, symbol)) 241 | end 242 | 243 | defp vertically_framed(lines, _, _), do: lines 244 | 245 | defp render_bottom_frame({table, %Meta{render_horizontal_frame?: false} = meta, opts, rendered}) do 246 | {table, meta, opts, rendered} 247 | end 248 | 249 | defp render_bottom_frame({table, meta, opts, rendered}) do 250 | line = 251 | render_line( 252 | meta.table_width, 253 | meta.intersections, 254 | opts[:bottom_frame_symbol], 255 | opts[:intersection_symbol] 256 | ) 257 | 258 | {table, meta, opts, [line | rendered]} 259 | end 260 | 261 | defp render_line(table_width, intersections, separator_symbol, intersection_symbol) do 262 | for n <- 0..(table_width - 1) do 263 | if n in intersections, do: intersection_symbol, else: separator_symbol 264 | end 265 | |> Enum.join() 266 | end 267 | 268 | defp render_cell_row(%Table{} = table, %Meta{} = meta, row, cell_separator, frame_symbol) do 269 | row_height = 270 | row 271 | |> Enum.map(&Cell.height/1) 272 | |> Enum.max() 273 | 274 | 1..row_height 275 | |> Enum.map(&render_cell_row_line(table, meta, row, cell_separator, &1)) 276 | |> vertically_framed(meta, frame_symbol) 277 | |> Enum.join("\n") 278 | end 279 | 280 | defp render_cell_row_line(%Table{} = table, %Meta{} = meta, row, separator, line_index) do 281 | row 282 | |> Enum.map(fn %Cell{wrapped_lines: lines} = cell -> 283 | line_value = Enum.at(lines, line_index - 1) || "" 284 | %Cell{cell | rendered_value: line_value} 285 | end) 286 | |> Enum.with_index() 287 | |> Enum.map(&render_cell(table, meta, &1)) 288 | |> Enum.intersperse(separator) 289 | |> Enum.join() 290 | end 291 | 292 | defp render_cell(%Table{} = table, %Meta{} = meta, {%Cell{} = cell, col_index}) do 293 | col_width = Meta.col_width(meta, col_index) 294 | col_padding = Table.get_column_meta(table, col_index, :padding) 295 | cell_align = Map.get(cell, :align) || Table.get_column_meta(table, col_index, :align) 296 | cell_color = Map.get(cell, :color) || Table.get_column_meta(table, col_index, :color) 297 | 298 | do_render_cell(cell.rendered_value, col_width, col_padding, align: cell_align) 299 | |> format_with_color(cell.rendered_value, cell_color) 300 | end 301 | 302 | defp do_render_cell(value, inner_width) do 303 | do_render_cell(value, inner_width, 0, align: :center) 304 | end 305 | 306 | defp do_render_cell(value, inner_width, _padding, align: :center) do 307 | value_len = String.length(strip_ansi_color_codes(value)) 308 | post_value = ((inner_width - value_len) / 2) |> round 309 | pre_value = inner_width - (post_value + value_len) 310 | String.duplicate(" ", pre_value) <> value <> String.duplicate(" ", post_value) 311 | end 312 | 313 | defp do_render_cell(value, inner_width, padding, align: align) do 314 | value_len = String.length(strip_ansi_color_codes(value)) 315 | alt_side_padding = inner_width - value_len - padding 316 | 317 | {pre_value, post_value} = 318 | case align do 319 | :left -> 320 | {padding, alt_side_padding} 321 | 322 | :right -> 323 | {alt_side_padding, padding} 324 | end 325 | 326 | String.duplicate(" ", pre_value) <> value <> String.duplicate(" ", post_value) 327 | end 328 | 329 | defp intersections(_table_width, _col_widths, vertical_style: :off), do: [] 330 | 331 | defp intersections(table_width, _col_widths, vertical_style: :frame) do 332 | [0, table_width - 1] 333 | |> Enum.into(MapSet.new()) 334 | end 335 | 336 | defp intersections(table_width, col_widths, vertical_style: :all) do 337 | col_widths = ordered_col_widths(col_widths) 338 | 339 | inner_intersections = 340 | Enum.reduce(col_widths, [0], fn x, [acc_h | _] = acc -> 341 | [acc_h + x + 1 | acc] 342 | end) 343 | 344 | ([0, table_width - 1] ++ inner_intersections) 345 | |> Enum.into(MapSet.new()) 346 | end 347 | 348 | defp max_dimensions(%Table{} = table) do 349 | {col_widths, row_heights} = 350 | [table.header_row | table.rows] 351 | |> Enum.with_index() 352 | |> Enum.reduce({%{}, %{}}, &reduce_row_maximums(table, &1, &2)) 353 | 354 | num_columns = map_size(col_widths) 355 | 356 | # Infer padding on left and right of title 357 | title_padding = 358 | [0, num_columns - 1] 359 | |> Enum.map(&Table.get_column_meta(table, &1, :padding)) 360 | |> Enum.sum() 361 | 362 | # Compare table body width with title width 363 | col_separators_widths = num_columns - 1 364 | body_width = (col_widths |> Map.values() |> Enum.sum()) + col_separators_widths 365 | title_width = if(is_nil(table.title), do: 0, else: String.length(table.title)) + title_padding 366 | 367 | # Add extra padding equally to all columns if required to match body and title width. 368 | revised_col_widths = 369 | if body_width >= title_width do 370 | col_widths 371 | else 372 | extra_padding = ((title_width - body_width) / num_columns) |> Float.ceil() |> round 373 | Enum.into(col_widths, %{}, fn {k, v} -> {k, v + extra_padding} end) 374 | end 375 | 376 | {revised_col_widths, row_heights} 377 | end 378 | 379 | defp reduce_row_maximums(%Table{} = table, {row, row_index}, {col_widths, row_heights}) do 380 | row 381 | |> Enum.with_index() 382 | |> Enum.reduce({col_widths, row_heights}, &reduce_cell_maximums(table, &1, &2, row_index)) 383 | end 384 | 385 | defp reduce_cell_maximums( 386 | %Table{} = table, 387 | {cell, col_index}, 388 | {col_widths, row_heights}, 389 | row_index 390 | ) do 391 | padding = Table.get_column_meta(table, col_index, :padding) 392 | {width, height} = content_dimensions(cell.rendered_value, padding) 393 | col_widths = Map.update(col_widths, col_index, width, &Enum.max([&1, width])) 394 | row_heights = Map.update(row_heights, row_index, height, &Enum.max([&1, height])) 395 | {col_widths, row_heights} 396 | end 397 | 398 | defp content_dimensions(value, padding) when is_binary(value) and is_number(padding) do 399 | lines = 400 | value 401 | |> strip_ansi_color_codes() 402 | |> String.split("\n") 403 | 404 | height = Enum.count(lines) 405 | width = lines |> Enum.map(&String.length/1) |> Enum.max() 406 | {width + padding * 2, height} 407 | end 408 | 409 | defp table_width(%{} = col_widths, vertical_frame?: vertical_frame?) do 410 | width = 411 | col_widths 412 | |> Map.values() 413 | |> Enum.intersperse(1) 414 | |> Enum.sum() 415 | 416 | if vertical_frame?, do: width + 2, else: width 417 | end 418 | 419 | defp ordered_col_widths(%{} = col_widths) do 420 | col_widths 421 | |> Enum.into([]) 422 | |> Enum.sort() 423 | |> Enum.map(&elem(&1, 1)) 424 | end 425 | 426 | defp frame_with(string, frame) do 427 | frame <> string <> frame 428 | end 429 | 430 | defp render_to_string({_, _, _, rendered_lines}) when is_list(rendered_lines) do 431 | rendered_lines 432 | |> Enum.map(&String.trim_trailing/1) 433 | |> Enum.reverse() 434 | |> Enum.join("\n") 435 | |> Kernel.<>("\n") 436 | end 437 | 438 | defp format_with_color(text, _, nil), do: text 439 | 440 | defp format_with_color(text, value, color) when is_function(color) do 441 | [color.(text, value) | IO.ANSI.reset()] 442 | |> IO.ANSI.format_fragment(true) 443 | end 444 | 445 | defp format_with_color(text, _, color) do 446 | [[color | text] | IO.ANSI.reset()] 447 | |> IO.ANSI.format_fragment(true) 448 | end 449 | 450 | defp strip_ansi_color_codes(text) do 451 | Regex.replace(~r|\e\[\d+m|u, text, "") 452 | end 453 | end 454 | -------------------------------------------------------------------------------- /lib/table_rex/renderer/text/meta.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.TableRex.Renderer.Text.Meta do 2 | @moduledoc """ 3 | The data structure for the `TableRex.Renderer.Text` rendering module, it holds results 4 | of style & dimension calculations to be passed down the render pipeline. 5 | """ 6 | alias Licensir.TableRex.Renderer.Text.Meta 7 | 8 | defstruct col_widths: %{}, 9 | row_heights: %{}, 10 | table_width: 0, 11 | intersections: [], 12 | render_horizontal_frame?: false, 13 | render_vertical_frame?: false, 14 | render_column_separators?: false, 15 | render_row_separators?: false 16 | 17 | @doc """ 18 | Retrieves the "inner width" of the table, which is the full width minus any frame. 19 | """ 20 | def inner_width(%Meta{table_width: table_width, render_vertical_frame?: true}) do 21 | table_width - 2 22 | end 23 | 24 | def inner_width(%Meta{table_width: table_width, render_vertical_frame?: false}) do 25 | table_width 26 | end 27 | 28 | @doc """ 29 | Retrieves the column width at the given column index. 30 | """ 31 | def col_width(meta, col_index) do 32 | Map.get(meta.col_widths, col_index) 33 | end 34 | 35 | @doc """ 36 | Retrieves the row width at the given row index. 37 | """ 38 | def row_height(meta, row_index) do 39 | Map.get(meta.row_heights, row_index) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/table_rex/table.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.TableRex.Table do 2 | @moduledoc """ 3 | A set of functions for working with tables. 4 | 5 | The `Table` is represented internally as a struct though the 6 | fields are private and must not be accessed directly. Instead, 7 | use the functions in this module. 8 | """ 9 | alias Licensir.TableRex.Cell 10 | alias Licensir.TableRex.Column 11 | alias Licensir.TableRex.Renderer 12 | alias Licensir.TableRex.Table 13 | 14 | defstruct title: nil, header_row: [], rows: [], columns: %{}, default_column: %Column{} 15 | 16 | @type t :: %__MODULE__{} 17 | 18 | @default_renderer Renderer.Text 19 | 20 | @doc """ 21 | Creates a new blank table. 22 | 23 | The table created will not be able to be rendered until it has some row data. 24 | 25 | ## Examples 26 | 27 | iex> Table.new 28 | %Licensir.TableRex.Table{} 29 | 30 | """ 31 | @spec new() :: Table.t() 32 | def new, do: %Table{} 33 | 34 | @doc """ 35 | Creates a new table with an initial set of rows and an optional header and title. 36 | """ 37 | @spec new(list, list, String.t() | nil) :: Table.t() 38 | def new(rows, header_row \\ [], title \\ nil) when is_list(rows) and is_list(header_row) do 39 | new() 40 | |> put_title(title) 41 | |> put_header(header_row) 42 | |> add_rows(rows) 43 | end 44 | 45 | # ------------ 46 | # Mutation API 47 | # ------------ 48 | 49 | @doc """ 50 | Sets a string as the optional table title. 51 | Set to `nil` or `""` to remove an already set title from renders. 52 | """ 53 | @spec put_title(Table.t(), String.t() | nil) :: Table.t() 54 | def put_title(%Table{} = table, ""), do: put_title(table, nil) 55 | 56 | def put_title(%Table{} = table, title) when is_binary(title) or is_nil(title) do 57 | %Table{table | title: title} 58 | end 59 | 60 | @doc """ 61 | Sets a list as the optional header row. 62 | Set to `nil` or `[]` to remove an already set header from renders. 63 | """ 64 | @spec put_header(Table.t(), list | nil) :: Table.t() 65 | def put_header(%Table{} = table, nil), do: put_header(table, []) 66 | 67 | def put_header(%Table{} = table, header_row) when is_list(header_row) do 68 | new_header_row = Enum.map(header_row, &Cell.to_cell(&1)) 69 | %Table{table | header_row: new_header_row} 70 | end 71 | 72 | @doc """ 73 | Sets column level information such as padding and alignment. 74 | """ 75 | @spec put_column_meta(Table.t(), integer | atom | Enum.t(), Keyword.t()) :: Table.t() 76 | def put_column_meta(%Table{} = table, col_index, col_meta) 77 | when is_integer(col_index) and is_list(col_meta) do 78 | col_meta = col_meta |> Enum.into(%{}) 79 | col = get_column(table, col_index) |> Map.merge(col_meta) 80 | new_columns = Map.put(table.columns, col_index, col) 81 | %Table{table | columns: new_columns} 82 | end 83 | 84 | def put_column_meta(%Table{} = table, :all, col_meta) when is_list(col_meta) do 85 | col_meta = col_meta |> Enum.into(%{}) 86 | # First update default column, then any already set columns. 87 | table = put_in(table.default_column, Map.merge(table.default_column, col_meta)) 88 | 89 | new_columns = 90 | Enum.reduce(table.columns, %{}, fn {col_index, col}, acc -> 91 | new_col = Map.merge(col, col_meta) 92 | Map.put(acc, col_index, new_col) 93 | end) 94 | 95 | %Table{table | columns: new_columns} 96 | end 97 | 98 | def put_column_meta(%Table{} = table, col_indexes, col_meta) when is_list(col_meta) do 99 | Enum.reduce(col_indexes, table, &put_column_meta(&2, &1, col_meta)) 100 | end 101 | 102 | @doc """ 103 | Sets cell level information such as alignment. 104 | """ 105 | @spec put_cell_meta(Table.t(), integer, integer, Keyword.t()) :: Table.t() 106 | def put_cell_meta(%Table{} = table, col_index, row_index, cell_meta) 107 | when is_integer(col_index) and is_integer(row_index) and is_list(cell_meta) do 108 | cell_meta = cell_meta |> Enum.into(%{}) 109 | inverse_row_index = -(row_index + 1) 110 | 111 | rows = 112 | List.update_at(table.rows, inverse_row_index, fn row -> 113 | List.update_at(row, col_index, &Map.merge(&1, cell_meta)) 114 | end) 115 | 116 | %Table{table | rows: rows} 117 | end 118 | 119 | @doc """ 120 | Sets cell level information for the header cells. 121 | """ 122 | @spec put_header_meta(Table.t(), integer | Enum.t(), Keyword.t()) :: Table.t() 123 | def put_header_meta(%Table{} = table, col_index, cell_meta) 124 | when is_integer(col_index) and is_list(cell_meta) do 125 | cell_meta = cell_meta |> Enum.into(%{}) 126 | header_row = List.update_at(table.header_row, col_index, &Map.merge(&1, cell_meta)) 127 | %Table{table | header_row: header_row} 128 | end 129 | 130 | def put_header_meta(%Table{} = table, col_indexes, cell_meta) when is_list(cell_meta) do 131 | Enum.reduce(col_indexes, table, &put_header_meta(&2, &1, cell_meta)) 132 | end 133 | 134 | @doc """ 135 | Adds a single row to the table. 136 | """ 137 | @spec add_row(Table.t(), list) :: Table.t() 138 | def add_row(%Table{} = table, row) when is_list(row) do 139 | new_row = Enum.map(row, &Cell.to_cell(&1)) 140 | %Table{table | rows: [new_row | table.rows]} 141 | end 142 | 143 | @doc """ 144 | Adds multiple rows to the table. 145 | """ 146 | @spec add_rows(Table.t(), list) :: Table.t() 147 | def add_rows(%Table{} = table, rows) when is_list(rows) do 148 | rows = 149 | rows 150 | |> Enum.reverse() 151 | |> Enum.map(fn row -> 152 | Enum.map(row, &Cell.to_cell(&1)) 153 | end) 154 | 155 | %Table{table | rows: rows ++ table.rows} 156 | end 157 | 158 | @doc """ 159 | Removes column meta for all columns, effectively resetting 160 | column meta back to the default options across the board. 161 | """ 162 | @spec clear_all_column_meta(Table.t()) :: Table.t() 163 | def clear_all_column_meta(%Table{} = table) do 164 | %Table{table | columns: %{}} 165 | end 166 | 167 | @doc """ 168 | Removes all row data from the table, keeping everything else. 169 | """ 170 | @spec clear_rows(Table.t()) :: Table.t() 171 | def clear_rows(%Table{} = table) do 172 | %Table{table | rows: []} 173 | end 174 | 175 | @doc """ 176 | Sorts the table rows by using the values in a specified column. 177 | 178 | This is very much a simple sorting function and relies on Elixir's 179 | built-in comparison operators & types to cover the basic cases. 180 | 181 | As each cell retains the original value it was created with, we 182 | use that value to sort on as this allows us to sort on many 183 | built-in types in the most obvious fashions. 184 | 185 | Remember that rows are stored internally in reverse order that 186 | they will be output in, to allow for fast insertion. 187 | 188 | Parameters: 189 | 190 | `column_index`: the 0-indexed column number to sort by 191 | `order`: supply :desc or :asc for sort direction. 192 | 193 | Returns a new Table, with sorted rows. 194 | """ 195 | @spec sort(Table.t(), integer, atom) :: Table.t() 196 | def sort(table, column_index, order \\ :desc) 197 | 198 | def sort(%Table{rows: [first_row | _]}, column_index, _order) 199 | when length(first_row) <= column_index do 200 | raise Licensir.TableRex.Error, 201 | message: 202 | "You cannot sort by column #{column_index}, as the table only has #{length(first_row)} column(s)" 203 | end 204 | 205 | def sort(table = %Table{rows: rows}, column_index, order) do 206 | %Table{table | rows: Enum.sort(rows, build_sort_function(column_index, order))} 207 | end 208 | 209 | defp build_sort_function(column_index, order) when order in [:desc, :asc] do 210 | fn previous, next -> 211 | %{raw_value: prev_value} = Enum.at(previous, column_index) 212 | %{raw_value: next_value} = Enum.at(next, column_index) 213 | 214 | if order == :desc do 215 | next_value > prev_value 216 | else 217 | next_value < prev_value 218 | end 219 | end 220 | end 221 | 222 | defp build_sort_function(_column_index, order) do 223 | raise Licensir.TableRex.Error, 224 | message: "Invalid sort order parameter: #{order}. Must be an atom, either :desc or :asc." 225 | end 226 | 227 | # ------------- 228 | # Retrieval API 229 | # ------------- 230 | 231 | defp get_column(%Table{} = table, col_index) when is_integer(col_index) do 232 | Map.get(table.columns, col_index, table.default_column) 233 | end 234 | 235 | @doc """ 236 | Retrieves the value of a column meta option at the specified col_index. 237 | If no value has been set, default values are returned. 238 | """ 239 | @spec get_column_meta(Table.t(), integer, atom) :: any 240 | def get_column_meta(%Table{} = table, col_index, key) 241 | when is_integer(col_index) and is_atom(key) do 242 | get_column(table, col_index) 243 | |> Map.fetch!(key) 244 | end 245 | 246 | @doc """ 247 | Returns a boolean detailing if the passed table has any row data set. 248 | """ 249 | @spec has_rows?(Table.t()) :: boolean 250 | def has_rows?(%Table{rows: []}), do: false 251 | def has_rows?(%Table{rows: rows}) when is_list(rows), do: true 252 | 253 | @doc """ 254 | Returns a boolean detailing if the passed table has a header row set. 255 | """ 256 | @spec has_header?(Table.t()) :: boolean 257 | def has_header?(%Table{header_row: []}), do: false 258 | def has_header?(%Table{header_row: header_row}) when is_list(header_row), do: true 259 | 260 | # ------------- 261 | # Rendering API 262 | # ------------- 263 | 264 | @doc """ 265 | Renders the current table state to string, ready for display via `IO.puts/2` or other means. 266 | 267 | At least one row must have been added before rendering. 268 | 269 | Returns `{:ok, rendered_string}` on success and `{:error, reason}` on failure. 270 | """ 271 | @spec render(Table.t(), list) :: Renderer.render_return() 272 | def render(%Table{} = table, opts \\ []) when is_list(opts) do 273 | {renderer, opts} = Keyword.pop(opts, :renderer, @default_renderer) 274 | opts = opts |> Enum.into(renderer.default_options) 275 | 276 | if Table.has_rows?(table) do 277 | renderer.render(table, opts) 278 | else 279 | {:error, "Table must have at least one row before being rendered"} 280 | end 281 | end 282 | 283 | @doc """ 284 | Renders the current table state to string, ready for display via `IO.puts/2` or other means. 285 | 286 | At least one row must have been added before rendering. 287 | 288 | Returns the rendered string on success, or raises `TableRex.Error` on failure. 289 | """ 290 | @spec render!(Table.t(), list) :: String.t() | no_return 291 | def render!(%Table{} = table, opts \\ []) when is_list(opts) do 292 | case render(table, opts) do 293 | {:ok, rendered_string} -> rendered_string 294 | {:error, reason} -> raise Licensir.TableRex.Error, message: reason 295 | end 296 | end 297 | end 298 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Licensir.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/unnawut/licensir" 5 | @version "0.7.0" 6 | 7 | def project do 8 | [ 9 | app: :licensir, 10 | version: @version, 11 | elixir: "~> 1.5", 12 | elixirc_paths: elixirc_paths(Mix.env()), 13 | escript: [main_module: Licensir.Licenses], 14 | name: "Licensir", 15 | deps: deps(), 16 | package: package(), 17 | docs: docs(), 18 | test_coverage: [tool: ExCoveralls], 19 | preferred_cli_env: [ 20 | coveralls: :test, 21 | "coveralls.detail": :test, 22 | "coveralls.post": :test, 23 | "coveralls.html": :test 24 | ] 25 | ] 26 | end 27 | 28 | # Specifies which paths to compile per environment. 29 | defp elixirc_paths(:test), do: ["test/support", "lib"] 30 | defp elixirc_paths(_), do: ["lib"] 31 | 32 | defp package do 33 | [ 34 | description: 35 | "An Elixir mix task that list the license(s) " <> 36 | "of all installed packages in your project.", 37 | files: ["lib", "priv", "mix.exs", "README.md", "LICENSE"], 38 | maintainers: ["Unnawut Leepaisalsuwanna"], 39 | licenses: ["MIT"], 40 | links: %{"GitHub" => @source_url} 41 | ] 42 | end 43 | 44 | defp deps do 45 | [ 46 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 47 | {:excoveralls, "~> 0.8", only: :test} 48 | ] 49 | end 50 | 51 | defp docs do 52 | [ 53 | extras: [ 54 | "LICENSE": [title: "License"], 55 | "README.md": [title: "Overview"] 56 | ], 57 | main: "readme", 58 | source_url: @source_url, 59 | source_ref: "v#{@version}", 60 | formatters: ["html"], 61 | filter_prefix: "Licensir" 62 | ] 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm", "fdc6066ceeccb3aa14049ab6edf0b9af3b64ae1b0db2a92d5c52146f373bbb1c"}, 3 | "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm", "e3be2bc3ae67781db529b80aa7e7c49904a988596e2dbff897425b48b3581161"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"}, 5 | "ex_doc": {:hex, :ex_doc, "0.24.2", "e4c26603830c1a2286dae45f4412a4d1980e1e89dc779fcd0181ed1d5a05c8d9", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e134e1d9e821b8d9e4244687fb2ace58d479b67b282de5158333b0d57c6fb7da"}, 6 | "excoveralls": {:hex, :excoveralls, "0.9.0", "dd597ccf119aa0be0c1c6681215df588596397833b8dd010fe3d1a48090f3119", [:mix], [{:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8d53498b4950f5a04fccd4d203c964b78e77d6459efc1dc418347628f9672788"}, 7 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "bb3cc62ecc10145f8f0965c05083a5278eae7ef1853d340cc9a7a3e27609b9bd"}, 9 | "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fc1a2f7340c422650504b1662f28fdf381f34cbd30664e8491744e52c9511d40"}, 10 | "jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b96c400e04b7b765c0854c05a4966323e90c0d11fee0483b1567cda079abb205"}, 11 | "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, 12 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 13 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, 14 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 15 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 16 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm", "7a4c8e1115a2732a67d7624e28cf6c9f30c66711a9e92928e745c255887ba465"}, 17 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 18 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm", "4f8805eb5c8a939cf2359367cb651a3180b27dfb48444846be2613d79355d65e"}, 19 | "table_rex": {:hex, :table_rex, "2.0.0", "712783cbc2decb4d644d2ab8ad9315294f960c41b2cf0539308164922e352084", [:mix], [], "hexpm"}, 20 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm", "da1d9bef8a092cc7e1e51f1298037a5ddfb0f657fe862dfe7ba4c5807b551c29"}, 21 | } 22 | -------------------------------------------------------------------------------- /priv/licenses/Apache2_text.txt: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 2 | -------------------------------------------------------------------------------- /priv/licenses/Apache2_text.variant-2.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | -------------------------------------------------------------------------------- /priv/licenses/Apache2_url.txt: -------------------------------------------------------------------------------- 1 | http://www.apache.org/licenses/LICENSE-2.0 2 | -------------------------------------------------------------------------------- /priv/licenses/BSD-3.txt: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 2 | 3 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 4 | 5 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 6 | 7 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /priv/licenses/BSD-3.variant-2.txt: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without 2 | modification, are permitted provided that the following conditions are 3 | met: 4 | 5 | * Redistributions of source code must retain the above copyright 6 | notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | * The names of its contributors may not be used to endorse or promote 13 | products derived from this software without specific prior written 14 | permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /priv/licenses/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | -------------------------------------------------------------------------------- /priv/licenses/GPLv2.txt: -------------------------------------------------------------------------------- 1 | Version 2, June 1991 2 | -------------------------------------------------------------------------------- /priv/licenses/GPLv3.txt: -------------------------------------------------------------------------------- 1 | Version 3, 29 June 2007 2 | -------------------------------------------------------------------------------- /priv/licenses/ISC.txt: -------------------------------------------------------------------------------- 1 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 2 | -------------------------------------------------------------------------------- /priv/licenses/ISC.variant-2.txt: -------------------------------------------------------------------------------- 1 | Permission to use, copy, modify, and/or distribute this software for any 2 | purpose with or without fee is hereby granted, provided that the above 3 | copyright notice and this permission notice appear in all copies. 4 | -------------------------------------------------------------------------------- /priv/licenses/LGPL.txt: -------------------------------------------------------------------------------- 1 | This version of the GNU Lesser General Public License incorporates 2 | the terms and conditions of version 3 of the GNU General Public 3 | License, supplemented by the additional permissions listed below. 4 | -------------------------------------------------------------------------------- /priv/licenses/LicensirMockLicense.txt: -------------------------------------------------------------------------------- 1 | This is the content of Licensir's Mock License. Used for testing the Licensir package. 2 | -------------------------------------------------------------------------------- /priv/licenses/MIT.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | -------------------------------------------------------------------------------- /priv/licenses/MIT.variant-2.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /priv/licenses/MIT.variant-3.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person 2 | obtaining a copy of this software and associated documentation 3 | files (the "Software"), to deal in the Software without 4 | restriction, including without limitation the rights to use, 5 | copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | copies of the Software, and to permit persons to whom the 7 | Software is furnished to do so, subject to the following 8 | conditions: 9 | 10 | The above copyright notice and this permission notice shall be 11 | included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 20 | OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /priv/licenses/MPL2.txt: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_license_undefined/hex_metadata.config: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unnawut/licensir/f7bc6ab5b8677867c36af25dea4b3b5bd77cb3ac/test/fixtures/deps/dep_license_undefined/hex_metadata.config -------------------------------------------------------------------------------- /test/fixtures/deps/dep_license_undefined/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DepLicenseUndefined.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :dep_license_undefined, 7 | version: "0.0.1", 8 | package: package() 9 | ] 10 | end 11 | 12 | defp package do 13 | [ 14 | # Undefined license 15 | ] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_of_dep/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DepOfDep.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :dep_of_dep, 7 | version: "0.0.1", 8 | package: package() 9 | ] 10 | end 11 | 12 | defp package do 13 | [ 14 | ] 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_one_license/LICENSE: -------------------------------------------------------------------------------- 1 | This is the content of Licensir's Mock License. Used for testing the Licensir package. 2 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_one_license/hex_metadata.config: -------------------------------------------------------------------------------- 1 | {<<"licenses">>,[<<"Licensir Mock License">>]}. 2 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_one_license/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DepOneLicense.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :dep_one_license, 7 | version: "1.0.0", 8 | package: package() 9 | ] 10 | end 11 | 12 | defp package do 13 | [ 14 | licenses: ["Licensir Mock License"] 15 | ] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_one_unrecognized_license_file/LICENSE: -------------------------------------------------------------------------------- 1 | Some license that is unknown. 2 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_one_unrecognized_license_file/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DepOneUnrecognizedLicense.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :dep_one_license, 7 | version: "1.0.0", 8 | package: package() 9 | ] 10 | end 11 | 12 | defp package do 13 | [ 14 | licenses: [] 15 | ] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_two_conflicting_licenses/LICENSE: -------------------------------------------------------------------------------- 1 | This is the content of Licensir's Mock License. Used for testing the Licensir package. 2 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_two_conflicting_licenses/hex_metadata.config: -------------------------------------------------------------------------------- 1 | {<<"licenses">>,[<<"License One">>]}. 2 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_two_conflicting_licenses/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DepTwoConflictingLicenses.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :dep_one_license, 7 | version: "1.0.0", 8 | package: package() 9 | ] 10 | end 11 | 12 | defp package do 13 | [ 14 | licenses: ["Licensir One"] 15 | ] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_two_licenses/hex_metadata.config: -------------------------------------------------------------------------------- 1 | {<<"licenses">>,[<<"License Two">>, <<"License Three">>]}. 2 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_two_licenses/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DepTwoLicenses.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :dep_two_licenses, 7 | version: "2.0.0", 8 | package: package() 9 | ] 10 | end 11 | 12 | defp package do 13 | [ 14 | licenses: ["License Two", "License Three"] 15 | ] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_two_variants_same_license/hex_metadata.config: -------------------------------------------------------------------------------- 1 | {<<"licenses">>,[<<"Apache 2">>, <<"Apache v2.0">>]}. 2 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_two_variants_same_license/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DepTwoVariantsSameLicense.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :dep_two_variants_same_license, 7 | version: "2.0.0", 8 | package: package() 9 | ] 10 | end 11 | 12 | defp package do 13 | [ 14 | licenses: ["Apache 2", "Apache v2.0"] 15 | ] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/fixtures/deps/dep_with_dep/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DepWithDep.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :dep_with_dep, 7 | version: "0.0.1", 8 | package: package(), 9 | deps: deps() 10 | ] 11 | end 12 | 13 | defp package do 14 | [ 15 | ] 16 | end 17 | 18 | defp deps do 19 | [ 20 | {:dep_of_dep, path: "../dep_of_dep"} 21 | ] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/licensir/guesser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Licensir.GuesserTest do 2 | use Licensir.Case 3 | alias Licensir.{Guesser, License} 4 | 5 | describe "Guesser.guess/1" do 6 | test "returns the license in hex_metadata" do 7 | license = %License{ 8 | hex_metadata: ["License in Mix"], 9 | file: nil 10 | } 11 | 12 | assert Guesser.guess(license).license == "License in Mix" 13 | end 14 | 15 | test "returns the license in file" do 16 | license = %License{ 17 | hex_metadata: nil, 18 | file: "License in file" 19 | } 20 | 21 | assert Guesser.guess(license).license == "License in file" 22 | end 23 | 24 | test "returns the license if the license in hex_metadata and file are equal" do 25 | license = %License{ 26 | hex_metadata: ["Same License"], 27 | file: "Same License" 28 | } 29 | 30 | assert Guesser.guess(license).license == "Same License" 31 | end 32 | 33 | test "returns unsure if the license in hex_metadata and file are not the same" do 34 | license = %License{ 35 | hex_metadata: ["License One"], 36 | file: "License Two" 37 | } 38 | 39 | assert Guesser.guess(license).license == "Unsure (found: License One, License Two)" 40 | end 41 | 42 | test "returns unsure if there are multiple licenses in hex_metadata and also one definted in file" do 43 | license = %License{ 44 | hex_metadata: ["License One", "License Two"], 45 | file: "License Three" 46 | } 47 | 48 | assert Guesser.guess(license).license == 49 | "Unsure (found: License One, License Two, License Three)" 50 | end 51 | 52 | test "returns Undefined if no license data is found" do 53 | license = %License{ 54 | hex_metadata: nil, 55 | file: nil 56 | } 57 | 58 | assert Guesser.guess(license).license == "Undefined" 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/licensir/naming_variants_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Licensir.NamingVariantsTest do 2 | use Licensir.Case 3 | alias Licensir.NamingVariants 4 | 5 | describe "normalize/1" do 6 | test "normalizes variants of Apache 2.0" do 7 | assert NamingVariants.normalize("Apache 2.0") == "Apache 2.0" 8 | assert NamingVariants.normalize("Apache 2") == "Apache 2.0" 9 | assert NamingVariants.normalize("Apache v2.0") == "Apache 2.0" 10 | assert NamingVariants.normalize("Apache-2.0") == "Apache 2.0" 11 | end 12 | 13 | test "normalizes nil to nil" do 14 | assert NamingVariants.normalize(nil) == nil 15 | end 16 | 17 | test "returns the original value if variants are not known" do 18 | assert NamingVariants.normalize("Unknown License 1.0") == "Unknown License 1.0" 19 | end 20 | 21 | test "supports a list of licenses" do 22 | assert NamingVariants.normalize(["Apache 2", nil, "Pass Through"]) == ["Apache 2.0", nil, "Pass Through"] 23 | end 24 | 25 | test "removes duplicates from the list of licenses" do 26 | assert NamingVariants.normalize(["One License", "One License"]) == ["One License"] 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/licensir/scanner_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Licensir.ScannerTest do 2 | use Licensir.Case 3 | 4 | test "returns a list of Licensir.Licenses struct" do 5 | licenses = Licensir.Scanner.scan([]) 6 | 7 | assert Enum.all?(licenses, fn license -> 8 | license.__struct__ == Licensir.License 9 | end) 10 | end 11 | 12 | test "returns a list of Licensir.TestApp's licenses" do 13 | licenses = Licensir.Scanner.scan([]) 14 | 15 | assert has_license?(licenses, %{app: :dep_one_license}) 16 | assert has_license?(licenses, %{app: :dep_two_licenses}) 17 | assert has_license?(licenses, %{app: :dep_license_undefined}) 18 | end 19 | 20 | test "can filter Licensir.TestApp's dependencies for top-level only" do 21 | licenses = Licensir.Scanner.scan([top_level_only: true]) 22 | 23 | assert has_license?(licenses, %{app: :dep_with_dep}) 24 | refute has_license?(licenses, %{app: :dep_of_dep}) 25 | end 26 | 27 | defp has_license?(licenses, search_map) do 28 | Enum.any?(licenses, fn license -> 29 | Map.merge(license, search_map) == license 30 | end) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/mix/tasks/licenses_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Licensir.Mix.Tasks.LicensesTest do 2 | use Licensir.Case 3 | import ExUnit.CaptureIO 4 | 5 | test "prints a list of dependencies and their licenses" do 6 | output = 7 | capture_io(fn -> 8 | Mix.Tasks.Licenses.run([]) 9 | end) 10 | 11 | expected = 12 | IO.ANSI.format_fragment([ 13 | [:yellow, "Notice: This is not a legal advice. Use the information below at your own risk."], :reset, "\n", 14 | "+-----------------------------------+---------+----------------------------------------------------+", "\n", 15 | "| Package | Version | License |", "\n", 16 | "+-----------------------------------+---------+----------------------------------------------------+", "\n", 17 | "| dep_license_undefined | | Undefined |", "\n", 18 | "| dep_of_dep | | Undefined |", "\n", 19 | "| dep_one_license | | Licensir Mock License |", "\n", 20 | "| dep_one_unrecognized_license_file | | Unrecognized license file content |", "\n", 21 | "| dep_two_conflicting_licenses | | Unsure (found: License One, Licensir Mock License) |", "\n", 22 | "| dep_two_licenses | | License Two, License Three |", "\n", 23 | "| dep_two_variants_same_license | | Apache 2.0 |", "\n", 24 | "| dep_with_dep | | Undefined |", "\n", 25 | "+-----------------------------------+---------+----------------------------------------------------+", "\n", "\n" 26 | ]) 27 | |> to_string() 28 | 29 | assert output == expected 30 | end 31 | 32 | test "prints csv format when given --csv flag" do 33 | output = 34 | capture_io(fn -> 35 | Mix.Tasks.Licenses.run(["--csv"]) 36 | end) 37 | 38 | expected = 39 | """ 40 | Package,Version,License\r 41 | dep_license_undefined,,Undefined\r 42 | dep_of_dep,,Undefined\r 43 | dep_one_license,,Licensir Mock License\r 44 | dep_one_unrecognized_license_file,,Unrecognized license file content\r 45 | dep_two_conflicting_licenses,,"Unsure (found: License One, Licensir Mock License)"\r 46 | dep_two_licenses,,"License Two, License Three"\r 47 | dep_two_variants_same_license,,Apache 2.0\r 48 | dep_with_dep,,Undefined\r 49 | """ 50 | 51 | assert output == expected 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/support/case.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.Case do 2 | use ExUnit.CaseTemplate 3 | alias Licensir.TestApp 4 | alias Mix.Project 5 | 6 | setup do 7 | Project.push(TestApp) 8 | on_exit(fn -> Project.pop() end) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/support/test_app.ex: -------------------------------------------------------------------------------- 1 | defmodule Licensir.TestApp do 2 | def project do 3 | [ 4 | deps: [ 5 | {:dep_with_dep, path: "test/fixtures/deps/dep_with_dep"}, 6 | {:dep_one_license, path: "test/fixtures/deps/dep_one_license"}, 7 | {:dep_two_licenses, path: "test/fixtures/deps/dep_two_licenses"}, 8 | {:dep_license_undefined, path: "test/fixtures/deps/dep_license_undefined"}, 9 | {:dep_two_variants_same_license, path: "test/fixtures/deps/dep_two_variants_same_license"}, 10 | {:dep_two_conflicting_licenses, path: "test/fixtures/deps/dep_two_conflicting_licenses"}, 11 | {:dep_one_unrecognized_license_file, path: "test/fixtures/deps/dep_one_unrecognized_license_file"} 12 | ] 13 | ] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------