blob: c42b357e1ef3e8a91304c9d5fd5ca76691fa9347 [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: #b06000;">;</span>')
.replace(/([{}*])/g, '<span style="color: #b06000;">$1</span>')
.replace(
/(-keep[a-z]*|-dontoptimize|-dontshrink|-dontobfuscate)/g,
'<span style="color: #b91c1c;">$1</span>',
)
.replace(
/\b(class|interface|enum)\b/g,
'<span style="color: #1d4ed8;">$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;
}