blob: dcddde09ab7f17bd7727365b70df7273b8470dd7 [file]
// 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 {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[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;
}