Repository: googlefonts/ufo2ft
Pull request #732 (2023-03-23 15:18:55)

iterClassDefinitions() should find nested class definitions
https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.g.ii
> Glyph class assignments can appear anywhere in the feature file. A glyph class name may be used in the feature file only after its definition.

Glyph class definitions can be top-level, inside a feature blocks but also inside a lookup block or inside lookup block itself inside a feature block. I guess other blocks like cvparameters or table would not make sense.

anthrotype: hey Denis! can you fix formatting with black, and also rebase on main where I made codecov not fail the PR checks (upload is flaky with PRs sometimes)

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e01b627..35291c3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -47,7 +47,7 @@ jobs:
         file: coverage.xml
         flags: unittests
         name: codecov-umbrella
-        fail_ci_if_error: false
+        fail_ci_if_error: true
         # see https://github.com/codecov/codecov-action/issues/557
         token: ${{ secrets.CODECOV_TOKEN }}
   deploy:
diff --git a/Lib/ufo2ft/featureWriters/ast.py b/Lib/ufo2ft/featureWriters/ast.py
index c94968e..c027db1 100644
--- a/Lib/ufo2ft/featureWriters/ast.py
+++ b/Lib/ufo2ft/featureWriters/ast.py
@@ -73,17 +73,12 @@ def findTable(feaLib, tag):
             return statement
 
 
-def iterClassDefinitions(feaFile, featureTag=None):
-    if featureTag is None:
-        # start from top-level class definitions
-        for s in feaFile.statements:
-            if isinstance(s, ast.GlyphClassDefinition):
-                yield s
-    # then iterate over per-feature class definitions
-    for fea in iterFeatureBlocks(feaFile, tag=featureTag):
-        for s in fea.statements:
-            if isinstance(s, ast.GlyphClassDefinition):
-                yield s
+def iterClassDefinitions(feaFile):
+    for s in feaFile.statements:
+        if isinstance(s, ast.GlyphClassDefinition):
+            yield s
+        elif isinstance(s, (ast.Block)):
+            yield from iterClassDefinitions(s)
 
 
 LOOKUP_FLAGS = {


Test patch:
diff --git a/tests/featureWriters/ast_test.py b/tests/featureWriters/ast_test.py
new file mode 100644
index 0000000..4591470
--- /dev/null
+++ b/tests/featureWriters/ast_test.py
@@ -0,0 +1,36 @@
+from io import StringIO
+
+from fontTools.feaLib.parser import Parser
+
+from ufo2ft.featureWriters import ast
+
+
+def test_iterClassDefinitions():
+    text = """
+    @TopLevelClass = [a b c];
+
+    lookup test_lookup {
+        @LookupLevelClass = [d e f];
+    } test_lookup;
+
+    feature test {
+        @FeatureLevelClass = [g h i];
+
+        lookup test_nested_lookup {
+            @NestedLookupLevelClass = [j k l];
+        } test_nested_lookup;
+    } test;
+    """
+    glyph_names = "a b c d e f g h i j k l".split()
+    feature_file = StringIO(text)
+    p = Parser(feature_file, glyph_names)
+    doc = p.parse()
+    class_defs = ast.iterClassDefinitions(doc)
+    result = [cd.name for cd in class_defs]
+    expected = [
+        "TopLevelClass",
+        "LookupLevelClass",
+        "FeatureLevelClass",
+        "NestedLookupLevelClass",
+    ]
+    assert result == expected