| #!/usr/bin/env python3 | |
| # Copyright (C) 2025 Apple Inc. All rights reserved. | |
| # | |
| # Redistribution and use in source and binary forms, with or without | |
| # modification, are permitted provided that the following conditions | |
| # are met: | |
| # 1. Redistributions of source code must retain the above copyright | |
| # notice, this list of conditions and the following disclaimer. | |
| # 2. Redistributions in binary form must reproduce the above copyright | |
| # notice, this list of conditions and the following disclaimer in the | |
| # documentation and/or other materials provided with the distribution. | |
| # | |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND | |
| # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
| # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR | |
| # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| import json | |
| import os | |
| import re | |
| SYSTEM_HEADER_INCLUDE_PATTERN_REGEX = re.compile(r'\s*\#include\s+\<(?P<include>.+)\>\s*') | |
| LOCAL_HEADER_INCLUDE_PATTERN_REGEX = re.compile(r'\s*\#include\s+"(?P<include>.+)"\s*') | |
| def main(): | |
| root_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), '../../')) | |
| source_dir = os.path.join(root_dir, 'Source') | |
| derived_sources_dir = os.path.join(root_dir, 'WebKitBuild', 'Release', 'DerivedSources') | |
| if not os.path.isdir(derived_sources_dir): | |
| derived_sources_dir = os.path.join(root_dir, 'WebKitBuild', 'Debug', 'DerivedSources') | |
| if not os.path.isdir(derived_sources_dir): | |
| print('No valid WebKitBuild directory') | |
| derived_sources_dir = None | |
| projects = [] | |
| for dir_name in os.listdir(source_dir): | |
| full_path = os.path.join(source_dir, dir_name) | |
| if not os.path.isdir(full_path) or dir_name in ['ThirdParty', 'cmake']: | |
| continue | |
| if dir_name == 'WebCore': | |
| projects.append(Project('PAL', find_source_files('PAL', root_dir, os.path.join(full_path, 'PAL')))) | |
| sources = find_source_files(dir_name, root_dir, full_path) | |
| if derived_sources_dir: | |
| sources += find_source_files(dir_name, os.path.dirname(derived_sources_dir), os.path.join(derived_sources_dir, dir_name)) | |
| projects.append(Project(dir_name, sources)) | |
| project_map = {} | |
| system_header_map = {} | |
| for project in projects: | |
| for source in project.sources(): | |
| system_header_map[source.system_include_path()] = source | |
| project_map[project.name()] = project | |
| for project in projects: | |
| project.resolve_includes(system_header_map) | |
| result = {'projects': [], 'sources': []} | |
| for project in projects: | |
| result['projects'].append(project.to_dict()) | |
| for source in project.sources(): | |
| result['sources'].append(source.to_dict()) | |
| print(json.dumps(result)) | |
| return | |
| for project in projects: | |
| project.compute_include_origins() | |
| for project in projects: | |
| print('====================', project.name(), '====================') | |
| for source in sorted(project.sources(), key=lambda source: source.total_include_count(), reverse=True): | |
| if not source.total_include_count(): | |
| continue | |
| print(source.path(), 'included by', source.total_include_count(), 'files') | |
| def find_source_files(project, root_dir, dir_path): | |
| source_files = [] | |
| for dirpath, dirnames, filenames in os.walk(dir_path): | |
| for file_name in filenames: | |
| file_path = os.path.join(dirpath, file_name) | |
| _, ext = os.path.splitext(file_path) | |
| if os.path.relpath(os.path.dirname(file_path), dir_path) == 'PAL': | |
| continue | |
| if ext not in ['.c', '.cpp', '.m', '.mm', '.h']: | |
| continue | |
| with open(file_path) as source_file: | |
| system_includes = [] | |
| local_includes = [] | |
| for line in source_file: | |
| include_match = SYSTEM_HEADER_INCLUDE_PATTERN_REGEX.match(line) | |
| if include_match: | |
| system_includes.append(include_match.group('include')) | |
| include_match = LOCAL_HEADER_INCLUDE_PATTERN_REGEX.match(line) | |
| if include_match: | |
| local_includes.append(include_match.group('include')) | |
| rel_path = os.path.relpath(file_path, dir_path) if project in ['WTF', 'PAL'] else file_name | |
| rel_path_to_root = os.path.relpath(file_path, root_dir) | |
| source_files.append(SourceFile(project, rel_path, rel_path_to_root, file_path, system_includes, local_includes)) | |
| return source_files | |
| class Project(object): | |
| def __init__(self, name, sources): | |
| self._name = name | |
| self._sources = sources | |
| self._source_map = {} | |
| for source in sources: | |
| if name == 'WTF' and source.local_include_path().startswith('icu/unicode/'): | |
| self._source_map[source.local_include_path().removeprefix('icu/')] = source | |
| else: | |
| self._source_map[source.local_include_path()] = source | |
| def to_dict(self): | |
| return {'name': self._name, 'sources': [source.path() for source in self._sources]} | |
| def name(self): | |
| return self._name | |
| def sources(self): | |
| return self._sources | |
| def resolve_includes(self, system_header_map): | |
| for source in self._sources: | |
| source.resolve_local_includes(self._source_map) | |
| source.resolve_system_includes(system_header_map) | |
| def compute_include_origins(self): | |
| for source in self._sources: | |
| if source.is_included_by_other_sources(): | |
| continue | |
| includes = compute_all_includes(source) | |
| for target in includes: | |
| target.add_total_include_count() | |
| def compute_all_includes(source, targets=set(), working_set=set()): | |
| if source in working_set: | |
| return targets; # Detected a cycle. | |
| for included_source in source.includes(): | |
| if included_source in targets: | |
| continue | |
| targets.add(included_source) | |
| compute_all_includes(included_source, targets, set(list(working_set) + [source])) | |
| return targets | |
| class SourceFile(object): | |
| def __init__(self, project, rel_path, rel_path_to_root, file_path, system_include_paths, local_include_paths): | |
| self._project = project | |
| self._rel_path = rel_path | |
| self._rel_path_to_root = rel_path_to_root | |
| self._file_path = file_path | |
| self._system_include_paths = system_include_paths | |
| self._system_includes = [] | |
| self._local_include_paths = local_include_paths | |
| self._local_includes = [] | |
| self._include_origins = [] | |
| self._transitive_include_count = None | |
| self._total_include_count = 0 | |
| self._not_found = [] | |
| def to_dict(self): | |
| return { | |
| 'project': self._project, | |
| 'name': self.local_include_path(), | |
| 'relative': self._rel_path_to_root, | |
| 'path': self.path(), | |
| 'includes': [source.path() for source in self.includes()], | |
| 'notFound': self._not_found, | |
| } | |
| def project(self): | |
| return self._project | |
| def path(self): | |
| return self._file_path | |
| def local_include_path(self): | |
| return self._rel_path | |
| def system_include_path(self): | |
| if self._project in ['WTF', 'PAL']: | |
| return self._rel_path | |
| return os.path.join(self._project, self._rel_path) | |
| def resolve_local_includes(self, source_map): | |
| for path in self._local_include_paths: | |
| target_source = source_map.get(path) | |
| if target_source: | |
| target_source.add_include_origin(self) | |
| self._local_includes.append(target_source) | |
| else: | |
| self._not_found.append(path) | |
| def resolve_system_includes(self, system_header_map): | |
| for path in self._system_include_paths: | |
| target_source = system_header_map.get(path) | |
| if target_source: | |
| target_source.add_include_origin(self) | |
| self._system_includes.append(target_source) | |
| else: | |
| self._not_found.append(path) | |
| def add_total_include_count(self): | |
| self._total_include_count += 1 | |
| def total_include_count(self): | |
| return self._total_include_count | |
| def add_include_origin(self, source): | |
| self._include_origins.append(source) | |
| def is_included_by_other_sources(self): | |
| return len(self._include_origins) | |
| def compute_include_count(self, current_path=[]): | |
| if current_path.count(self): | |
| print('Detected a cycle!', [source.path() for source in current_path] + [self.path()]) | |
| return 0 | |
| if self._transitive_include_count is None: | |
| count = 1 | |
| for origin in self._include_origins: | |
| count += origin.compute_include_count(current_path + [self]) | |
| self._transitive_include_count = count | |
| print('transitive count:', count, 'origins:', len(self._include_origins), self.path()) | |
| return self._transitive_include_count | |
| def includes(self): | |
| return self._system_includes + self._local_includes | |
| def not_found(self): | |
| return self._not_found | |
| if __name__ == '__main__': | |
| main() |