Skip to content

Commit 686b661

Browse files
authored
fix: reject exported_concepts entries with no definition (#317)
Export validation moves from process_required_modules into parse_plain_source, so an undefined export fails --dry-run on the exporting module instead of surfacing in a downstream consumer. Two silent cases now fail too: exporting a default concept (the old check never matched) and re-exporting a concept inherited via requires.
1 parent 8dd06ad commit 686b661

14 files changed

Lines changed: 191 additions & 30 deletions

‎plain_file.py‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -615,15 +615,45 @@ def parse_plain_source( # noqa: C901
615615
f"Plain syntax error: Syntax error at line {nested_acceptance_test_heading.line_number}: {plain_spec.ACCEPTANCE_TEST_HEADING} heading should be nested under specific functional spec."
616616
)
617617

618+
defined_concepts = set[str]()
618619
if plain_source[plain_spec.DEFINITIONS] is not None:
619620
with PlainRenderer() as renderer:
620621
for token in plain_source[plain_spec.DEFINITIONS].children:
621622
rendered_token = renderer.render(token)
622623
new_concepts, _ = concept_utils.extract_concepts_from_definition(rendered_token)
624+
defined_concepts.update(new_concepts)
623625
for concept in new_concepts:
624626
if concept in required_concepts:
625627
required_concepts.remove(concept)
626628

629+
if EXPORTED_CONCEPTS_DIRECTIVE in plain_source_obj.metadata:
630+
module_name = modules_trace[-1] if modules_trace else None
631+
subject = f"Module '{module_name}'" if module_name else "Module"
632+
633+
for entry in plain_source_obj.metadata[EXPORTED_CONCEPTS_DIRECTIVE]:
634+
exported_concepts, errors = concept_utils.extract_concepts_from_definition(entry)
635+
if errors:
636+
raise PlainSyntaxError(
637+
f"Plain syntax error: {subject} exports an invalid concept (`{entry}`). "
638+
f"Expected a concept token, e.g. :ConceptName:."
639+
)
640+
641+
for exported_concept in exported_concepts:
642+
if exported_concept in concept_utils.DEFAULT_CONCEPTS:
643+
raise PlainSyntaxError(
644+
f"Plain syntax error: {subject} cannot export default concept {exported_concept}. "
645+
f"Only user-defined concepts can be exported."
646+
)
647+
648+
if exported_concept not in defined_concepts:
649+
message = f"Plain syntax error: {subject} exports undefined concept {exported_concept}."
650+
if REQUIRES_DIRECTIVE in plain_source_obj.metadata:
651+
message += (
652+
" An exported concept must be defined in this module's definitions or in an "
653+
"imported module; a concept inherited through requires cannot be re-exported."
654+
)
655+
raise PlainSyntaxError(message)
656+
627657
required_modules = []
628658
if REQUIRES_DIRECTIVE in plain_source_obj.metadata:
629659
required_modules = plain_source_obj.metadata[REQUIRES_DIRECTIVE]
@@ -710,15 +740,7 @@ def process_required_modules(
710740
if EXPORTED_CONCEPTS_DIRECTIVE in plain_file_parse_result.plain_source_obj.metadata:
711741
exported_concepts = list[str]()
712742
for concept in plain_file_parse_result.plain_source_obj.metadata[EXPORTED_CONCEPTS_DIRECTIVE]:
713-
if concept in concept_utils.DEFAULT_CONCEPTS:
714-
raise PlainSyntaxError(
715-
f"Plain syntax error: Default concept cannot be exported: {concept}. Only user-defined concepts can be exported."
716-
)
717-
718-
if isinstance(concept, str):
719-
exported_concepts.extend(concept_utils.extract_concepts_from_definition(concept)[0])
720-
else:
721-
raise PlainSyntaxError(f"Plain syntax error: Invalid exported concept: {concept}.")
743+
exported_concepts.extend(concept_utils.extract_concepts_from_definition(concept)[0])
722744

723745
with PlainRenderer() as renderer:
724746
for exported_concept in exported_concepts:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
***definitions***
2+
- :A: relates to :B:.
3+
- :B: relates to :A:.
4+
5+
***implementation reqs***
6+
- The :A: concept must be implemented.
7+
8+
***functional specs***
9+
- Implement a wrapper for the :A: concept.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
exported_concepts:
3+
- :UnitTests:
4+
---
5+
6+
***definitions***
7+
- :NewConcept: is a new concept.
8+
9+
***implementation reqs***
10+
- The :NewConcept: must be implemented.
11+
12+
***functional specs***
13+
- Implement a wrapper for the :NewConcept: concept.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
import:
3+
- exported_concepts_base
4+
exported_concepts:
5+
- :Concept:
6+
---
7+
8+
***implementation reqs***
9+
- The :Concept: concept must be implemented.
10+
11+
***functional specs***
12+
- Implement a wrapper for the :Concept: concept.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
requires:
3+
- exported_concepts
4+
exported_concepts:
5+
- :Concept:
6+
---
7+
8+
***definitions***
9+
- :DependantConcept: depends on the :Concept: concept.
10+
11+
***implementation reqs***
12+
- The :DependantConcept: concept must be implemented.
13+
14+
***functional specs***
15+
- Implement a wrapper for the :DependantConcept: concept.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
exported_concepts:
3+
- not a concept at all
4+
---
5+
6+
***definitions***
7+
- :A: is a concept.
8+
9+
***implementation reqs***
10+
- The :A: concept must be implemented.
11+
12+
***functional specs***
13+
- Implement a wrapper for the :A: concept.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
exported_concepts:
3+
- :NewConcept:
4+
- :Concept:
5+
---
6+
7+
***definitions***
8+
- :NewConcept: is a new concept.
9+
10+
***implementation reqs***
11+
- The :NewConcept: must be implemented.
12+
13+
***functional specs***
14+
- Implement a wrapper for the :NewConcept: concept.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
requires:
3+
- exported_concepts_missing_definition
4+
---
5+
6+
***definitions***
7+
- :AnotherConcept: is a new concept.
8+
9+
***implementation reqs***
10+
- :AnotherConcept: must be implemented.
11+
12+
***functional specs***
13+
- Implement a wrapper for the :AnotherConcept: concept.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
exported_concepts:
3+
- :NewConcept:, :OtherConcept:
4+
---
5+
6+
***definitions***
7+
- :NewConcept: is a new concept.
8+
- :OtherConcept: is another concept.
9+
10+
***implementation reqs***
11+
- The :NewConcept: and :OtherConcept: concepts must be implemented.
12+
13+
***functional specs***
14+
- Implement a wrapper for the :NewConcept: and :OtherConcept: concepts.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
exported_concepts:
3+
- :NewConcept:, :OtherConcept:
4+
---
5+
6+
***definitions***
7+
- :NewConcept: is a new concept.
8+
9+
***implementation reqs***
10+
- The :NewConcept: concept must be implemented.
11+
12+
***functional specs***
13+
- Implement a wrapper for the :NewConcept: concept.

0 commit comments

Comments
 (0)