mirror of
https://github.com/jfmengels/elm-review.git
synced 2024-12-18 07:01:48 +03:00
Add NoInvalidLicense
This commit is contained in:
parent
c652a24d48
commit
382a6c8604
227
src/NoInvalidLicense.elm
Normal file
227
src/NoInvalidLicense.elm
Normal 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 } }
|
133
tests/NoInvalidLicenseTest.elm
Normal file
133
tests/NoInvalidLicenseTest.elm
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
Loading…
Reference in New Issue
Block a user