Presubmit: Warn about useless forward declarations

Checks that added or removed lines in affected header files
do not lead to new useless class or struct forward declaration.

BUG=662195
TEST=PRESUBMIT_test.py ForwardDeclarationTest

Review-Url: https://codereview.chromium.org/2532583002
Cr-Commit-Position: refs/heads/master@{#434449}
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index d78629a..d4b30dd 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -1584,6 +1584,46 @@
   return []
 
 
+def _CheckUselessForwardDeclarations(input_api, output_api):
+  """Checks that added or removed lines in affected header files
+     do not lead to new useless class or struct forward declaration.
+  """
+  results = []
+  class_pattern = input_api.re.compile(r'^class\s+(\w+);$',
+                                       input_api.re.MULTILINE)
+  struct_pattern = input_api.re.compile(r'^struct\s+(\w+);$',
+                                        input_api.re.MULTILINE)
+  for f in input_api.AffectedFiles(include_deletes=False):
+    if not f.LocalPath().endswith('.h'):
+      continue
+
+    contents = input_api.ReadFile(f)
+    fwd_decls = input_api.re.findall(class_pattern, contents)
+    fwd_decls.extend(input_api.re.findall(struct_pattern, contents))
+
+    useless_fwd_decls = []
+    for decl in fwd_decls:
+      count = sum(1 for _ in input_api.re.finditer(
+        r'\b%s\b' % input_api.re.escape(decl), contents))
+      if count == 1:
+        useless_fwd_decls.append(decl)
+
+    if not useless_fwd_decls:
+      continue
+
+    for line in f.GenerateScmDiff().splitlines():
+      if (line.startswith('-') and not line.startswith('--') or
+          line.startswith('+') and not line.startswith('++')):
+        for decl in useless_fwd_decls:
+          if input_api.re.search(r'\b%s\b' % decl, line[1:]):
+            results.append(output_api.PresubmitPromptWarning(
+              '%s: %s forward declaration is becoming useless' %
+              (f.LocalPath(), decl)))
+            useless_fwd_decls.remove(decl)
+
+  return results
+
+
 def _CheckAndroidToastUsage(input_api, output_api):
   """Checks that code uses org.chromium.ui.widget.Toast instead of
      android.widget.Toast (Chromium Toast doesn't force hardware
@@ -2033,6 +2073,7 @@
   results.extend(_CheckJavaStyle(input_api, output_api))
   results.extend(_CheckIpcOwners(input_api, output_api))
   results.extend(_CheckMojoUsesNewWrapperTypes(input_api, output_api))
+  results.extend(_CheckUselessForwardDeclarations(input_api, output_api))
 
   if any('PRESUBMIT.py' == f.LocalPath() for f in input_api.AffectedFiles()):
     results.extend(input_api.canned_checks.RunUnitTestsInDirectory(