diff mbox series

[v2] parse: warn on trailing whitespace in parsed lines

Message ID 20260806112435.25128-1-jaipaul.cheernam@est.tech
State New
Headers show
Series [v2] parse: warn on trailing whitespace in parsed lines | expand

Commit Message

Jaipaul Cheernam Aug. 6, 2026, 11:24 a.m. UTC
rstrip() silently strips trailing whitespace before processing. This
hides invisible spaces or tabs that end up in patch context and cause
patches to fail to apply across branches.

Warn on any line with trailing whitespace so developers can fix it early.

Signed-off-by: Jaipaul Cheernam <jaipaul.cheernam@est.tech>
---
Changes since v1:
- Broadened from just backslash continuation to any trailing whitespace,
  as suggested by Richard.

Real example from oe-core master (libssh2_1.11.1.bb, line 18):
    file://CVE-2025-15661-3.patch \<TAB>   
This caused CVE patches to fail to apply on wrynose where the trailing
tab was absent — invisible context mismatch.

Full parse (bitbake -p) across oe-core + meta-openembedded (2970 recipes):
- 228 unique trailing whitespace instances (verified, 0 false positives)
- 82 files affected (53 .bb, 19 .bbclass, 8 .inc, 2 .conf)
- 92 in oe-core, 136 in meta-openembedded

bbclass files like useradd.bbclass, sanity.bbclass, buildstats.bbclass
account for most of the warning noise since they are re-parsed for every
recipe that inherits them.

Tested with bitbake-selftest:
  $ PYTHONPATH=lib python3 -m unittest lib.bb.tests.parse.ParseTest -v
  3 new tests pass, no regressions.

A separate cleanup series will follow to fix the affected files.

 lib/bb/parse/parse_py/BBHandler.py   |  5 ++++-
 lib/bb/parse/parse_py/ConfHandler.py | 10 ++++++++--
 lib/bb/tests/parse.py                | 28 ++++++++++++++++++++++++++++
 3 files changed, 40 insertions(+), 3 deletions(-)
diff mbox series

Patch

diff --git a/lib/bb/parse/parse_py/BBHandler.py b/lib/bb/parse/parse_py/BBHandler.py
index 008fec230..3ac694e20 100644
--- a/lib/bb/parse/parse_py/BBHandler.py
+++ b/lib/bb/parse/parse_py/BBHandler.py
@@ -104,7 +104,10 @@  def get_statements(filename, absolute_filename, base_name):
                 lineno = lineno + 1
                 s = f.readline()
                 if not s: break
-                s = s.rstrip()
+                raw = s.rstrip('\n').rstrip('\r')
+                s = raw.rstrip()
+                if s and raw != s:
+                    bb.warn("Trailing whitespace in %s, line %s" % (filename, lineno))
                 feeder(lineno, s, filename, base_name, statements)
 
         if __inpython__:
diff --git a/lib/bb/parse/parse_py/ConfHandler.py b/lib/bb/parse/parse_py/ConfHandler.py
index 9ddbae123..e4306d035 100644
--- a/lib/bb/parse/parse_py/ConfHandler.py
+++ b/lib/bb/parse/parse_py/ConfHandler.py
@@ -134,12 +134,18 @@  def handle(fn, data, include, baseconfig=False):
             # skip empty lines
             if not w:
                 continue
-            s = s.rstrip()
+            raw = s.rstrip('\n').rstrip('\r')
+            s = raw.rstrip()
+            if s and raw != s:
+                bb.warn("Trailing whitespace in %s, line %s" % (fn, lineno))
             while s[-1] == '\\':
                 line = f.readline()
                 origline += line
-                s2 = line.rstrip()
+                raw2 = line.rstrip('\n').rstrip('\r')
+                s2 = raw2.rstrip()
                 lineno = lineno + 1
+                if s2 and raw2 != s2:
+                    bb.warn("Trailing whitespace in %s, line %s" % (fn, lineno))
                 if (not s2 or s2 and s2[0] != "#") and s[0] == "#" :
                     bb.fatal("There is a confusing multiline, partially commented expression starting on line %s of file %s:\n%s\nPlease clarify whether this is all a comment or should be parsed." % (origlineno, fn, origline))
 
diff --git a/lib/bb/tests/parse.py b/lib/bb/tests/parse.py
index 6ac2137e0..5ccc6e93a 100644
--- a/lib/bb/tests/parse.py
+++ b/lib/bb/tests/parse.py
@@ -638,3 +638,31 @@  EXTRA_OECONF:append = " foobar"
             output = run_bitbake(["bitbake", "-e", "recipe-file1"], builddir, extraenv).splitlines()
             self.assertIn('BBCLASS_FILE="recipe-file.inc"', output)
             self.assertIn(f'BBCLASS_RECIPE_FILE="recipe-file1.bb"', output)
+
+    trailing_whitespace_continuation = "A = \"1 \\\t  \n2\"\n"
+
+    def test_parse_trailing_whitespace_continuation(self):
+        """Test that trailing whitespace after backslash continuation emits a warning"""
+        with self.parsehelper(self.trailing_whitespace_continuation) as f:
+            with self.assertLogs('BitBake', level='WARNING') as cm:
+                d = bb.parse.handle(f.name, self.d)['']
+        self.assertTrue(any("Trailing whitespace" in msg for msg in cm.output))
+        self.assertEqual(d.getVar("A"), "1 2")
+
+    trailing_whitespace_after_value = 'A = "1"  \n'
+
+    def test_parse_trailing_whitespace_after_value(self):
+        """Test that trailing whitespace after a normal value emits a warning"""
+        with self.parsehelper(self.trailing_whitespace_after_value) as f:
+            with self.assertLogs('BitBake', level='WARNING') as cm:
+                d = bb.parse.handle(f.name, self.d)['']
+        self.assertTrue(any("Trailing whitespace" in msg for msg in cm.output))
+        self.assertEqual(d.getVar("A"), "1")
+
+    clean_continuation = "A = \"1 \\\n2\"\n"
+
+    def test_parse_clean_continuation_no_warning(self):
+        """Test that clean backslash continuation does not warn"""
+        with self.parsehelper(self.clean_continuation) as f:
+            d = bb.parse.handle(f.name, self.d)['']
+        self.assertEqual(d.getVar("A"), "1 2")