Add NoInvalidLicense

This commit is contained in:
Jeroen Engels 2020-02-16 19:03:49 +01:00
parent c652a24d48
commit 382a6c8604
2 changed files with 360 additions and 0 deletions

227
src/NoInvalidLicense.elm Normal file
View File

@ -0,0 +1,227 @@
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.ModuleName exposing (ModuleName)
import Elm.Syntax.Node exposing (Node)
import Elm.Syntax.Range exposing (Range)
import Review.Project
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 =
Rule.newProjectRuleSchema "NoInvalidLicense"
{ moduleVisitorSchema = moduleVisitorSchema
, initProjectContext = initProjectContext
, fromProjectToModule = fromProjectToModule
, fromModuleToProject = fromModuleToProject
, foldProjectContexts = foldProjectContexts
}
|> Rule.withProjectElmJsonVisitor elmJsonVisitor
|> Rule.withProjectDependenciesVisitor dependenciesVisitor
|> Rule.withFinalProjectEvaluation (finalEvaluationForProject configuration)
|> Rule.fromProjectRuleSchema
type alias Configuration =
{ allowed : List String
, forbidden : List String
}
moduleVisitorSchema : Rule.ModuleRuleSchema {} ModuleContext -> Rule.ModuleRuleSchema { hasAtLeastOneVisitor : () } ModuleContext
moduleVisitorSchema schema =
schema
|> Rule.withModuleDefinitionVisitor (\_ context -> ( [], context ))
dependenciesVisitor : Dict String Review.Project.Dependency -> ProjectContext -> ProjectContext
dependenciesVisitor dependencies projectContext =
let
licenses : Dict String String
licenses =
dependencies
|> Dict.toList
|> List.filterMap
(\( packageName, dependency ) ->
case dependency.elmJson of
Elm.Project.Package { license } ->
Just ( packageName, Elm.License.toString license )
Elm.Project.Application _ ->
Nothing
)
|> Dict.fromList
in
{ projectContext | licenses = licenses }
-- PROJECT VISITORS
elmJsonVisitor : Maybe { elmJsonKey : Rule.ElmJsonKey, project : Elm.Project.Project } -> ProjectContext -> ProjectContext
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
{ projectContext
| elmJsonKey = Just elmJsonKey
, directProjectDependencies = directProjectDependencies
}
Nothing ->
projectContext
-- CONTEXT
type alias ProjectContext =
{ elmJsonKey : Maybe Rule.ElmJsonKey
, licenses : Dict String String
, directProjectDependencies : Set String
}
type alias ModuleContext =
ProjectContext
initProjectContext : ProjectContext
initProjectContext =
{ elmJsonKey = Nothing
, licenses = Dict.empty
, directProjectDependencies = Set.empty
}
fromProjectToModule : Rule.FileKey -> Node ModuleName -> ProjectContext -> ModuleContext
fromProjectToModule _ _ projectContext =
projectContext
fromModuleToProject : Rule.FileKey -> Node ModuleName -> ModuleContext -> ProjectContext
fromModuleToProject _ _ moduleContext =
moduleContext
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
foldProjectContexts _ previousContext =
previousContext
-- FINAL EVALUATION
finalEvaluationForProject : Configuration -> ProjectContext -> List Error
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 } }

View File

@ -0,0 +1,133 @@
module NoInvalidLicenseTest exposing (all)
import Elm.Project
import Elm.Version
import Json.Decode as Decode
import NoInvalidLicense exposing (rule)
import Review.Project as Project exposing (Project)
import Review.Test
import Test exposing (Test, describe, test)
createProject : String -> Project
createProject license =
Project.new
|> Project.withElmJson (createElmJson applicationElmJson)
|> Project.withDependency (dependency license)
createElmJson : String -> { path : String, raw : String, project : Elm.Project.Project }
createElmJson rawElmJson =
case Decode.decodeString Elm.Project.decoder rawElmJson of
Ok elmJson ->
{ path = "elm.json"
, raw = rawElmJson
, project = elmJson
}
Err _ ->
createElmJson rawElmJson
applicationElmJson : String
applicationElmJson =
"""
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/core": "1.0.0",
"author/dependency": "1.0.0"
},
"indirect": {}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}"""
dependency : String -> Project.Dependency
dependency license =
{ name = "author/dependency"
, version = Elm.Version.one
, modules =
[ { name = "Foo"
, comment = ""
, unions = []
, aliases = []
, values = []
, binops = []
}
]
, elmJson = .project <| createElmJson ("""
{
"type": "package",
"name": "author/dependency",
"summary": "Summary",
"license": \"""" ++ license ++ """",
"version": "1.0.0",
"exposed-modules": [
"Foo"
],
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {
"elm/core": "1.0.0 <= v < 2.0.0"
},
"test-dependencies": {}
}""")
}
sourceCode : String
sourceCode =
"""
module A exposing (a)
a = 1
"""
all : Test
all =
describe "NoInvalidLicense"
[ test "should not report anything if there is no `elm.json` file" <|
\() ->
sourceCode
|> Review.Test.run (rule { allowed = [], forbidden = [] })
|> Review.Test.expectNoErrors
, test "should not report anything if all dependencies have a license that is allowed" <|
\() ->
sourceCode
|> Review.Test.runWithProjectData (createProject "MIT") (rule { allowed = [ "MIT" ], forbidden = [] })
|> Review.Test.expectNoErrors
, test "should report an error if a dependency has an unknown license" <|
\() ->
sourceCode
|> Review.Test.runWithProjectData (createProject "BSD-3-Clause") (rule { allowed = [ "MIT" ], forbidden = [] })
|> Review.Test.expectErrorsForElmJson
[ Review.Test.error
{ message = "Unknown license `BSD-3-Clause` for dependency `author/dependency`"
, 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."
]
, under = "author/dependency"
}
]
, test "should report an error if a dependency has a forbidden license" <|
\() ->
sourceCode
|> Review.Test.runWithProjectData (createProject "BSD-3-Clause") (rule { allowed = [ "MIT" ], forbidden = [ "BSD-3-Clause" ] })
|> Review.Test.expectErrorsForElmJson
[ Review.Test.error
{ message = "Forbidden license `BSD-3-Clause` for dependency `author/dependency`"
, details = [ "This license has been marked as forbidden and you should therefore not use this package." ]
, under = "author/dependency"
}
]
]