public RenameProperty(Master input, AccessWithName expression, String newName) { super(input); this.newName = newName; if (!AstUtil.isName(newName)) { log.fatal("New name is invalid"); return; } // if the expression is unreachable, don't bother // if (input.getAccessedObjects(expression).isEmpty()) // log.warn(expression.getName(), "Expression is unreachable"); // if (expression.getDirectReceivers(input).isEmpty()) { // log.warn(expression.getName(), "Expression is unreachable"); // } String oldName = expression.getName(); NodeFinder finder = new NodeFinder( input, IPropertyAccessNode.class, ANameExp.class, ANormalObjectLiteralProperty.class, ABinopExp.class); List<AccessWithName> nodesWithNewName = AccessFinder.getNamedAccesses(finder, newName); Set<ABinopExp> dynamicInExps = new HashSet<ABinopExp>(); // find nodes with new name and dynamic in expressions for (ABinopExp exp : finder.getAllNodesOfType(ABinopExp.class)) { if (exp.getOp().kindPBinop() != EBinop.IN) continue; PExp left = exp.getLeft(); if (!(left instanceof AConstExp)) { dynamicInExps.add(exp); continue; // non-constant node cannot be renamed } AConstExp leftc = (AConstExp) left; if (!(leftc.getConst() instanceof AStringConst)) continue; // int and boolean are safe AStringConst sc = (AStringConst) leftc.getConst(); String name = Literals.parseStringLiteral(sc.getStringLiteral().getText()); if (name.equals(newName)) nodesWithNewName.add(new ConstInExpAccess(sc, exp)); } List<Access> propertySensitiveNodes = new ArrayList<Access>(); propertySensitiveNodes.addAll(AccessFinder.getNamedAccesses(finder, oldName)); ForInDynamicAccesses forinAccess = getForInDynamicAccesses(input, finder); for (ADynamicPropertyExp exp : forinAccess.getSafeExps()) { propertySensitiveNodes.add(new DynamicPropertyExpAccess(exp)); } affected = FamilyClosure.compute( new RenamingFamily(input, oldName, forinAccess), propertySensitiveNodes, Collections.singleton(expression), Collections.<Host>emptySet()); affectedObjects = ObjectHost.unwrap(CollectionUtil.filter(affected.getAffectedObjects(), ObjectHost.class)); affectedScopes = CollectionUtil.filter(affected.getAffectedObjects(), ScopeHost.class); Set<IFunction> violatingNatives = new HashSet<IFunction>(); for (AccessWithoutName access : getAffectedDynamicAccesses()) { if (input.isNativeCode(access.getNode())) continue; boolean safe = false; if (access instanceof DynamicPropertyExpAccess) { DynamicPropertyExpAccess dyn = (DynamicPropertyExpAccess) access; if (input.isDefinitelyArrayLookup(dyn.getExp()) || forinAccess.getSafeExps().contains(dyn.getExp())) { safe = true; } } if (safe) continue; log.warn( access.getNode(), "Access %s might be affected, but cannot be updated.", access.getNode()); } for (ADynamicPropertyExp exp : finder.getAllNodesOfType(ADynamicPropertyExp.class)) { if (input.isDefinitelyArrayLookup(exp) || forinAccess.getSafeExps().contains(exp)) continue; Set<ObjectValue> receivers = input.getReceivers(exp); if (CollectionUtil.intersects(receivers, affectedObjects)) if (input.isNativeCode(exp)) { violatingNatives.add(exp.getAncestor(IFunction.class)); } else { log.warn(exp, "Expression %s might be affected, but cannot be updated.", exp); } } // check for for-in loop conflicts for (AForInStmt forin : finder.getAllNodesOfType(AForInStmt.class)) { if (forinAccess.getSafeForIns().contains(forin)) { continue; } Set<ObjectValue> receivers = input.getAllPrototypes(input.getForInObjects(forin), true); if (CollectionUtil.intersects(receivers, affectedObjects)) { if (input.isNativeCode(forin)) { violatingNatives.add(forin.getAncestor(IFunction.class)); } else { log.warn(forin, "Property name produced by for-in loop may change"); } } } // check for dynamic 'in' expression conflicts for (ABinopExp exp : dynamicInExps) { if (input.isNativeCode(exp)) continue; Set<ObjectValue> objs = input.getInExpObjectArgs(exp); if (CollectionUtil.intersects(objs, affectedObjects)) log.warn(exp, "The expression %s might be affected, but cannot be updated.", exp); } // check if new name clashes with existing properties for (AccessWithName node : this.getAffectedNames()) { if (input.isNativeCode(node.getNode())) continue; Set<ObjectValue> base = node.getBase(input, newName); if (!base.isEmpty()) { log.error( node.getNode(), "Renaming expression %s may clash with existing property of name %s.", node.getNode(), newName); } if (node instanceof AccessVariable) { // note: the check with getBase above took care of conflicting with-scopes and global object // properties AccessVariable varAccess = (AccessVariable) node; for (ScopeHost scope : varAccess.getSearchedScopes(input, oldName)) { if (scope.isGlobal()) { continue; } if (scope.getScope().getDeclaredVariables().contains(newName)) { log.error( node.getNode(), "Renaming expression %s will change resolution of variable %s.", node.getNode(), newName); } } } } // check if existing accesses with the new name are affected for (AccessWithName node : nodesWithNewName) { if (input.isNativeCode(node.getNode())) continue; Set<ObjectValue> receivers = node.getReceivers(input); if (CollectionUtil.intersects(receivers, affectedObjects)) { log.error(node.getNode(), "Expression %s may clash with renamed property.", node.getNode()); } else if (node instanceof AccessVariable) { AccessVariable varAccess = (AccessVariable) node; if (CollectionUtil.intersects( new HashSet<ScopeHost>(varAccess.getSearchedScopes(input)), affectedScopes)) { log.error( node.getNode(), "Expression %s may clash with renamed property.", node.getNode()); } } } // check for renaming native stuff in the harness files for (AccessWithName node : this.getAffectedNames()) { if (input.isNativeCode(node.getNode())) log.error("Native property %s cannot be renamed", node.getNode()); } // check for renaming native stuff not in the harness files // XXX The difference between code in harness files and internal natives is quite // analysis-specific - can it be abstracted?? for (NativeFunctionValue nativ : JSUtil.NATIVE_FUNCTIONS) { if (affectedObjects.contains(nativ.getFunctionPrototype()) && nativ.getNativeMembers().contains(oldName)) log.error("Native property %s.%s cannot be renamed.", nativ.getPrettyName(), oldName); } // check for renaming of toString, valueOf or constructor if (oldName.equals("toString") || oldName.equals("valueOf")) log.warn("Renaming %s may affect implicit coercions.", oldName); if (newName.equals("toString") || newName.equals("valueOf")) log.warn("Renaming to %s may affect implicit coersions", newName); if (oldName.equals("constructor") || newName.equals("constructor")) log.warn("Renaming property from/to 'constructor' may affect instanceof."); if (CollectionUtil.containsInstanceOf(affectedObjects, FunctionValue.class) && (oldName.equals("prototype") || newName.equals("prototype"))) { log.warn("Cannot rename 'prototype' property."); } boolean coercedStringsAreAffected = affectedObjects.contains(new CoercedPrimitiveObjectValue(StringValue.Instance)); boolean isLength = oldName.equals("length") || newName.equals("length"); if (coercedStringsAreAffected && isLength) { log.error("Cannot rename String.length property"); } boolean arraysAreAffected = input .getAllPrototypes(affectedObjects, false) .contains( new UserFunctionValue(input.getHarnessNativeFunction("Array"), MainContext.Instance) .getFunctionPrototype()); if (arraysAreAffected && isLength) { log.error("Cannot rename Array.length property"); } // check for calling a native function that performs dynamic property access violatingNatives.addAll(findNativeDynamicAccesses(affectedObjects)); for (IFunction func : violatingNatives) { log.warn( "Renamed property may be dynamically accessed by native function %s", getNativeFunctionName(func)); } checkEval(input); // TODO more conflict checks? }
@Override public List<ScopeHost> getSearchedScopes(Master input, String name) { return Collections.singletonList( ScopeHost.fromScope(input.getScope(decl.getAncestor(ABody.class)))); }