Add a utility to extract identifiers from public headers.

This is experimental for now, and will later be used or changed to help
with symbol prefixing. The ultimate goal is finding a way to safely link
BoringSSL and OpenSSL into the same binary, or even two BoringSSL
versions; this tool is part of the effort of first analyzing the public
symbols BoringSSL is providing to figure out next steps.

Tested:
  ```
  for f in include/openssl/*.h; do echo "#include <${f#include/}>"; done |\
    clang++ -x c++ -std=c++17 -Iinclude -fsyntax-only -Xclang -ast-dump=json - \
    go run util/extract_identifiers_clang_json.go > extract_identifiers.txt
  ```
  has identical output and no errors on both Linux and MacOS from
  clang-16 to clang-next (which will become clang-22).

Bug: 42220000
Change-Id: I3b3228242b29b8d97fa3a059e8e24caf91f39901
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/84708
Commit-Queue: Adam Langley <agl@google.com>
Reviewed-by: Adam Langley <agl@google.com>
Auto-Submit: Rudolf Polzer <rpolzer@google.com>
diff --git a/util/extract_identifiers_clang_json.go b/util/extract_identifiers_clang_json.go
new file mode 100644
index 0000000..37fc5d9
--- /dev/null
+++ b/util/extract_identifiers_clang_json.go
@@ -0,0 +1,482 @@
+// Copyright (c) 2025 The BoringSSL Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build ignore
+
+// extract_identifiers_clang_json parses the BoringSSL public includes and (for now)
+// outputs a report of all identifiers defined therein. Sample usage:
+//
+// for f in include/openssl/*.h; do echo "#include <${f#include/}>"; done |\
+//   clang++ -x c++ -std=c++17 -Iinclude -fsyntax-only -Xclang -ast-dump=json - \
+//   go run util/extract_identifiers_clang_json.go > extract_identifiers.txt
+//
+// Note that right now the output of this tool is for human use only.
+// The tool will likely be changed further for the purpose of symbol prefixing
+// and auditing thereof.
+
+package main
+
+import (
+	"encoding/json"
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"regexp"
+	"strings"
+)
+
+var (
+	dumpTree     = flag.Bool("dump_tree", false, "dump syntax tree while processing")
+	dumpFullTree = flag.Bool("dump_full_tree", false, "dump syntax tree while processing including system headers")
+	keepGoing    = flag.Bool("keep_going", false, "continue even after errors")
+	language     = flag.String("language", "C", "language to consider the source to be")
+)
+
+// node is a node from the Clang AST dump.
+type node struct {
+	Kind  string
+	Loc   loc
+	Inner []*node `json:",omitempty"`
+	Decl  *node
+
+	// Node fields that may or may not matter depending on `Kind`.
+	CompleteDefinition bool   `json:",omitempty"`
+	IsImplicit         bool   `json:",omitempty"`
+	Language           string `json:",omitempty"`
+	Name               string `json:",omitempty"`
+	PreviousDecl       string `json:",omitempty"`
+	StorageClass       string `json:",omitempty"`
+	TagUsed            string `json:",omitempty"`
+}
+
+// loc is a location from the Clang AST dump.
+type loc struct {
+	File         string `json:",omitempty"`
+	SpellingLoc  *loc   `json:",omitempty"`
+	ExpansionLoc *loc   `json:",omitempty"`
+}
+
+// file finds the file path of a loc.
+func (l loc) file() string {
+	if l.ExpansionLoc != nil {
+		return l.ExpansionLoc.file()
+	}
+	if l.SpellingLoc != nil {
+		return l.SpellingLoc.file()
+	}
+	return l.File
+}
+
+// decompress undoes the filename field compression from
+// JSONNodeDumper::writeSourceLocation and JSONNodeDumper::writeBareSourceLocation.
+func (l *loc) decompress(lastFile *string) {
+	if l == nil {
+		return
+	}
+	l.SpellingLoc.decompress(lastFile)
+	l.ExpansionLoc.decompress(lastFile)
+	if l.SpellingLoc != nil || l.ExpansionLoc != nil {
+		return
+	}
+	if l.File == "" {
+		l.File = *lastFile
+	} else {
+		*lastFile = l.File
+	}
+}
+
+// decompressLocsInternal is a helper for decompressLocs.
+//
+// It keeps state in its lastFile pointer.
+func (n *node) decompressLocsInternal(lastFile *string) {
+	n.Loc.decompress(lastFile)
+	for _, child := range n.Inner {
+		child.decompressLocsInternal(lastFile)
+	}
+}
+
+// decompressLocs decompresses all Loc fields below a node.
+//
+// Should be called right after parsing.
+func (n *node) decompressLocs() {
+	var lastFile string
+	n.decompressLocsInternal(&lastFile)
+}
+
+// storage represents the storage class of a node.
+type storage int
+
+const (
+	noStorage storage = iota
+	externStorage
+	staticStorage
+)
+
+// storage finds the storage class of the node.
+func (n node) storage() (storage, error) {
+	var storage storage
+	switch n.StorageClass {
+	case "", "extern":
+		storage = externStorage
+	case "static":
+		storage = staticStorage
+	default:
+		return noStorage, fmt.Errorf("no handling for storage class %q", n.StorageClass)
+	}
+	return storage, nil
+}
+
+// namespacing indicates how the identifier respects namespaces.
+type namespacing int
+
+const (
+	alwaysGlobal     namespacing = iota // Never in namespace (such as preprocessor macros).
+	globalIfC                           // Respects namespace unless in extern "C" (such as functions).
+	alwaysNamespaced                    // Always respects namespace (such as types).
+)
+
+// linking indicates how the identifier responds to extern "C" or similar.
+type linking int
+
+const (
+	neverLinked     linking = iota // Ignores linkage information (such as types).
+	respectsLinkage                // Respects linkage information (such as functions).
+)
+
+// walker is data that is transported to inner nodes while parsing.
+type walker struct {
+	*walkerStatic // Data that can be mutated even by downstream nodes.
+
+	inBoringSSL   bool     // Whether the code originates from BoringSSL.
+	depth         int      // Nesting depth (for -dump_tree output).
+	namespace     []string // C++ namespace sequence the node is in.
+	anonNamespace bool     // Whether the node is in a C++ anonymous namespace.
+	language      string   // Can be "C" or "C++".
+	record        bool     // Whether the current node is part of a record.
+}
+
+// walkerStatic is data that is transported in reading direction while parsing.
+type walkerStatic struct {
+	seen map[string]string // All identifiers seen so far.
+}
+
+func newWalker() walker {
+	return walker{
+		walkerStatic: &walkerStatic{
+			seen: map[string]string{},
+		},
+		language: *language,
+	}
+}
+
+// Consider files with a non-absolute path to be BoringSSL,
+// whereas absolute paths usually indicate system header locations.
+//
+// Note that any non-word character in the first two characters is treated as
+// indicating an absolute path to catch "<built-in>", "/foo/bar.h" and "C:\foo\bar.h".
+var (
+	boringSSLPath = regexp.MustCompile(`^\w\w`)
+)
+
+// updateInBoringSSL checks whether the given directive is a file/line directive,
+// and if so, checks if it's likely part of BoringSSL or not.
+//
+// The return value indicates whether it's a file/line directive.
+// If it is, `*in` will be updated to the current status of whether this is BoringSSL.
+func (w *walker) updateInBoringSSL(kind string, loc loc) {
+	if kind == "TranslationUnitDecl" {
+		w.inBoringSSL = true
+		return
+	}
+	w.inBoringSSL = boringSSLPath.MatchString(loc.file())
+}
+
+// visit traverses a node in the AST and analyzes it for identifiers contained therein.
+func (w walker) visit(n *node) (err error) {
+	nodeWithoutChildren := *n
+	nodeWithoutChildren.Inner = nil
+	nodeCode, err := json.Marshal(nodeWithoutChildren)
+	if err != nil {
+		return err
+	}
+
+	if (*dumpTree && w.inBoringSSL) || *dumpFullTree {
+		log.Printf("%*s[%s] %s: %s (%d children)",
+			w.depth, "",
+			strings.Join(w.namespace, "::"),
+			n.Kind,
+			nodeCode,
+			len(n.Inner))
+	}
+
+	// Allow to ignore errors.
+	defer func() {
+		if *keepGoing && err != nil {
+			log.Printf("ERROR: %v", err)
+			err = nil
+		}
+	}()
+
+	// Update "w".
+	w.depth++
+
+	// Update "in BoringSSL".
+	w.updateInBoringSSL(n.Kind, n.Loc)
+
+	if !w.inBoringSSL || n.IsImplicit {
+		// If suppressed, below nodes are not interesting.
+		// Also, skip any non-BoringSSL code such as system headers.
+		return nil
+	}
+
+	switch n.Kind {
+	// Nodes that need handling.
+	case "CXXRecordDecl", "RecordDecl":
+		if w.record && n.CompleteDefinition {
+			return nil
+		}
+		if n.Name != "" {
+			if err := w.collectIdentifier(n.TagUsed, alwaysNamespaced, neverLinked, noStorage, n.Name); err != nil {
+				return err
+			}
+		}
+		w.record = true
+	case "EnumDecl":
+		if w.record {
+			return nil
+		}
+		if n.Name != "" {
+			if err := w.collectIdentifier("enum", alwaysNamespaced, neverLinked, noStorage, n.Name); err != nil {
+				return err
+			}
+		}
+	case "EnumConstantDecl":
+		if w.record {
+			return nil
+		}
+		if err := w.collectIdentifier("enumerator", alwaysNamespaced, neverLinked, noStorage, n.Name); err != nil {
+			return err
+		}
+		return nil // Do not recurse.
+	case "FunctionDecl":
+		if w.record {
+			return nil
+		}
+		if n.PreviousDecl != "" {
+			return // Definition or redeclaration doesn't need to be looked at again (and may have incomplete qualifiers).
+		}
+		storage, err := n.storage()
+		if err != nil {
+			return fmt.Errorf("could not find storage class of function: %w: %s", err, nodeCode)
+		}
+		if err := w.collectIdentifier("function", globalIfC, respectsLinkage, storage, n.Name); err != nil {
+			return err
+		}
+	case "LinkageSpecDecl":
+		if n.Language != "" {
+			w.language = n.Language
+		}
+	case "NamespaceDecl":
+		if n.Name == "" {
+			w.anonNamespace = true
+		} else {
+			w.namespace = append(append([]string(nil), w.namespace...), n.Name)
+		}
+	case "TypeAliasDecl", "TypeAliasTemplateDecl":
+		if w.record {
+			return nil
+		}
+		if err := w.collectIdentifier("using", alwaysNamespaced, neverLinked, noStorage, n.Name); err != nil {
+			return err
+		}
+	case "TypedefDecl":
+		if w.record {
+			return nil
+		}
+		if len(n.Inner) == 1 && n.Inner[0].Kind == "ElaboratedType" && len(n.Inner[0].Inner) == 1 && n.Inner[0].Inner[0].Decl != nil && n.Inner[0].Inner[0].Decl.Name == n.Name {
+			// typedef struct X X;
+			return nil
+		}
+		if err := w.collectIdentifier("typedef", alwaysNamespaced, neverLinked, noStorage, n.Name); err != nil {
+			return err
+		}
+	case "VarDecl":
+		if n.PreviousDecl != "" {
+			return // Definition or redeclaration doesn't need to be looked at again (and may have incomplete qualifiers).
+		}
+		storage, err := n.storage()
+		if err != nil {
+			return fmt.Errorf("could not find storage class of variable: %w: %s", err, nodeCode)
+		}
+		if err := w.collectIdentifier("var", globalIfC, respectsLinkage, storage, n.Name); err != nil {
+			return err
+		}
+		return nil // Do not recurse. (Maybe should, to catch `struct ...` in variable types?)
+	// Singletons that should be skipped.
+	case
+		"AccessSpecDecl",
+		"AlignedAttr",
+		"BuiltinAttr",
+		"BuiltinType",
+		"ConstAttr",
+		"DependentNameType",
+		"DeprecatedAttr",
+		"EnumType",
+		"FormatAttr",
+		"NoThrowAttr",
+		"ParmVarDecl",
+		"RecordType",
+		"UnresolvedUsingValueDecl",
+		"UnusedAttr",
+		"UsingDirectiveDecl",
+		"VectorType",
+		"WarnUnusedResultAttr":
+		if len(n.Inner) != 0 {
+			// If this ever fires, check AST to see if any of the node's children could be useful,
+			// then categorize the node type into one of the following two cases.
+			return fmt.Errorf("singleton node of kind %q has children: %s", n.Kind, nodeCode)
+		}
+	// Nodes that should be skipped including possible children.
+	case
+		"CXXConstructorDecl",
+		"CXXConversionDecl",
+		"CXXDeductionGuideDecl",
+		"CXXDestructorDecl",
+		"CXXMethodDecl",
+		"ClassTemplatePartialSpecializationDecl",
+		"ClassTemplateSpecializationDecl",
+		"CompoundStmt",
+		"FieldDecl",
+		"FriendDecl",
+		"NonTypeTemplateParmDecl",
+		"StaticAssertDecl",
+		"TemplateArgument",
+		"TemplateTypeParmDecl",
+		"VarTemplateDecl":
+		return nil // Do not recurse.
+	// Nodes that should just be recursed into.
+	case
+		"ClassTemplateDecl",
+		"ConstantArrayType",
+		"DecayedType",
+		"ElaboratedType",
+		"FunctionProtoType",
+		"FunctionTemplateDecl",
+		"IndirectFieldDecl",
+		"ParenType",
+		"PointerType",
+		"QualType",
+		"TemplateSpecializationType",
+		"TranslationUnitDecl",
+		"TypedefType":
+		// Just recurse.
+	default:
+		return fmt.Errorf("no handling for node kind %q: %s", n.Kind, nodeCode)
+	}
+
+	// If we get here (via fallthrough usually), we want to recurse.
+	// To avoid recursing, use return.
+	for _, child := range n.Inner {
+		err = w.visit(child)
+		if err != nil {
+			break
+		}
+	}
+
+	return err
+}
+
+// collectIdentifier sends an identifier to the output.
+func (w walker) collectIdentifier(tag string, namespacing namespacing, linking linking, storage storage, name string) error {
+	var fqn string
+	if w.anonNamespace {
+		fqn = "<anonymous>::" + name
+	} else {
+		fqn = strings.Join(append(append([]string(nil), w.namespace...), name), "::")
+	}
+
+	var linkage string
+	switch linking {
+	case neverLinked:
+		linkage = ""
+	case respectsLinkage:
+		switch storage {
+		case externStorage:
+			linkage = fmt.Sprintf("extern %q ", w.language)
+		case staticStorage:
+			linkage = "static "
+		default:
+			return fmt.Errorf("respecting storage, but storage not set for %v", fqn)
+		}
+	}
+
+	var identifier string
+	switch namespacing {
+	case alwaysGlobal:
+		identifier = name
+	case globalIfC:
+		if w.language != "C" {
+			identifier = fqn
+		} else {
+			identifier = name
+		}
+	case alwaysNamespaced:
+		identifier = fqn
+	}
+
+	declaration := fmt.Sprintf("%s%s %s;", linkage, tag, identifier)
+	key := identifier
+	seen, found := w.seen[key]
+	if found {
+		if seen != declaration {
+			return fmt.Errorf("duplicate distinct definition of %v: %v and %v", key, seen, declaration)
+		}
+		return nil
+	}
+	w.seen[key] = declaration
+	fmt.Printf("%s\n", declaration)
+	return nil
+}
+
+// Main is the main program.
+func Main() error {
+	j := json.NewDecoder(os.Stdin)
+
+	w := newWalker()
+
+	for j.More() {
+		var root node
+		err := j.Decode(&root)
+		if err != nil {
+			return err
+		}
+		root.decompressLocs()
+		err = w.visit(&root)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// main runs Main turning errors into exit codes.
+func main() {
+	flag.Parse()
+	err := Main()
+	if err != nil {
+		log.Panicf("error returned from Main: %v", err)
+	}
+}