2020-02-16 21:03:49 +03:00
|
|
|
module NoInvalidLicense exposing (rule)
|
|
|
|
|
|
|
|
{-| Forbid the use of dependencies that use unknown or forbidden licenses.
|
|
|
|
|
|
|
|
|
|
|
|
# Rule
|
|
|
|
|
|
|
|
@docs rule
|
|
|
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
import Dict exposing (Dict)
|
|
|
|
import Elm.License
|
|
|
|
import Elm.Package
|
|
|
|
import Elm.Project
|
|
|
|
import Elm.Syntax.Range exposing (Range)
|
2020-03-07 20:37:00 +03:00
|
|
|
import Review.Project.Dependency as Dependency exposing (Dependency)
|
2020-02-16 21:03:49 +03:00
|
|
|
import Review.Rule as Rule exposing (Error, Rule)
|
|
|
|
import Set exposing (Set)
|
|
|
|
|
|
|
|
|
|
|
|
{-| Forbid the use of dependencies that use unknown or forbidden licenses.
|
|
|
|
|
|
|
|
config =
|
|
|
|
[ NoInvalidLicense.rule
|
|
|
|
{ allowed = [ "BSD-3-Clause", "MIT" ]
|
|
|
|
, forbidden = [ "GPL-3.0-only", "GPL-3.0-or-later" ]
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
-}
|
|
|
|
rule : Configuration -> Rule
|
|
|
|
rule configuration =
|
2020-03-19 21:21:49 +03:00
|
|
|
Rule.newProjectRuleSchema "NoInvalidLicense" initialProjectContext
|
2020-03-03 00:52:22 +03:00
|
|
|
|> Rule.withElmJsonProjectVisitor elmJsonVisitor
|
2020-03-03 00:54:22 +03:00
|
|
|
|> Rule.withDependenciesProjectVisitor dependenciesVisitor
|
2020-02-16 21:03:49 +03:00
|
|
|
|> Rule.withFinalProjectEvaluation (finalEvaluationForProject configuration)
|
|
|
|
|> Rule.fromProjectRuleSchema
|
|
|
|
|
|
|
|
|
|
|
|
type alias Configuration =
|
|
|
|
{ allowed : List String
|
|
|
|
, forbidden : List String
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-03-19 01:01:04 +03:00
|
|
|
dependenciesVisitor : Dict String Dependency -> ProjectContext -> ( List nothing, ProjectContext )
|
2020-02-16 21:03:49 +03:00
|
|
|
dependenciesVisitor dependencies projectContext =
|
|
|
|
let
|
|
|
|
licenses : Dict String String
|
|
|
|
licenses =
|
|
|
|
dependencies
|
|
|
|
|> Dict.toList
|
|
|
|
|> List.filterMap
|
|
|
|
(\( packageName, dependency ) ->
|
2020-03-07 20:37:00 +03:00
|
|
|
case Dependency.elmJson dependency of
|
2020-02-16 21:03:49 +03:00
|
|
|
Elm.Project.Package { license } ->
|
|
|
|
Just ( packageName, Elm.License.toString license )
|
|
|
|
|
|
|
|
Elm.Project.Application _ ->
|
|
|
|
Nothing
|
|
|
|
)
|
|
|
|
|> Dict.fromList
|
|
|
|
in
|
2020-03-19 01:01:04 +03:00
|
|
|
( [], { projectContext | licenses = licenses } )
|
2020-02-16 21:03:49 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- PROJECT VISITORS
|
|
|
|
|
|
|
|
|
2020-03-19 01:08:06 +03:00
|
|
|
elmJsonVisitor : Maybe { elmJsonKey : Rule.ElmJsonKey, project : Elm.Project.Project } -> ProjectContext -> ( List nothing, ProjectContext )
|
2020-02-16 21:03:49 +03:00
|
|
|
elmJsonVisitor maybeProject projectContext =
|
|
|
|
case maybeProject of
|
|
|
|
Just { elmJsonKey, project } ->
|
|
|
|
let
|
|
|
|
directProjectDependencies : Set String
|
|
|
|
directProjectDependencies =
|
|
|
|
case project of
|
|
|
|
Elm.Project.Package { deps } ->
|
|
|
|
deps
|
|
|
|
|> List.map (Tuple.first >> Elm.Package.toString)
|
|
|
|
|> Set.fromList
|
|
|
|
|
|
|
|
Elm.Project.Application { depsDirect } ->
|
|
|
|
depsDirect
|
|
|
|
|> List.map (Tuple.first >> Elm.Package.toString)
|
|
|
|
|> Set.fromList
|
|
|
|
in
|
2020-03-19 01:08:06 +03:00
|
|
|
( []
|
|
|
|
, { projectContext
|
2020-02-16 21:03:49 +03:00
|
|
|
| elmJsonKey = Just elmJsonKey
|
|
|
|
, directProjectDependencies = directProjectDependencies
|
2020-03-19 01:08:06 +03:00
|
|
|
}
|
|
|
|
)
|
2020-02-16 21:03:49 +03:00
|
|
|
|
|
|
|
Nothing ->
|
2020-03-19 01:08:06 +03:00
|
|
|
( [], projectContext )
|
2020-02-16 21:03:49 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- CONTEXT
|
|
|
|
|
|
|
|
|
|
|
|
type alias ProjectContext =
|
|
|
|
{ elmJsonKey : Maybe Rule.ElmJsonKey
|
|
|
|
, licenses : Dict String String
|
|
|
|
, directProjectDependencies : Set String
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-03-19 19:55:44 +03:00
|
|
|
initialProjectContext : ProjectContext
|
|
|
|
initialProjectContext =
|
2020-02-16 21:03:49 +03:00
|
|
|
{ elmJsonKey = Nothing
|
|
|
|
, licenses = Dict.empty
|
|
|
|
, directProjectDependencies = Set.empty
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- FINAL EVALUATION
|
|
|
|
|
|
|
|
|
2020-03-25 20:02:37 +03:00
|
|
|
finalEvaluationForProject : Configuration -> ProjectContext -> List (Error {})
|
2020-02-16 21:03:49 +03:00
|
|
|
finalEvaluationForProject configuration projectContext =
|
|
|
|
case projectContext.elmJsonKey of
|
|
|
|
Just elmJsonKey ->
|
|
|
|
let
|
|
|
|
allowed : Set String
|
|
|
|
allowed =
|
|
|
|
Set.fromList configuration.allowed
|
|
|
|
|
|
|
|
forbidden : Set String
|
|
|
|
forbidden =
|
|
|
|
Set.fromList configuration.forbidden
|
|
|
|
|
|
|
|
unknownOrForbiddenLicenses : Dict String String
|
|
|
|
unknownOrForbiddenLicenses =
|
|
|
|
projectContext.licenses
|
|
|
|
|> Dict.filter (\_ license -> not <| Set.member license allowed)
|
|
|
|
in
|
|
|
|
unknownOrForbiddenLicenses
|
|
|
|
|> Dict.toList
|
|
|
|
|> List.map
|
|
|
|
(\( name, license ) ->
|
|
|
|
if Set.member license forbidden then
|
|
|
|
Rule.errorForElmJson elmJsonKey
|
|
|
|
(\elmJson ->
|
|
|
|
{ message = "Forbidden license `" ++ license ++ "` for dependency `" ++ name ++ "`"
|
|
|
|
, details = [ "This license has been marked as forbidden and you should therefore not use this package." ]
|
|
|
|
, range = findPackageNameInElmJson name elmJson
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
else
|
|
|
|
Rule.errorForElmJson elmJsonKey
|
|
|
|
(\elmJson ->
|
|
|
|
{ message = "Unknown license `" ++ license ++ "` for dependency `" ++ name ++ "`"
|
|
|
|
, details =
|
|
|
|
[ "Talk to your legal team and see if this license is allowed. If it is allowed, add it to the list of allowed licenses. Otherwise, add it to the list of forbidden licenses and remove this dependency."
|
|
|
|
, "More info about licenses at https://spdx.org/licenses."
|
|
|
|
]
|
|
|
|
, range = findPackageNameInElmJson name elmJson
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
Nothing ->
|
|
|
|
[]
|
|
|
|
|
|
|
|
|
|
|
|
findPackageNameInElmJson : String -> String -> Range
|
|
|
|
findPackageNameInElmJson packageName elmJson =
|
|
|
|
elmJson
|
|
|
|
|> String.lines
|
|
|
|
|> List.indexedMap Tuple.pair
|
|
|
|
|> List.filterMap
|
|
|
|
(\( row, line ) ->
|
|
|
|
case String.indexes ("\"" ++ packageName ++ "\"") line of
|
|
|
|
[] ->
|
|
|
|
Nothing
|
|
|
|
|
|
|
|
column :: _ ->
|
|
|
|
Just
|
|
|
|
{ start =
|
|
|
|
{ row = row + 1
|
|
|
|
, column = column + 2
|
|
|
|
}
|
|
|
|
, end =
|
|
|
|
{ row = row + 1
|
|
|
|
, column = column + String.length packageName + 2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|> List.head
|
|
|
|
|> Maybe.withDefault { start = { row = 1, column = 1 }, end = { row = 10000, column = 1 } }
|