Sadly, no. Puppet masquerades as being declarative just like Ansible does. It's very easy to get into a situation where your Puppet class is just a fancy shell script which works the first time and then fails because a file doesn't exist or something. There is nothing to lint for this or to protect against this.
I know Puppet is a mess. What you say is that you spill lot of exec's into a class? You can avoid that, can't you? If you just declare "resources" and connect them via relationships, then it seems pretty "declarative" to me, i.e. there's no sequence of actions in Puppet DSL and when you run puppet it only does what's necessary. While I find it truly "declarative", I still think Puppet and its ecosystem just suck, for lot of other reasons.
I don't think Ansible even tries to hide the fact that it's just imperative sequence of actions. I wonder where the people saying opposite are coming from. It's just retarded imperative language.
There's so many cases where a hacky Exec or five becomes necessary. For example, I am not aware of any Puppet module that does disk partitioning + formatting. Our solution was to write a class with a few Execs to do that. But there's so many edge cases where this can go wrong if it's not truly idempotent. An accidental disk wipe across our fleet would be a disaster, so what did we do? We basically wrapped the class in a big if guard that checks if the disk is already partitioned. That's not idempotent at all of course or declarative - it's just a script.
I can think of another horrifying example. We extract archives on to the machine to a specific target directory, and all files in that directory should have a specific owner and group. So we have a File resource that states the target directory exists which is ordered before the Archive resource. But we need to recursively chown after extraction too because tarballs preserve the original owner and group on extraction. We can't add a File resource with the same path that states the owner and group after extraction because duplicate resources aren't allowed. The only solution in this case is a hacky Exec. And that isn't declarative either because you'd have to check the owner/group of all files recursively to know whether it's in the right state or not. We just have this Exec run if the Archive resource refreshes. What if something else adds a file to this directory with the wrong owner/group? It wouldn't be corrected. Again, not declarative.
And god help you if a machine in the fleet runs out of disk space and resources only partially apply. The state will be completely fucked up and you'll have to manually nuke directories to correct it. Because Puppet only pretends to be declarative.
At a previous job we used beaker tests as part of our pipeline to test puppet modules & environments before we would merge to prod. One of the things this tests is that the module applies and doesn't try to change anything / error out on subsequent runs.
The documentation sucks though imo, every different guide you find will show a completely different way of doing it