Skip to content

Commit 0f94b98

Browse files
Closure Teamcopybara-github
authored andcommitted
Improve constant inference in BanEelementSetAttribute
PiperOrigin-RevId: 552261964
1 parent 8036074 commit 0f94b98

File tree

2 files changed

+102
-25
lines changed

2 files changed

+102
-25
lines changed

src/com/google/javascript/jscomp/ConformanceRules.java

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import java.util.List;
6262
import java.util.Locale;
6363
import java.util.Set;
64+
import java.util.function.Supplier;
6465
import java.util.regex.Pattern;
6566
import java.util.regex.PatternSyntaxException;
6667
import org.jspecify.nullness.Nullable;
@@ -1052,7 +1053,26 @@ private static String removeTypeDecl(String specName) throws InvalidRequirementS
10521053
return specName.substring(index + 1);
10531054
}
10541055

1055-
private static @Nullable String inferStringValue(@Nullable Scope scope, Node node) {
1056+
private static boolean isAnnotatedAsConst(JSDocInfo jsdoc) {
1057+
return jsdoc != null && jsdoc.isConstant();
1058+
}
1059+
1060+
private static @Nullable String getValueFromGlobalName(
1061+
@Nullable Scope scope, GlobalNamespace gns, String qualifiedName) {
1062+
GlobalNamespace.Name gn = gns.getOwnSlot(qualifiedName);
1063+
if (gn == null
1064+
|| gn.getGlobalSets() != 1
1065+
|| gn.getTotalSets() != 1
1066+
|| !isAnnotatedAsConst(gn.getJSDocInfo())) {
1067+
return null;
1068+
}
1069+
1070+
return inferStringValue(
1071+
scope, NodeUtil.getRValueOfLValue(gn.getDeclaration().getNode()), () -> gns);
1072+
}
1073+
1074+
private static @Nullable String inferStringValue(
1075+
@Nullable Scope scope, Node node, Supplier<GlobalNamespace> globalNamespaceSupplier) {
10561076
if (node == null) {
10571077
return null;
10581078
}
@@ -1072,35 +1092,43 @@ private static String removeTypeDecl(String specName) throws InvalidRequirementS
10721092
if (var == null) {
10731093
return null;
10741094
}
1075-
if (!var.isConst()) {
1076-
JSDocInfo jsdocInfo = var.getJSDocInfo();
1077-
if (jsdocInfo == null || !jsdocInfo.isConstant()) {
1078-
return null;
1079-
}
1095+
if (!var.isConst() && !isAnnotatedAsConst(var.getJSDocInfo())) {
1096+
return null;
10801097
}
10811098
Node initialValue = var.getInitialValue();
1082-
return inferStringValue(var.getScope(), initialValue);
1099+
return inferStringValue(var.getScope(), initialValue, globalNamespaceSupplier);
10831100
case GETPROP:
10841101
JSType type = node.getJSType();
1085-
if (type == null || !type.isEnumElementType()) {
1086-
// For simplicity, only support enums. The JS style guide requires enums to be
1087-
// effectively immutable and all enum items should be statically known.
1088-
// See go/js-style#features-objects-enums.
1102+
if (type == null) {
10891103
return null;
10901104
}
10911105

1092-
Node enumSource = type.toMaybeEnumElementType().getEnumType().getSource();
1093-
if (enumSource == null) {
1094-
return null;
1095-
}
1096-
if (!enumSource.isObjectLit() && !enumSource.isClassMembers()) {
1097-
return null;
1106+
// The JS style guide requires enums to be effectively immutable and all enum items should
1107+
// be statically known. See go/js-style#features-objects-enums.
1108+
if (type.isEnumElementType()) {
1109+
Node enumSource = type.toMaybeEnumElementType().getEnumType().getSource();
1110+
if (enumSource == null) {
1111+
return null;
1112+
}
1113+
if (!enumSource.isObjectLit() && !enumSource.isClassMembers()) {
1114+
return null;
1115+
}
1116+
return inferStringValue(
1117+
null,
1118+
NodeUtil.getFirstPropMatchingKey(enumSource, node.getString()),
1119+
globalNamespaceSupplier);
1120+
} else if (type.isString()) {
1121+
GlobalNamespace gns = globalNamespaceSupplier.get();
1122+
if (gns == null) {
1123+
return null;
1124+
}
1125+
return getValueFromGlobalName(scope, gns, node.getQualifiedName());
10981126
}
1099-
return inferStringValue(
1100-
null, NodeUtil.getFirstPropMatchingKey(enumSource, node.getString()));
1101-
default:
11021127
return null;
1128+
default:
1129+
// Do nothing
11031130
}
1131+
return null;
11041132
}
11051133

11061134
private static boolean isXid(JSType type) {
@@ -2316,14 +2344,25 @@ public static final class SecuritySensitiveAttributes {
23162344
"data");
23172345

23182346
private final ImmutableSet<String> bannedAtrrs;
2347+
private final Supplier<GlobalNamespace> globalNamespaceSupplier;
23192348

23202349
public SecuritySensitiveAttributes() {
2321-
this.bannedAtrrs = ALL_BANNED_ATTRS;
2350+
this(ALL_BANNED_ATTRS, () -> null);
2351+
}
2352+
2353+
public SecuritySensitiveAttributes(Supplier<GlobalNamespace> globalNamespaceSupplier) {
2354+
this(ALL_BANNED_ATTRS, globalNamespaceSupplier);
23222355
}
23232356

23242357
public SecuritySensitiveAttributes(Collection<String> bannedAtrrs) {
2358+
this(bannedAtrrs, () -> null);
2359+
}
2360+
2361+
public SecuritySensitiveAttributes(
2362+
Collection<String> bannedAtrrs, Supplier<GlobalNamespace> globalNamespaceSupplier) {
23252363
this.bannedAtrrs =
23262364
bannedAtrrs.stream().map(s -> s.toLowerCase(Locale.ROOT)).collect(toImmutableSet());
2365+
this.globalNamespaceSupplier = globalNamespaceSupplier;
23272366
}
23282367

23292368
/**
@@ -2343,7 +2382,8 @@ public boolean contains(String attributeName) {
23432382
*/
23442383
public ConformanceResult checkConformanceForAttributeName(
23452384
NodeTraversal traversal, Node attrName) {
2346-
String literalName = ConformanceUtil.inferStringValue(traversal.getScope(), attrName);
2385+
String literalName =
2386+
ConformanceUtil.inferStringValue(traversal.getScope(), attrName, globalNamespaceSupplier);
23472387
if (literalName == null) {
23482388
// xid() obfuscates attribute names, thus never clashing with security-sensitive attributes.
23492389
return ConformanceUtil.isXid(attrName.getJSType())
@@ -2368,7 +2408,8 @@ public ConformanceResult checkConformanceForAttributeName(
23682408
*/
23692409
public ConformanceResult checkConformanceForAttributeNameWithHighConfidence(
23702410
NodeTraversal traversal, Node attrName) {
2371-
String literalName = ConformanceUtil.inferStringValue(traversal.getScope(), attrName);
2411+
String literalName =
2412+
ConformanceUtil.inferStringValue(traversal.getScope(), attrName, globalNamespaceSupplier);
23722413
if (literalName == null) {
23732414
return ConformanceResult.CONFORMANCE;
23742415
}
@@ -2396,6 +2437,15 @@ public static final class BanElementSetAttribute extends AbstractRule {
23962437
private static final ImmutableSet<String> BANNED_PROPERTIES =
23972438
ImmutableSet.of(SET_ATTRIBUTE, SET_ATTRIBUTE_NS, SET_ATTRIBUTE_NODE, SET_ATTRIBUTE_NODE_NS);
23982439

2440+
private boolean performGlobalNamespaceAnalysis = true;
2441+
private GlobalNamespace globalNamespace;
2442+
2443+
/**
2444+
* The cap for script size, beyond which we give up performing global namespace analysis due to
2445+
* exccessive performance cost. The value is purely herustic and subject to future regulation.
2446+
*/
2447+
private static final int GLOBAL_NAMESPACE_ANALYSIS_LIMIT = 10000;
2448+
23992449
private final JSType elementType;
24002450
private final SecuritySensitiveAttributes securitySensitiveAttributes;
24012451
private final ConformanceResult defaultDecisionForUncertainCases;
@@ -2409,14 +2459,31 @@ public static final class BanElementSetAttribute extends AbstractRule {
24092459
public BanElementSetAttribute(AbstractCompiler compiler, Requirement requirement)
24102460
throws InvalidRequirementSpec {
24112461
super(compiler, requirement);
2412-
securitySensitiveAttributes = new SecuritySensitiveAttributes(requirement.getValueList());
2462+
securitySensitiveAttributes =
2463+
new SecuritySensitiveAttributes(requirement.getValueList(), this::getGlobalNamespace);
24132464
defaultDecisionForUncertainCases =
24142465
requirement.getReportLooseTypeViolations()
24152466
? ConformanceResult.VIOLATION
24162467
: ConformanceResult.CONFORMANCE;
24172468
elementType = compiler.getTypeRegistry().getGlobalType(ELEMENT_TYPE_NAME);
24182469
}
24192470

2471+
/**
2472+
* Build GlobalNamespace starting from the Root node of the input, excluding externs. Skip the
2473+
* build if the input AST is too large.
2474+
*/
2475+
@Nullable GlobalNamespace getGlobalNamespace() {
2476+
if (performGlobalNamespaceAnalysis && globalNamespace == null) {
2477+
if (NodeUtil.countAstSizeUpToLimit(compiler.getJsRoot(), GLOBAL_NAMESPACE_ANALYSIS_LIMIT)
2478+
>= GLOBAL_NAMESPACE_ANALYSIS_LIMIT) {
2479+
performGlobalNamespaceAnalysis = false;
2480+
} else {
2481+
globalNamespace = new GlobalNamespace(compiler, compiler.getJsRoot());
2482+
}
2483+
}
2484+
return globalNamespace;
2485+
}
2486+
24202487
@Override
24212488
protected ConformanceResult checkConformance(NodeTraversal traversal, Node node) {
24222489
if (node.isCall()) {
@@ -2502,7 +2569,8 @@ private Optional<String> getBannedPropertyName(Node callNode) {
25022569
private ConformanceResult checkConformanceOnGetElement(
25032570
NodeTraversal traversal, Node getElementNode) {
25042571
Node key = getElementNode.getSecondChild();
2505-
String keyName = ConformanceUtil.inferStringValue(traversal.getScope(), key);
2572+
String keyName =
2573+
ConformanceUtil.inferStringValue(traversal.getScope(), key, this::getGlobalNamespace);
25062574
if (keyName != null) {
25072575
if (securitySensitiveAttributes.contains(keyName.toLowerCase(Locale.ROOT))
25082576
|| (requirement.getReportLooseTypeViolations()

test/com/google/javascript/jscomp/CheckConformanceTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3696,6 +3696,15 @@ public void testBanElementSetAttributeLoose() {
36963696
"var foo = 'safe';",
36973697
"(new HTMLScriptElement).setAttribute(foo, 'xxx');")));
36983698

3699+
testNoWarning(
3700+
externs(externs),
3701+
srcs(
3702+
lines(
3703+
"goog.provide('test.Attribute');",
3704+
"/** @const */",
3705+
"test.Attribute.foo = 'safe';",
3706+
"(new HTMLScriptElement).setAttribute(test.Attribute.foo, 'xxx');")));
3707+
36993708
testNoWarning(
37003709
externs(externs),
37013710
srcs(

0 commit comments

Comments
 (0)