Clean up not-implemented detection

Move hack_dependencies_not_implemented into a class to make the file
structure easier to understand and reduce the visibility of the
_implemented_dependencies cache. Rename it because it's no longer a
temporary hack (originally intended to work around the fact that not all
PSA_WANT symbols were implemented), it's now a way to detect test cases for
cryptographic mechanisms that are declared but not implemented.

Internal refactoring only. No behavior change.

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
diff --git a/scripts/mbedtls_dev/psa_information.py b/scripts/mbedtls_dev/psa_information.py
index 0e68df0..6f9c738 100644
--- a/scripts/mbedtls_dev/psa_information.py
+++ b/scripts/mbedtls_dev/psa_information.py
@@ -6,7 +6,7 @@
 
 
 import re
-from typing import Dict, FrozenSet, List, Optional, Set
+from typing import Dict, FrozenSet, Iterator, List, Optional, Set
 
 from . import macro_collector
 from . import test_case
@@ -54,30 +54,6 @@
     used.difference_update(SYMBOLS_WITHOUT_DEPENDENCY)
     return sorted(psa_want_symbol(name) for name in used)
 
-# Skip test cases for which the dependency symbols are not defined.
-# We assume that this means that a required mechanism is not implemented.
-# Note that if we erroneously skip generating test cases for
-# mechanisms that are not implemented, this should be caught
-# by the NOT_SUPPORTED test cases generated by generate_psa_tests.py
-# in test_suite_psa_crypto_not_supported and test_suite_psa_crypto_op_fail:
-# those emit negative tests, which will not be skipped here.
-def read_implemented_dependencies(filename: str) -> FrozenSet[str]:
-    return frozenset(symbol
-                     for line in open(filename)
-                     for symbol in re.findall(r'\bPSA_WANT_\w+\b', line))
-_implemented_dependencies = None #type: Optional[FrozenSet[str]] #pylint: disable=invalid-name
-
-def hack_dependencies_not_implemented(dependencies: List[str]) -> None:
-    global _implemented_dependencies #pylint: disable=global-statement,invalid-name
-    if _implemented_dependencies is None:
-        _implemented_dependencies = \
-            read_implemented_dependencies('include/psa/crypto_config.h')
-        _implemented_dependencies = _implemented_dependencies.union(
-            read_implemented_dependencies('include/mbedtls/config_psa.h'))
-    for dep in dependencies:
-        if dep.startswith('PSA_WANT') and dep not in _implemented_dependencies:
-            dependencies.append('DEPENDENCY_NOT_IMPLEMENTED_YET_' + dep)
-    dependencies.sort()
 
 class Information:
     """Gather information about PSA constructors."""
@@ -119,6 +95,47 @@
     involved in a given test case.
     """
 
+    # Use a class variable to cache the set of implemented dependencies.
+    # Call read_implemented_dependencies() to fill the cache.
+    _implemented_dependencies = None #type: Optional[FrozenSet[str]]
+
+    DEPENDENCY_SYMBOL_RE = re.compile(r'\bPSA_WANT_\w+\b')
+    @classmethod
+    def _yield_implemented_dependencies(cls) -> Iterator[str]:
+        for filename in ['include/psa/crypto_config.h',
+                         'include/mbedtls/config_psa.h']:
+            with open(filename) as inp:
+                content = inp.read()
+            yield from cls.DEPENDENCY_SYMBOL_RE.findall(content)
+
+    @classmethod
+    def read_implemented_dependencies(cls) -> FrozenSet[str]:
+        if cls._implemented_dependencies is None:
+            cls._implemented_dependencies = \
+                frozenset(cls._yield_implemented_dependencies())
+            # Redundant return to reassure pylint (mypy is fine without it).
+            # Known issue: https://github.com/pylint-dev/pylint/issues/3045
+            return cls._implemented_dependencies
+        return cls._implemented_dependencies
+
+    # We skip test cases for which the dependency symbols are not defined.
+    # We assume that this means that a required mechanism is not implemented.
+    # Note that if we erroneously skip generating test cases for
+    # mechanisms that are not implemented, this should be caught
+    # by the NOT_SUPPORTED test cases generated by generate_psa_tests.py
+    # in test_suite_psa_crypto_not_supported and test_suite_psa_crypto_op_fail:
+    # those emit negative tests, which will not be skipped here.
+    def detect_not_implemented_dependencies(self) -> None:
+        """Detect dependencies that are not implemented."""
+        all_implemented_dependencies = self.read_implemented_dependencies()
+        not_implemented = set()
+        for dep in self.dependencies:
+            if (dep.startswith('PSA_WANT') and
+                    dep not in all_implemented_dependencies):
+                not_implemented.add('DEPENDENCY_NOT_IMPLEMENTED_YET_' + dep)
+        self.dependencies = sorted(not_implemented) + self.dependencies
+        self.dependencies.sort()
+
     def __init__(self) -> None:
         super().__init__()
         self.key_bits = None #type: Optional[int]
@@ -157,5 +174,5 @@
                 dependencies[i] = '!' + dependencies[i]
         if self.key_bits is not None:
             dependencies = finish_family_dependencies(dependencies, self.key_bits)
-        hack_dependencies_not_implemented(dependencies)
-        self.dependencies += dependencies
+        self.dependencies += sorted(dependencies)
+        self.detect_not_implemented_dependencies()