mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-26 22:36:01 +03:00
Disallow duplicate declaration names (#2015)
This commit is contained in:
parent
f6d5ddfaa2
commit
b452313b4c
@ -41,6 +41,10 @@ makeDeclType ''App
|
||||
-- | Collection of domain types that are standard for Wasp, that define what the Wasp language looks like.
|
||||
-- These are injected this way instead of hardcoding them into the Analyzer in order to make it
|
||||
-- easier to modify and maintain the Wasp compiler/language.
|
||||
|
||||
-- *** MAKE SURE TO UPDATE: The `validateUniqueDeclarationNames` function in the `Wasp.AppSpec.Valid` module
|
||||
-- when you add a new declaration type here, we need to check for duplicate declaration names
|
||||
-- for the new declaration type.
|
||||
stdTypes :: TD.TypeDefinitions
|
||||
stdTypes =
|
||||
TD.addDeclType @App $
|
||||
|
@ -30,7 +30,7 @@ import qualified Wasp.AppSpec.App.Client as Client
|
||||
import qualified Wasp.AppSpec.App.Db as AS.Db
|
||||
import qualified Wasp.AppSpec.App.EmailSender as AS.EmailSender
|
||||
import qualified Wasp.AppSpec.App.Wasp as Wasp
|
||||
import Wasp.AppSpec.Core.Decl (takeDecls)
|
||||
import Wasp.AppSpec.Core.Decl (IsDecl, takeDecls)
|
||||
import qualified Wasp.AppSpec.Crud as AS.Crud
|
||||
import qualified Wasp.AppSpec.Entity as Entity
|
||||
import qualified Wasp.AppSpec.Entity.Field as Entity.Field
|
||||
@ -43,7 +43,7 @@ import qualified Wasp.Node.Version as V
|
||||
import qualified Wasp.Psl.Ast.Model as PslModel
|
||||
import qualified Wasp.SemanticVersion as SV
|
||||
import qualified Wasp.SemanticVersion.VersionBound as SVB
|
||||
import Wasp.Util (isCapitalized)
|
||||
import Wasp.Util (findDuplicateElems, isCapitalized)
|
||||
import qualified Wasp.Version as WV
|
||||
|
||||
data ValidationError = GenericValidationError !String | GenericValidationWarning !String
|
||||
@ -78,6 +78,7 @@ validateAppSpec spec =
|
||||
validateApiRoutesAreUnique spec,
|
||||
validateApiNamespacePathsAreUnique spec,
|
||||
validateCrudOperations spec,
|
||||
validateUniqueDeclarationNames spec,
|
||||
validateDeclarationNames spec,
|
||||
validatePrismaOptions spec,
|
||||
validateWebAppBaseDir spec,
|
||||
@ -267,6 +268,39 @@ validateCrudOperations spec =
|
||||
maybeIdBlockAttribute = Entity.getIdBlockAttribute entity
|
||||
(entityName, entity) = AS.resolveRef spec (AS.Crud.entity crud)
|
||||
|
||||
{- ORMOLU_DISABLE -}
|
||||
-- *** MAKE SURE TO UPDATE: Unit tests in `AppSpec.ValidTest` module named "duplicate declarations validation"
|
||||
-- to include the new declaration type.
|
||||
{- ORMOLU_ENABLE -}
|
||||
validateUniqueDeclarationNames :: AppSpec -> [ValidationError]
|
||||
validateUniqueDeclarationNames spec =
|
||||
concat
|
||||
[ checkIfDeclarationsAreUnique "page" (AS.getPages spec),
|
||||
checkIfDeclarationsAreUnique "route" (AS.getRoutes spec),
|
||||
checkIfDeclarationsAreUnique "action" (AS.getActions spec),
|
||||
checkIfDeclarationsAreUnique "query" (AS.getQueries spec),
|
||||
checkIfDeclarationsAreUnique "api" (AS.getApis spec),
|
||||
checkIfDeclarationsAreUnique "apiNamespace" (AS.getApiNamespaces spec),
|
||||
checkIfDeclarationsAreUnique "crud" (AS.getCruds spec),
|
||||
checkIfDeclarationsAreUnique "entity" (AS.getEntities spec),
|
||||
checkIfDeclarationsAreUnique "job" (AS.getJobs spec)
|
||||
]
|
||||
where
|
||||
checkIfDeclarationsAreUnique :: IsDecl a => String -> [(String, a)] -> [ValidationError]
|
||||
checkIfDeclarationsAreUnique declTypeName decls = case duplicateDeclNames of
|
||||
[] -> []
|
||||
(firstDuplicateDeclName : _) ->
|
||||
[ GenericValidationError $
|
||||
"There are duplicate "
|
||||
++ declTypeName
|
||||
++ " declarations with name '"
|
||||
++ firstDuplicateDeclName
|
||||
++ "'."
|
||||
]
|
||||
where
|
||||
duplicateDeclNames :: [String]
|
||||
duplicateDeclNames = findDuplicateElems $ map fst decls
|
||||
|
||||
validateDeclarationNames :: AppSpec -> [ValidationError]
|
||||
validateDeclarationNames spec =
|
||||
concat
|
||||
|
@ -39,6 +39,7 @@ module Wasp.Util
|
||||
textToLazyBS,
|
||||
trim,
|
||||
secondsToMicroSeconds,
|
||||
findDuplicateElems,
|
||||
)
|
||||
where
|
||||
|
||||
@ -51,7 +52,7 @@ import qualified Data.ByteString.Lazy as BSL
|
||||
import qualified Data.ByteString.UTF8 as BSU
|
||||
import Data.Char (isSpace, isUpper, toLower, toUpper)
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import Data.List (intercalate)
|
||||
import Data.List (group, intercalate, sort)
|
||||
import Data.List.Split (splitOn, wordsBy)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
@ -266,3 +267,6 @@ textToLazyBS = TLE.encodeUtf8 . TL.fromStrict
|
||||
|
||||
secondsToMicroSeconds :: Int -> Int
|
||||
secondsToMicroSeconds = (* 1000000)
|
||||
|
||||
findDuplicateElems :: Ord a => [a] -> [a]
|
||||
findDuplicateElems = map head . filter ((> 1) . length) . group . sort
|
||||
|
@ -8,18 +8,25 @@ import Fixtures (systemSPRoot)
|
||||
import qualified StrongPath as SP
|
||||
import Test.Tasty.Hspec
|
||||
import qualified Wasp.AppSpec as AS
|
||||
import qualified Wasp.AppSpec.Action as AS.Action
|
||||
import qualified Wasp.AppSpec.Api as AS.Api
|
||||
import qualified Wasp.AppSpec.ApiNamespace as AS.ApiNamespace
|
||||
import qualified Wasp.AppSpec.App as AS.App
|
||||
import qualified Wasp.AppSpec.App.Auth as AS.Auth
|
||||
import qualified Wasp.AppSpec.App.Auth.EmailVerification as AS.Auth.EmailVerification
|
||||
import qualified Wasp.AppSpec.App.Auth.PasswordReset as AS.Auth.PasswordReset
|
||||
import qualified Wasp.AppSpec.App.Db as AS.Db
|
||||
import qualified Wasp.AppSpec.App.EmailSender as AS.EmailSender
|
||||
import qualified Wasp.AppSpec.App.Wasp as AS.Wasp
|
||||
import qualified Wasp.AppSpec.Core.Decl as AS.Decl
|
||||
import qualified Wasp.AppSpec.Core.Ref as AS.Core.Ref
|
||||
import qualified Wasp.AppSpec.Crud as AS.Crud
|
||||
import qualified Wasp.AppSpec.Entity as AS.Entity
|
||||
import qualified Wasp.AppSpec.ExtImport as AS.ExtImport
|
||||
import qualified Wasp.AppSpec.Job as AS.Job
|
||||
import qualified Wasp.AppSpec.PackageJson as AS.PJS
|
||||
import qualified Wasp.AppSpec.Page as AS.Page
|
||||
import qualified Wasp.AppSpec.Query as AS.Query
|
||||
import qualified Wasp.AppSpec.Route as AS.Route
|
||||
import qualified Wasp.AppSpec.Valid as ASV
|
||||
import qualified Wasp.Psl.Ast.Model as PslM
|
||||
@ -323,6 +330,56 @@ spec_AppSpecValid = do
|
||||
it "returns an error if the Dummy email sender is used when building the app" $ do
|
||||
ASV.validateAppSpec (makeSpec (Just dummyEmailSender) True)
|
||||
`shouldBe` [ASV.GenericValidationError "app.emailSender must not be set to Dummy when building for production."]
|
||||
|
||||
describe "duplicate declarations validation" $ do
|
||||
-- Page
|
||||
let pageDecl = makeBasicPageDecl "testPage"
|
||||
|
||||
-- Route
|
||||
let routeDecl = makeBasicRouteDecl "testRoute" "testPage"
|
||||
|
||||
-- Action
|
||||
let actionDecl = makeBasicActionDecl "testAction"
|
||||
|
||||
-- Query
|
||||
let queryDecl = makeBasicQueryDecl "testQuery"
|
||||
|
||||
-- Api
|
||||
let apiDecl1 = makeBasicApiDecl "testApi" (AS.Api.GET, "/foo/bar")
|
||||
-- Using a different route not to trigger duplicate route errors
|
||||
let apiDecl2 = makeBasicApiDecl "testApi" (AS.Api.GET, "/different/route")
|
||||
|
||||
-- ApiNamespace
|
||||
let apiNamespaceDecl1 = makeBasicApiNamespaceDecl "testApiNamespace" "/foo"
|
||||
-- Using a different path not to trigger duplicate route errors
|
||||
let apiNamespaceDecl2 = makeBasicApiNamespaceDecl "testApiNamespace" "/different/path"
|
||||
|
||||
-- Crud
|
||||
let crudDecl = makeBasicCrudDecl "testCrud" "TestEntity"
|
||||
|
||||
-- Entity
|
||||
let entityDecl = makeBasicEntityDecl "TestEntity"
|
||||
|
||||
-- Job
|
||||
let jobDecl = makeBasicJobDecl "testJob"
|
||||
|
||||
let testDuplicateDecls decls declTypeName expectedErrorMessage = it ("returns an error if there are duplicate " ++ declTypeName ++ " declarations") $ do
|
||||
ASV.validateAppSpec
|
||||
( basicAppSpec
|
||||
{ AS.decls = decls
|
||||
}
|
||||
)
|
||||
`shouldBe` [ASV.GenericValidationError expectedErrorMessage]
|
||||
|
||||
testDuplicateDecls [basicAppDecl, pageDecl, pageDecl] "page" "There are duplicate page declarations with name 'testPage'."
|
||||
testDuplicateDecls [basicAppDecl, routeDecl, routeDecl] "route" "There are duplicate route declarations with name 'testRoute'."
|
||||
testDuplicateDecls [basicAppDecl, actionDecl, actionDecl] "action" "There are duplicate action declarations with name 'testAction'."
|
||||
testDuplicateDecls [basicAppDecl, queryDecl, queryDecl] "query" "There are duplicate query declarations with name 'testQuery'."
|
||||
testDuplicateDecls [basicAppDecl, apiDecl1, apiDecl2] "api" "There are duplicate api declarations with name 'testApi'."
|
||||
testDuplicateDecls [basicAppDecl, apiNamespaceDecl1, apiNamespaceDecl2] "apiNamespace" "There are duplicate apiNamespace declarations with name 'testApiNamespace'."
|
||||
testDuplicateDecls [basicAppDecl, crudDecl, crudDecl, entityDecl] "crud" "There are duplicate crud declarations with name 'testCrud'."
|
||||
testDuplicateDecls [basicAppDecl, entityDecl, entityDecl] "entity" "There are duplicate entity declarations with name 'TestEntity'."
|
||||
testDuplicateDecls [basicAppDecl, jobDecl, jobDecl] "job" "There are duplicate job declarations with name 'testJob'."
|
||||
where
|
||||
makeIdField name typ =
|
||||
PslM.Field
|
||||
@ -345,7 +402,13 @@ spec_AppSpecValid = do
|
||||
{ AS.Wasp.version = "^" ++ show WV.waspVersion
|
||||
},
|
||||
AS.App.title = "Test App",
|
||||
AS.App.db = Nothing,
|
||||
AS.App.db =
|
||||
Just $
|
||||
AS.Db.Db
|
||||
{ AS.Db.system = Just AS.Db.PostgreSQL,
|
||||
AS.Db.seeds = Nothing,
|
||||
AS.Db.prisma = Nothing
|
||||
},
|
||||
AS.App.server = Nothing,
|
||||
AS.App.client = Nothing,
|
||||
AS.App.auth = Nothing,
|
||||
@ -389,10 +452,104 @@ spec_AppSpecValid = do
|
||||
|
||||
basicPageName = "TestPage"
|
||||
|
||||
basicPageDecl = AS.Decl.makeDecl basicPageName basicPage
|
||||
|
||||
basicRoute = AS.Route.Route {AS.Route.to = AS.Core.Ref.Ref basicPageName, AS.Route.path = "/test"}
|
||||
basicPageDecl = makeBasicPageDecl basicPageName
|
||||
|
||||
basicRouteName = "TestRoute"
|
||||
|
||||
basicRouteDecl = AS.Decl.makeDecl basicRouteName basicRoute
|
||||
basicRouteDecl = makeBasicRouteDecl basicRouteName basicPageName
|
||||
|
||||
makeBasicPageDecl name =
|
||||
AS.Decl.makeDecl
|
||||
name
|
||||
AS.Page.Page
|
||||
{ AS.Page.component = dummyExtImport,
|
||||
AS.Page.authRequired = Nothing
|
||||
}
|
||||
|
||||
makeBasicRouteDecl name pageName =
|
||||
AS.Decl.makeDecl
|
||||
name
|
||||
AS.Route.Route {AS.Route.to = AS.Core.Ref.Ref pageName, AS.Route.path = "/test"}
|
||||
|
||||
makeBasicActionDecl name =
|
||||
AS.Decl.makeDecl
|
||||
name
|
||||
AS.Action.Action
|
||||
{ AS.Action.auth = Nothing,
|
||||
AS.Action.entities = Nothing,
|
||||
AS.Action.fn = dummyExtImport
|
||||
}
|
||||
|
||||
makeBasicQueryDecl name =
|
||||
AS.Decl.makeDecl
|
||||
name
|
||||
AS.Query.Query
|
||||
{ AS.Query.auth = Nothing,
|
||||
AS.Query.entities = Nothing,
|
||||
AS.Query.fn = dummyExtImport
|
||||
}
|
||||
|
||||
makeBasicApiDecl name route =
|
||||
AS.Decl.makeDecl
|
||||
name
|
||||
AS.Api.Api
|
||||
{ AS.Api.fn = dummyExtImport,
|
||||
AS.Api.middlewareConfigFn = Nothing,
|
||||
AS.Api.entities = Nothing,
|
||||
AS.Api.httpRoute = route,
|
||||
AS.Api.auth = Nothing
|
||||
}
|
||||
|
||||
makeBasicApiNamespaceDecl name path =
|
||||
AS.Decl.makeDecl
|
||||
name
|
||||
AS.ApiNamespace.ApiNamespace
|
||||
{ AS.ApiNamespace.middlewareConfigFn = dummyExtImport,
|
||||
AS.ApiNamespace.path = path
|
||||
}
|
||||
|
||||
makeBasicCrudDecl name entityName =
|
||||
AS.Decl.makeDecl
|
||||
name
|
||||
AS.Crud.Crud
|
||||
{ -- CRUD references testEntity, which is defined below,
|
||||
-- it needs to be included in the test declarations.
|
||||
AS.Crud.entity = AS.Core.Ref.Ref entityName,
|
||||
AS.Crud.operations =
|
||||
AS.Crud.CrudOperations
|
||||
{ AS.Crud.get =
|
||||
Just $
|
||||
AS.Crud.CrudOperationOptions
|
||||
{ AS.Crud.isPublic = Nothing,
|
||||
AS.Crud.overrideFn = Nothing
|
||||
},
|
||||
AS.Crud.getAll = Nothing,
|
||||
AS.Crud.create = Nothing,
|
||||
AS.Crud.update = Nothing,
|
||||
AS.Crud.delete = Nothing
|
||||
}
|
||||
}
|
||||
|
||||
makeBasicEntityDecl name =
|
||||
AS.Decl.makeDecl
|
||||
name
|
||||
(AS.Entity.makeEntity $ PslM.Body [PslM.ElementField $ makeIdField "id" PslM.String])
|
||||
|
||||
makeBasicJobDecl name =
|
||||
AS.Decl.makeDecl
|
||||
name
|
||||
AS.Job.Job
|
||||
{ AS.Job.executor = AS.Job.PgBoss,
|
||||
AS.Job.perform =
|
||||
AS.Job.Perform
|
||||
{ AS.Job.fn = dummyExtImport,
|
||||
AS.Job.executorOptions = Nothing
|
||||
},
|
||||
AS.Job.schedule = Nothing,
|
||||
AS.Job.entities = Nothing
|
||||
}
|
||||
|
||||
dummyExtImport =
|
||||
AS.ExtImport.ExtImport
|
||||
(AS.ExtImport.ExtImportModule "Dummy")
|
||||
(fromJust $ SP.parseRelFileP "dummy/File")
|
||||
|
@ -149,3 +149,14 @@ spec_checksum :: Spec
|
||||
spec_checksum = do
|
||||
it "Correctly calculates checksum of string" $ do
|
||||
checksumFromString "test" `shouldBe` hexFromString "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
|
||||
|
||||
spec_findDuplicateElems :: Spec
|
||||
spec_findDuplicateElems = do
|
||||
it "Finds duplicate elements in a list" $ do
|
||||
findDuplicateElems ([1, 2, 3, 4, 5, 1, 2, 3, 4, 5] :: [Int]) `shouldBe` [1, 2, 3, 4, 5]
|
||||
|
||||
it "Returns empty list if there are no duplicates" $ do
|
||||
findDuplicateElems ([1, 2, 3, 4, 5] :: [Int]) `shouldBe` []
|
||||
|
||||
it "Returns empty list for empty list" $ do
|
||||
findDuplicateElems ([] :: [Int]) `shouldBe` []
|
||||
|
Loading…
Reference in New Issue
Block a user