[tint][gen] Detect #includes without necessary guards

This prevents unguarded includes to targets that require TINT_BUILD_XXX
flags to be enabled.

Change-Id: I604b473a9cb24044346e8ef14f841851570b0086
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/155443
Reviewed-by: James Price <jrprice@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/docs/tint/gen.md b/docs/tint/gen.md
index 7d48ac6..14a7907 100644
--- a/docs/tint/gen.md
+++ b/docs/tint/gen.md
@@ -126,6 +126,8 @@
 {
   /* An override for the output file name for the target */
   "OutputName": "name",
+  /* An additional condition for building this target */
+  "Condition": "cond",
   "AdditionalDependencies": {
     "Internal": [
       /*
diff --git a/tools/src/cmd/gen/build/build.go b/tools/src/cmd/gen/build/build.go
index 15e17b5..7ab9bfe 100644
--- a/tools/src/cmd/gen/build/build.go
+++ b/tools/src/cmd/gen/build/build.go
@@ -209,23 +209,63 @@
 	// parseFile parses the source file at 'path' represented by 'file'
 	// As this is run concurrently, it must not modify any shared state (including file)
 	parseFile := func(path string, file *File) (string, *ParsedFile, error) {
+		conditions := []Condition{}
+
 		body, err := os.ReadFile(file.AbsPath())
 		if err != nil {
 			return path, nil, err
 		}
 		out := &ParsedFile{}
 		for i, line := range strings.Split(string(body), "\n") {
+			wrapErr := func(err error) error {
+				return fmt.Errorf("%v:%v %w", file.Path(), i+1, err)
+			}
 			if match := reIgnoreFile.FindStringSubmatch(line); len(match) > 0 {
 				out.removeFromProject = true
 				continue
 			}
+			if match := reIf.FindStringSubmatch(line); len(match) > 0 {
+				condition, err := cnf.Parse(strings.ToLower(match[1]))
+				if err != nil {
+					condition = Condition{{cnf.Unary{Var: "FAILED_TO_PARSE_CONDITION"}}}
+				}
+				if len(conditions) > 0 {
+					condition = cnf.And(condition, conditions[len(conditions)-1])
+				}
+				conditions = append(conditions, condition)
+			}
+			if match := reIfdef.FindStringSubmatch(line); len(match) > 0 {
+				conditions = append(conditions, Condition{})
+			}
+			if match := reIfndef.FindStringSubmatch(line); len(match) > 0 {
+				conditions = append(conditions, Condition{})
+			}
+			if match := reElse.FindStringSubmatch(line); len(match) > 0 {
+				if len(conditions) == 0 {
+					return path, nil, wrapErr(fmt.Errorf("#else without #if"))
+				}
+				conditions[len(conditions)-1] = cnf.Not(conditions[len(conditions)-1])
+			}
+			if match := reEndif.FindStringSubmatch(line); len(match) > 0 {
+				if len(conditions) == 0 {
+					return path, nil, wrapErr(fmt.Errorf("#endif without #if"))
+				}
+				conditions = conditions[:len(conditions)-1]
+			}
 			if match := reCondition.FindStringSubmatch(line); len(match) > 0 {
 				out.conditions = append(out.conditions, match[1])
 			}
 			if !reIgnoreInclude.MatchString(line) {
 				for _, re := range []*regexp.Regexp{reInclude, reHashImport, reAtImport} {
 					if match := re.FindStringSubmatch(line); len(match) > 0 {
-						out.includes = append(out.includes, Include{match[1], i + 1})
+						include := Include{
+							Path: match[1],
+							Line: i + 1,
+						}
+						if len(conditions) > 0 {
+							include.Condition = conditions[len(conditions)-1]
+						}
+						out.includes = append(out.includes, include)
 					}
 				}
 			}
@@ -322,6 +362,14 @@
 			// Apply any custom output name
 			target.OutputName = tc.cfg.OutputName
 
+			if tc.cfg.Condition != "" {
+				condition, err := cnf.Parse(tc.cfg.Condition)
+				if err != nil {
+					return fmt.Errorf("%v: %v", path, err)
+				}
+				target.Condition = cnf.And(target.Condition, condition)
+			}
+
 			// Add any additional internal dependencies
 			for _, depPattern := range tc.cfg.AdditionalDependencies.Internal {
 				match, err := match.New(depPattern)
@@ -424,6 +472,25 @@
 						addExternalDependency(dependency)
 					}
 
+					noneIfEmpty := func(cond Condition) string {
+						if len(cond) == 0 {
+							return "<none>"
+						}
+						return cond.String()
+					}
+					sourceConditions := cnf.And(cnf.And(include.Condition, file.Condition), file.Target.Condition)
+					targetConditions := cnf.And(includeFile.Condition, includeFile.Target.Condition)
+					if missing := targetConditions.Remove(sourceConditions); len(missing) > 0 {
+						return fmt.Errorf(`%v:%v #include "%v" requires guard: #if %v
+
+%v build conditions: %v
+%v build conditions: %v`,
+							file.Path(), include.Line, include.Path, strings.ToUpper(missing.String()),
+							file.Path(), noneIfEmpty(sourceConditions),
+							include.Path, targetConditions,
+						)
+					}
+
 				} else {
 					// Check for external includes
 					for _, external := range p.externals.Values() {
@@ -647,6 +714,11 @@
 
 var (
 	// Regular expressions used by this file
+	reIf            = regexp.MustCompile(`\s*#\s*if\s+(.*)`)
+	reIfdef         = regexp.MustCompile(`\s*#\s*ifdef\s+(.*)`)
+	reIfndef        = regexp.MustCompile(`\s*#\s*ifndef\s+(.*)`)
+	reElse          = regexp.MustCompile(`\s*#\s*else\s+(.*)`)
+	reEndif         = regexp.MustCompile(`\s*#\s*endif`)
 	reInclude       = regexp.MustCompile(`\s*#\s*include\s*(?:\"|<)([^(\"|>)]+)(?:\"|>)`)
 	reHashImport    = regexp.MustCompile(`\s*#\s*import\s*\<([\w\/\.]+)\>`)
 	reAtImport      = regexp.MustCompile(`\s*@\s*import\s*(\w+)\s*;`)
diff --git a/tools/src/cmd/gen/build/directory_config.go b/tools/src/cmd/gen/build/directory_config.go
index 4548ad6..666a231 100644
--- a/tools/src/cmd/gen/build/directory_config.go
+++ b/tools/src/cmd/gen/build/directory_config.go
@@ -18,6 +18,8 @@
 type TargetConfig struct {
 	// Override for the output name of this target
 	OutputName string
+	// Conditionals for this target
+	Condition string
 	// Additional dependencies to add to this target
 	AdditionalDependencies struct {
 		// List of internal dependency patterns
diff --git a/tools/src/cmd/gen/build/file.go b/tools/src/cmd/gen/build/file.go
index 7fb9b9c..98b5bf1 100644
--- a/tools/src/cmd/gen/build/file.go
+++ b/tools/src/cmd/gen/build/file.go
@@ -18,8 +18,9 @@
 
 // Include describes a single #include in a file
 type Include struct {
-	Path string
-	Line int
+	Path      string
+	Line      int
+	Condition Condition
 }
 
 // File holds information about a source file
diff --git a/tools/src/cnf/expr.go b/tools/src/cnf/expr.go
index 2466901..cdd5853 100644
--- a/tools/src/cnf/expr.go
+++ b/tools/src/cnf/expr.go
@@ -14,6 +14,8 @@
 
 package cnf
 
+import "dawn.googlesource.com/dawn/tools/src/container"
+
 // Expr is a boolean expression, expressed in a Conjunctive Normal Form.
 // Expr is an alias to Ands, which represent all the OR expressions that are
 // AND'd together.
@@ -38,3 +40,18 @@
 	// The name of the variable
 	Var string
 }
+
+// Remove returns a new expression with all the And expressions of o removed from e
+func (e Expr) Remove(o Expr) Expr {
+	set := container.NewSet[Key]()
+	for _, expr := range o {
+		set.Add(expr.Key())
+	}
+	out := Expr{}
+	for _, expr := range e {
+		if !set.Contains(expr.Key()) {
+			out = append(out, expr)
+		}
+	}
+	return out
+}