Disallow duplicate declaration names (#2015)

This commit is contained in:
Mihovil Ilakovac 2024-05-13 11:01:58 +02:00 committed by GitHub
parent f6d5ddfaa2
commit b452313b4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 218 additions and 8 deletions

View File

@ -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 $

View File

@ -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

View File

@ -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

View File

@ -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")

View 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` []