| // Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| /** |
| * Utility functions for analyzing keep radius data. |
| */ |
| function hasGlobalRule(data, ruleName) { |
| if (!data || !data.globalKeepRuleKeepRadiusTable) return false; |
| return data.globalKeepRuleKeepRadiusTable.some(r => r.source === ruleName); |
| } |
| |
| function getDisallowObfuscationCount(data) { |
| if (hasGlobalRule(data, '-dontobfuscate')) { |
| return getLiveItemCount(data); |
| } |
| return getScore(data, 'DONT_OBFUSCATE'); |
| } |
| function getDisallowOptimizationCount(data) { |
| if (hasGlobalRule(data, '-dontoptimize')) { |
| return getLiveItemCount(data); |
| } |
| return getScore(data, 'DONT_OPTIMIZE'); |
| } |
| function getDisallowShrinkingCount(data) { |
| if (hasGlobalRule(data, '-dontshrink')) { |
| return getLiveItemCount(data); |
| } |
| return getScore(data, 'DONT_SHRINK'); |
| } |
| function getLiveItemCount(data) { |
| if (!data || !data.buildInfo) return 0; |
| return (data.buildInfo.liveClassCount || 0) + |
| (data.buildInfo.liveFieldCount || 0) + |
| (data.buildInfo.liveMethodCount || 0); |
| } |
| /** |
| * Returns detailed counts for the stats table. |
| */ |
| function getDetailedStats(data) { |
| if (!data) return null; |
| const build = data.buildInfo || {}; |
| const stats = { |
| classes: { total: build.liveClassCount || 0, obfuscation: 0, optimization: 0, shrinking: 0 }, |
| fields: { total: build.liveFieldCount || 0, obfuscation: 0, optimization: 0, shrinking: 0 }, |
| methods: { total: build.liveMethodCount || 0, obfuscation: 0, optimization: 0, shrinking: 0 }, |
| overall: { total: getLiveItemCount(data), obfuscation: 0, optimization: 0, shrinking: 0 } |
| }; |
| |
| const constraintsMap = getConstraintsMap(data); |
| const rulesMap = getRulesConstraintsMap(data); |
| |
| const hasObfuscate = hasGlobalRule(data, '-dontobfuscate'); |
| const hasOptimize = hasGlobalRule(data, '-dontoptimize'); |
| const hasShrink = hasGlobalRule(data, '-dontshrink'); |
| |
| const processTable = (table, key, forcedObfuscation, forcedOptimization, forcedShrinking) => { |
| if (forcedObfuscation) { |
| stats[key].obfuscation = stats[key].total; |
| } |
| if (forcedOptimization) { |
| stats[key].optimization = stats[key].total; |
| } |
| if (forcedShrinking) { |
| stats[key].shrinking = stats[key].total; |
| } |
| |
| if (!forcedObfuscation || !forcedOptimization || !forcedShrinking) { |
| if (!table) return; |
| table.forEach(item => { |
| const keptBy = item.keptBy || []; |
| const constraints = keptBy.flatMap(ruleId => constraintsMap.get(rulesMap.get(ruleId)) || []); |
| |
| if (!forcedObfuscation && constraints.includes('DONT_OBFUSCATE')) stats[key].obfuscation++; |
| if (!forcedOptimization && constraints.includes('DONT_OPTIMIZE')) stats[key].optimization++; |
| if (!forcedShrinking && constraints.includes('DONT_SHRINK')) stats[key].shrinking++; |
| }); |
| } |
| }; |
| |
| processTable(data.keptClassInfoTable, 'classes', hasObfuscate, hasOptimize, hasShrink); |
| processTable(data.keptFieldInfoTable, 'fields', hasObfuscate, hasOptimize, hasShrink); |
| processTable(data.keptMethodInfoTable, 'methods', hasObfuscate, hasOptimize, hasShrink); |
| |
| stats.overall.obfuscation = stats.classes.obfuscation + stats.fields.obfuscation + stats.methods.obfuscation; |
| stats.overall.optimization = stats.classes.optimization + stats.fields.optimization + stats.methods.optimization; |
| stats.overall.shrinking = stats.classes.shrinking + stats.fields.shrinking + stats.methods.shrinking; |
| return stats; |
| } |
| function getConstraintsMap(data) { |
| const constraintsMap = new Map(); |
| if (data.keepConstraintsTable) { |
| data.keepConstraintsTable.forEach(c => { |
| constraintsMap.set(c.id, c.constraints || []); |
| }); |
| } |
| return constraintsMap; |
| } |
| function getRulesConstraintsMap(data) { |
| const rulesMap = new Map(); |
| if (data.keepRuleKeepRadiusTable) { |
| data.keepRuleKeepRadiusTable.forEach(r => { |
| rulesMap.set(r.id, r.constraintsId); |
| }); |
| } |
| return rulesMap; |
| } |
| function getImpactArray(constraints) { |
| if (!constraints) return []; |
| return constraints; |
| } |
| /** |
| * Returns formatted rules for the table. |
| */ |
| function getRules(data) { |
| if (!data || !data.keepRuleKeepRadiusTable) return []; |
| const constraintsMap = getConstraintsMap(data); |
| const rules = data.keepRuleKeepRadiusTable.map(rule => { |
| const constraints = constraintsMap.get(rule.constraintsId); |
| const br = rule.keepRadius || {}; |
| const classes = (br.classKeepRadius || []).length; |
| const fields = (br.fieldKeepRadius || []).length; |
| const methods = (br.methodKeepRadius || []).length; |
| return { |
| id: rule.id, |
| name: rule.source, |
| impact: getImpactArray(constraints), |
| matches: { |
| classes, |
| fields, |
| methods, |
| total: classes + fields + methods |
| }, |
| subsumedBy: br.subsumedBy || [] |
| }; |
| }); |
| |
| if (data.globalKeepRuleKeepRadiusTable) { |
| const totalClasses = data.buildInfo?.liveClassCount || 0; |
| const totalFields = data.buildInfo?.liveFieldCount || 0; |
| const totalMethods = data.buildInfo?.liveMethodCount || 0; |
| const totalLive = totalClasses + totalFields + totalMethods; |
| |
| data.globalKeepRuleKeepRadiusTable.forEach(rule => { |
| if (rule.source === '-dontoptimize' || rule.source === '-dontshrink' || rule.source === '-dontobfuscate') { |
| const constraints = [...(constraintsMap.get(rule.constraintsId) || [])]; |
| if (rule.source === '-dontobfuscate' && !constraints.includes('DONT_OBFUSCATE')) { |
| constraints.push('DONT_OBFUSCATE'); |
| } |
| if (rule.source === '-dontoptimize' && !constraints.includes('DONT_OPTIMIZE')) { |
| constraints.push('DONT_OPTIMIZE'); |
| } |
| if (rule.source === '-dontshrink' && !constraints.includes('DONT_SHRINK')) { |
| constraints.push('DONT_SHRINK'); |
| } |
| rules.push({ |
| id: rule.id, |
| name: rule.source, |
| impact: getImpactArray(constraints), |
| matches: { |
| classes: totalClasses, |
| fields: totalFields, |
| methods: totalMethods, |
| total: totalLive |
| }, |
| subsumedBy: [] |
| }); |
| } |
| }); |
| } |
| return rules; |
| } |
| /** |
| * Returns formatted files (origins) for the table. |
| */ |
| function getRuleFiles(data) { |
| if (!data || !data.fileOriginTable || !data.keepRuleKeepRadiusTable) return []; |
| const fileMap = new Map(); |
| const constraintsMap = getConstraintsMap(data); |
| data.fileOriginTable.forEach(f => { |
| const mavenName = formatMavenCoordinate(f.mavenCoordinate); |
| fileMap.set(f.id, { |
| id: f.id, |
| name: mavenName || f.filename, |
| keepRules: 0, |
| matches: { classes: new Set(), fields: new Set(), methods: new Set() }, |
| impact: { |
| obfuscation: new Set(), |
| optimization: new Set(), |
| shrinking: new Set() |
| } |
| }); |
| }); |
| data.keepRuleKeepRadiusTable.forEach(rule => { |
| const fileId = rule.origin?.fileOriginId; |
| const fileEntry = fileMap.get(fileId); |
| if (fileEntry) { |
| fileEntry.keepRules++; |
| const br = rule.keepRadius || {}; |
| const c = br.classKeepRadius || []; |
| const f = br.fieldKeepRadius || []; |
| const m = br.methodKeepRadius || []; |
| c.forEach(id => fileEntry.matches.classes.add(id)); |
| f.forEach(id => fileEntry.matches.fields.add(id)); |
| m.forEach(id => fileEntry.matches.methods.add(id)); |
| const constraints = constraintsMap.get(rule.constraintsId) || []; |
| if (constraints.includes('DONT_OBFUSCATE')) { |
| c.forEach(id => fileEntry.impact.obfuscation.add('c' + id)); |
| f.forEach(id => fileEntry.impact.obfuscation.add('f' + id)); |
| m.forEach(id => fileEntry.impact.obfuscation.add('m' + id)); |
| } |
| if (constraints.includes('DONT_OPTIMIZE')) { |
| c.forEach(id => fileEntry.impact.optimization.add('c' + id)); |
| f.forEach(id => fileEntry.impact.optimization.add('f' + id)); |
| m.forEach(id => fileEntry.impact.optimization.add('m' + id)); |
| } |
| if (constraints.includes('DONT_SHRINK')) { |
| c.forEach(id => fileEntry.impact.shrinking.add('c' + id)); |
| f.forEach(id => fileEntry.impact.shrinking.add('f' + id)); |
| m.forEach(id => fileEntry.impact.shrinking.add('m' + id)); |
| } |
| } |
| }); |
| if (data.globalKeepRuleKeepRadiusTable) { |
| data.globalKeepRuleKeepRadiusTable.forEach(rule => { |
| const fileId = rule.origin?.fileOriginId; |
| const fileEntry = fileMap.get(fileId); |
| if (fileEntry) { |
| fileEntry.keepRules++; |
| } |
| }); |
| } |
| return Array.from(fileMap.values()).map(f => ({ |
| id: f.id, |
| name: f.name, |
| keepRules: f.keepRules, |
| matches: { |
| classes: f.matches.classes.size, |
| fields: f.matches.fields.size, |
| methods: f.matches.methods.size, |
| total: f.matches.classes.size + f.matches.fields.size + f.matches.methods.size |
| }, |
| impact: { |
| obfuscation: f.impact.obfuscation.size, |
| optimization: f.impact.optimization.size, |
| shrinking: f.impact.shrinking.size |
| } |
| })); |
| } |
| function formatDescriptor(desc) { |
| if (!desc) return "Unknown"; |
| let dimensions = 0; |
| while (desc[dimensions] === '[') { |
| dimensions++; |
| } |
| let base = desc.substring(dimensions); |
| let res = ""; |
| if (base.startsWith('L') && base.endsWith(';')) { |
| res = base.substring(1, base.length - 1).replace(/\//g, '.'); |
| } else if (base.length === 1) { |
| switch (base[0]) { |
| case 'V': |
| res = "void"; |
| break; |
| case 'Z': |
| res = "boolean"; |
| break; |
| case 'B': |
| res = "byte"; |
| break; |
| case 'S': |
| res = "short"; |
| break; |
| case 'C': |
| res = "char"; |
| break; |
| case 'I': |
| res = "int"; |
| break; |
| case 'J': |
| res = "long"; |
| break; |
| case 'F': |
| res = "float"; |
| break; |
| case 'D': |
| res = "double"; |
| break; |
| default: |
| res = base; |
| } |
| } else { |
| res = base; |
| } |
| for (let i = 0; i < dimensions; i++) { |
| res += "[]"; |
| } |
| return res; |
| } |
| function formatMethodName(methodRef, dataOrLookups, typeRefMap) { |
| if (!methodRef) return "Unknown method"; |
| |
| if (dataOrLookups && dataOrLookups.typeReference) { |
| const lookups = dataOrLookups; |
| const className = formatDescriptor(lookups.typeReference.get(methodRef.classReferenceId)); |
| const proto = lookups.protoReference.get(methodRef.protoReferenceId); |
| let params = ""; |
| if (proto && proto.parametersId) { |
| const list = lookups.typeReferenceList.get(proto.parametersId); |
| if (list && list.typeReferenceIds) { |
| params = list.typeReferenceIds.map(id => formatDescriptor(lookups.typeReference.get(id))).join(', '); |
| } |
| } |
| return `${className}.${methodRef.name}(${params})`; |
| } |
| |
| const data = dataOrLookups; |
| const className = formatDescriptor(typeRefMap.get(methodRef.classReferenceId)); |
| const proto = (data.protoReferenceTable || []).find(p => p.id === methodRef.protoReferenceId); |
| let params = ""; |
| if (proto && proto.parametersId) { |
| const list = (data.typeReferenceListTable || []).find(l => l.id === proto.parametersId); |
| if (list && list.typeReferenceIds) { |
| params = list.typeReferenceIds.map(id => formatDescriptor(typeRefMap.get(id))).join(', '); |
| } |
| } |
| return `${className}.${methodRef.name}(${params})`; |
| } |
| function formatFieldName(fieldRef, dataOrLookups, typeRefMap) { |
| if (!fieldRef) return "Unknown field"; |
| |
| if (dataOrLookups && dataOrLookups.typeReference) { |
| const lookups = dataOrLookups; |
| const className = formatDescriptor(lookups.typeReference.get(fieldRef.classReferenceId)); |
| return `${className}.${fieldRef.name}`; |
| } |
| |
| const className = formatDescriptor(typeRefMap.get(fieldRef.classReferenceId)); |
| return `${className}.${fieldRef.name}`; |
| } |
| function formatMavenCoordinate(m) { |
| if (!m || !m.groupId) return null; |
| return `${m.groupId}:${m.artifactId}:${m.version}`; |
| } |
| function escapeHTML(str) { |
| if (!str) return ""; |
| return str.replace(/[&<>"']/g, function (m) { |
| return { |
| '&': '&', |
| '<': '<', |
| '>': '>', |
| '"': '"', |
| "'": ''' |
| }[m]; |
| }); |
| } |
| function highlightRule(source) { |
| if (!source) return ""; |
| const escapedSource = escapeHTML(source); |
| // Highlight semicolon FIRST with negative lookbehind so we don't match inside subsequently inserted HTML tags or existing HTML entities |
| return escapedSource |
| .replace(/(?<!&[a-zA-Z0-9#]+);/g, '<span style="color: #ca8a04;">;</span>') |
| .replace(/([{}*])/g, '<span style="color: #ca8a04;">$1</span>') |
| .replace(/(-keep[a-z]*|-dontoptimize|-dontshrink|-dontobfuscate)/g, '<span style="color: #dc2626;">$1</span>') |
| .replace(/\b(class|interface|enum)\b/g, '<span style="color: #2563eb;">$1</span>'); |
| } |
| |
| /** |
| * Shared helper to count kept items that have a specific constraint. |
| * An item is counted if ANY of the keep rules that keep it has the specified constraint. |
| */ |
| function getScore(data, constraintName) { |
| if (!data) return 0; |
| const constraintsMap = getConstraintsMap(data); |
| const rulesMap = getRulesConstraintsMap(data); |
| let count = 0; |
| const tables = [ |
| data.keptClassInfoTable, |
| data.keptFieldInfoTable, |
| data.keptMethodInfoTable |
| ]; |
| tables.forEach(table => { |
| if (table) { |
| table.forEach(item => { |
| const hasRuleWithConstraint = (item.keptBy || []).some(ruleId => { |
| const constraintsId = rulesMap.get(ruleId); |
| const constraints = constraintsMap.get(constraintsId); |
| return constraints && constraints.includes(constraintName); |
| }); |
| if (hasRuleWithConstraint) { |
| count++; |
| } |
| }); |
| } |
| }); |
| return count; |
| } |