| #!/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() |