Repository: fonttools/fonttools

Issue:
feaLib should not error if enum pos conflict with preceding single pair pos
the Adobe feature file spec says about enum pos:

> 6.b.ii. Enumerating pairs
> [...] Since this representation is convenient for generating a large number of specific pairs, it may be used even when some of the pairs generated by the 'enum' rules are incorrect. Specific pairs generated by an 'enum' rule may be overridden by specifying preceding single pairs. Because of this case, it is not an error when specific kern pairs conflict because they have the same glyphs. When specific kern pair rules conflict, the first rule specified is used, and later conflicting rule are skipped.

Consider this snippet of feature code:
```
@Y_LC = [y yacute ydieresis];
@SMALL_PUNC = [comma semicolon period];

feature kern {
  pos yacute semicolon -70;  # specific pair
  enum pos @Y_LC semicolon -80;  # specific pairs
  pos @Y_LC @SMALL_PUNC -100;  # class pair
} kern;
```

Compiling with feaLib produces this error: `Already defined position for pair yacute semicolon ...`

Whereas, compiling with makeotf only produces a warning and the more specific pair pos takes precedence over the following conflicting enum pos rules.

```
makeotfexe [WARNING] <...> Pair positioning has conflicting statements in 'kern' feature;
choosing the first value: yacute semicolon
```

In ufo2ft we even have a method to prevent such feaLib error:
See https://github.com/googlei18n/ufo2ft/issues/205

I think feaLib should do the same as makeotf. Resolve the conflict automatically in favour of the more specific pair pos rules between specific glyphs that has occurred before a conflicting enum pos rule, and just emit a warning (or even an info message, because this notation is just a convenience for the user).


Discussion:
I also noticed that makeotf prints the same warning message if two single-glyph pair pos rules conflict with each other as in

```
pos yacute semicolon -70;
pos yacute semicolon -80;
```

the first always wins. I think we shall do the same.
On the other hand, if we were to relax this "Already defined" from an error to a warning for pair pos rules, why should we not also do it for the other rules (e.g. single pos, single sub, multiple sub, alternate sub)? :thinking: 
Warnings tend to get ignored. Shouldn’t designers rather write correct sources? No strong opinions though, I’m not a font designer.
I tend to agree on that. But the spec clearly states that enum pos can be used like that, and gives a way to systematically resolve such conflicts (take the preceding pos rule)
Ah, in that case...

Resolving patch:
diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py
--- a/Lib/fontTools/feaLib/builder.py
+++ b/Lib/fontTools/feaLib/builder.py
@@ -9,6 +9,10 @@
 from fontTools.ttLib import newTable, getTableModule
 from fontTools.ttLib.tables import otBase, otTables
 import itertools
+import logging
+
+
+log = logging.getLogger(__name__)
 
 
 def addOpenTypeFeatures(font, featurefile):
@@ -1424,15 +1428,19 @@ def addGlyphPair(self, location, glyph1, value1, glyph2, value2):
         key = (glyph1, glyph2)
         oldValue = self.glyphPairs.get(key, None)
         if oldValue is not None:
+            # the Feature File spec explicitly allows specific pairs generated
+            # by an 'enum' rule to be overridden by preceding single pairs;
+            # we emit a warning and use the previously defined value
             otherLoc = self.locations[key]
-            raise FeatureLibError(
-                'Already defined position for pair %s %s at %s:%d:%d'
-                % (glyph1, glyph2, otherLoc[0], otherLoc[1], otherLoc[2]),
-                location)
-        val1, _ = makeOpenTypeValueRecord(value1, pairPosContext=True)
-        val2, _ = makeOpenTypeValueRecord(value2, pairPosContext=True)
-        self.glyphPairs[key] = (val1, val2)
-        self.locations[key] = location
+            log.warning(
+                'Already defined position for pair %s %s at %s:%d:%d; '
+                'choosing the first value',
+                glyph1, glyph2, otherLoc[0], otherLoc[1], otherLoc[2])
+        else:
+            val1, _ = makeOpenTypeValueRecord(value1, pairPosContext=True)
+            val2, _ = makeOpenTypeValueRecord(value2, pairPosContext=True)
+            self.glyphPairs[key] = (val1, val2)
+            self.locations[key] = location
 
     def add_subtable_break(self, location):
         self.pairs.append((self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_,
diff --git a/Lib/fontTools/misc/loggingTools.py b/Lib/fontTools/misc/loggingTools.py
--- a/Lib/fontTools/misc/loggingTools.py
+++ b/Lib/fontTools/misc/loggingTools.py
@@ -467,7 +467,7 @@ def assertRegex(self, regexp):
 		import re
 		pattern = re.compile(regexp)
 		for r in self.records:
-			if pattern.search(r.msg):
+			if pattern.search(r.getMessage()):
 				return True
 		assert 0, "Pattern '%s' not found in logger records" % regexp
 


Test patch:
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py
--- a/Tests/feaLib/builder_test.py
+++ b/Tests/feaLib/builder_test.py
@@ -1,6 +1,7 @@
 from __future__ import print_function, division, absolute_import
 from __future__ import unicode_literals
 from fontTools.misc.py23 import *
+from fontTools.misc.loggingTools import CapturingLogHandler
 from fontTools.feaLib.builder import Builder, addOpenTypeFeatures, \
         addOpenTypeFeaturesFromString
 from fontTools.feaLib.error import FeatureLibError
@@ -13,6 +14,7 @@
 import shutil
 import sys
 import tempfile
+import logging
 import unittest
 
 
@@ -196,16 +198,29 @@ def test_multipleSubst_multipleSubstitutionsForSameGlyph(self):
             "    sub f_f_i by f f i;"
             "} test;")
 
-    def test_pairPos_redefinition(self):
-        self.assertRaisesRegex(
-            FeatureLibError,
-            r"Already defined position for pair A B "
-            "at .*:2:[0-9]+",  # :2: = line 2
-            self.build,
-            "feature test {\n"
-            "    pos A B 123;\n"  # line 2
-            "    pos A B 456;\n"
-            "} test;\n")
+    def test_pairPos_redefinition_warning(self):
+        # https://github.com/fonttools/fonttools/issues/1147
+        logger = logging.getLogger("fontTools.feaLib.builder")
+        with CapturingLogHandler(logger, "WARNING") as captor:
+            # the pair "yacute semicolon" is redefined in the enum pos
+            font = self.build(
+                "@Y_LC = [y yacute ydieresis];"
+                "@SMALL_PUNC = [comma semicolon period];"
+                "feature kern {"
+                "  pos yacute semicolon -70;"
+                "  enum pos @Y_LC semicolon -80;"
+                "  pos @Y_LC @SMALL_PUNC -100;"
+                "} kern;")
+
+        captor.assertRegex("Already defined position for pair yacute semicolon")
+
+        # the first definition prevails: yacute semicolon -70
+        st = font["GPOS"].table.LookupList.Lookup[0].SubTable[0]
+        self.assertEqual(st.Coverage.glyphs[2], "yacute")
+        self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph,
+                         "semicolon")
+        self.assertEqual(vars(st.PairSet[2].PairValueRecord[0].Value1),
+                         {"XAdvance": -70})
 
     def test_singleSubst_multipleSubstitutionsForSameGlyph(self):
         self.assertRaisesRegex(