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. -- | 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 -- 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. -- 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.TypeDefinitions
stdTypes = stdTypes =
TD.addDeclType @App $ 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.Db as AS.Db
import qualified Wasp.AppSpec.App.EmailSender as AS.EmailSender import qualified Wasp.AppSpec.App.EmailSender as AS.EmailSender
import qualified Wasp.AppSpec.App.Wasp as Wasp 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.Crud as AS.Crud
import qualified Wasp.AppSpec.Entity as Entity import qualified Wasp.AppSpec.Entity as Entity
import qualified Wasp.AppSpec.Entity.Field as Entity.Field 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.Psl.Ast.Model as PslModel
import qualified Wasp.SemanticVersion as SV import qualified Wasp.SemanticVersion as SV
import qualified Wasp.SemanticVersion.VersionBound as SVB import qualified Wasp.SemanticVersion.VersionBound as SVB
import Wasp.Util (isCapitalized) import Wasp.Util (findDuplicateElems, isCapitalized)
import qualified Wasp.Version as WV import qualified Wasp.Version as WV
data ValidationError = GenericValidationError !String | GenericValidationWarning !String data ValidationError = GenericValidationError !String | GenericValidationWarning !String
@ -78,6 +78,7 @@ validateAppSpec spec =
validateApiRoutesAreUnique spec, validateApiRoutesAreUnique spec,
validateApiNamespacePathsAreUnique spec, validateApiNamespacePathsAreUnique spec,
validateCrudOperations spec, validateCrudOperations spec,
validateUniqueDeclarationNames spec,
validateDeclarationNames spec, validateDeclarationNames spec,
validatePrismaOptions spec, validatePrismaOptions spec,
validateWebAppBaseDir spec, validateWebAppBaseDir spec,
@ -267,6 +268,39 @@ validateCrudOperations spec =
maybeIdBlockAttribute = Entity.getIdBlockAttribute entity maybeIdBlockAttribute = Entity.getIdBlockAttribute entity
(entityName, entity) = AS.resolveRef spec (AS.Crud.entity crud) (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 :: AppSpec -> [ValidationError]
validateDeclarationNames spec = validateDeclarationNames spec =
concat concat

View File

@ -39,6 +39,7 @@ module Wasp.Util
textToLazyBS, textToLazyBS,
trim, trim,
secondsToMicroSeconds, secondsToMicroSeconds,
findDuplicateElems,
) )
where where
@ -51,7 +52,7 @@ import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.UTF8 as BSU import qualified Data.ByteString.UTF8 as BSU
import Data.Char (isSpace, isUpper, toLower, toUpper) import Data.Char (isSpace, isUpper, toLower, toUpper)
import qualified Data.HashMap.Strict as M 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.List.Split (splitOn, wordsBy)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Text (Text) import Data.Text (Text)
@ -266,3 +267,6 @@ textToLazyBS = TLE.encodeUtf8 . TL.fromStrict
secondsToMicroSeconds :: Int -> Int secondsToMicroSeconds :: Int -> Int
secondsToMicroSeconds = (* 1000000) 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 qualified StrongPath as SP
import Test.Tasty.Hspec import Test.Tasty.Hspec
import qualified Wasp.AppSpec as AS 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 as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.Auth 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.EmailVerification as AS.Auth.EmailVerification
import qualified Wasp.AppSpec.App.Auth.PasswordReset as AS.Auth.PasswordReset 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.EmailSender as AS.EmailSender
import qualified Wasp.AppSpec.App.Wasp as AS.Wasp import qualified Wasp.AppSpec.App.Wasp as AS.Wasp
import qualified Wasp.AppSpec.Core.Decl as AS.Decl import qualified Wasp.AppSpec.Core.Decl as AS.Decl
import qualified Wasp.AppSpec.Core.Ref as AS.Core.Ref 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.Entity as AS.Entity
import qualified Wasp.AppSpec.ExtImport as AS.ExtImport 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.PackageJson as AS.PJS
import qualified Wasp.AppSpec.Page as AS.Page 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.Route as AS.Route
import qualified Wasp.AppSpec.Valid as ASV import qualified Wasp.AppSpec.Valid as ASV
import qualified Wasp.Psl.Ast.Model as PslM 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 it "returns an error if the Dummy email sender is used when building the app" $ do
ASV.validateAppSpec (makeSpec (Just dummyEmailSender) True) ASV.validateAppSpec (makeSpec (Just dummyEmailSender) True)
`shouldBe` [ASV.GenericValidationError "app.emailSender must not be set to Dummy when building for production."] `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 where
makeIdField name typ = makeIdField name typ =
PslM.Field PslM.Field
@ -345,7 +402,13 @@ spec_AppSpecValid = do
{ AS.Wasp.version = "^" ++ show WV.waspVersion { AS.Wasp.version = "^" ++ show WV.waspVersion
}, },
AS.App.title = "Test App", 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.server = Nothing,
AS.App.client = Nothing, AS.App.client = Nothing,
AS.App.auth = Nothing, AS.App.auth = Nothing,
@ -389,10 +452,104 @@ spec_AppSpecValid = do
basicPageName = "TestPage" basicPageName = "TestPage"
basicPageDecl = AS.Decl.makeDecl basicPageName basicPage basicPageDecl = makeBasicPageDecl basicPageName
basicRoute = AS.Route.Route {AS.Route.to = AS.Core.Ref.Ref basicPageName, AS.Route.path = "/test"}
basicRouteName = "TestRoute" 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 spec_checksum = do
it "Correctly calculates checksum of string" $ do it "Correctly calculates checksum of string" $ do
checksumFromString "test" `shouldBe` hexFromString "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" 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` []