| OLD | NEW |
| (Empty) |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 package com.google.javascript.jscomp; | |
| 6 | |
| 7 import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; | |
| 8 import com.google.javascript.rhino.IR; | |
| 9 import com.google.javascript.rhino.JSDocInfoBuilder; | |
| 10 import com.google.javascript.rhino.JSTypeExpression; | |
| 11 import com.google.javascript.rhino.Node; | |
| 12 import com.google.javascript.rhino.Token; | |
| 13 | |
| 14 import java.util.ArrayList; | |
| 15 import java.util.Arrays; | |
| 16 import java.util.HashMap; | |
| 17 import java.util.HashSet; | |
| 18 import java.util.List; | |
| 19 import java.util.Map; | |
| 20 import java.util.Set; | |
| 21 | |
| 22 /** | |
| 23 * Compiler pass for Chrome-specific needs. It handles the following Chrome JS f
eatures: | |
| 24 * <ul> | |
| 25 * <li>namespace declaration using {@code cr.define()}, | |
| 26 * <li>unquoted property declaration using {@code {cr|Object}.defineProperty()}. | |
| 27 * </ul> | |
| 28 * | |
| 29 * <p>For the details, see tests inside ChromePassTest.java. | |
| 30 */ | |
| 31 public class ChromePass extends AbstractPostOrderCallback implements CompilerPas
s { | |
| 32 final AbstractCompiler compiler; | |
| 33 | |
| 34 private Set<String> createdObjects; | |
| 35 | |
| 36 private static final String CR_DEFINE = "cr.define"; | |
| 37 private static final String CR_EXPORT_PATH = "cr.exportPath"; | |
| 38 private static final String OBJECT_DEFINE_PROPERTY = "Object.defineProperty"
; | |
| 39 private static final String CR_DEFINE_PROPERTY = "cr.defineProperty"; | |
| 40 private static final String CR_MAKE_PUBLIC = "cr.makePublic"; | |
| 41 | |
| 42 private static final String CR_DEFINE_COMMON_EXPLANATION = "It should be cal
led like this:" | |
| 43 + " cr.define('name.space', function() '{ ... return {Export: Intern
al}; }');"; | |
| 44 | |
| 45 static final DiagnosticType CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS = | |
| 46 DiagnosticType.error("JSC_CR_DEFINE_WRONG_NUMBER_OF_ARGUMENTS", | |
| 47 "cr.define() should have exactly 2 arguments. " + CR_DEFINE_
COMMON_EXPLANATION); | |
| 48 | |
| 49 static final DiagnosticType CR_EXPORT_PATH_TOO_FEW_ARGUMENTS = | |
| 50 DiagnosticType.error("JSC_CR_EXPORT_PATH_TOO_FEW_ARGUMENTS", | |
| 51 "cr.exportPath() should have at least 1 argument: path name.
"); | |
| 52 | |
| 53 static final DiagnosticType CR_DEFINE_INVALID_FIRST_ARGUMENT = | |
| 54 DiagnosticType.error("JSC_CR_DEFINE_INVALID_FIRST_ARGUMENT", | |
| 55 "Invalid first argument for cr.define(). " + CR_DEFINE_COMMO
N_EXPLANATION); | |
| 56 | |
| 57 static final DiagnosticType CR_DEFINE_INVALID_SECOND_ARGUMENT = | |
| 58 DiagnosticType.error("JSC_CR_DEFINE_INVALID_SECOND_ARGUMENT", | |
| 59 "Invalid second argument for cr.define(). " + CR_DEFINE_COMM
ON_EXPLANATION); | |
| 60 | |
| 61 static final DiagnosticType CR_DEFINE_INVALID_RETURN_IN_FUNCTION = | |
| 62 DiagnosticType.error("JSC_CR_DEFINE_INVALID_RETURN_IN_SECOND_ARGUMEN
T", | |
| 63 "Function passed as second argument of cr.define() should re
turn the" | |
| 64 + " dictionary in its last statement. " + CR_DEFINE_COMMON_E
XPLANATION); | |
| 65 | |
| 66 static final DiagnosticType CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND = | |
| 67 DiagnosticType.error("JSC_CR_DEFINE_PROPERTY_INVALID_PROPERTY_KIND", | |
| 68 "Invalid cr.PropertyKind passed to cr.defineProperty(): expe
cted ATTR," | |
| 69 + " BOOL_ATTR or JS, found \"{0}\"."); | |
| 70 | |
| 71 static final DiagnosticType CR_MAKE_PUBLIC_HAS_NO_JSDOC = | |
| 72 DiagnosticType.error("JSC_CR_MAKE_PUBLIC_HAS_NO_JSDOC", | |
| 73 "Private method exported by cr.makePublic() has no JSDoc."); | |
| 74 | |
| 75 static final DiagnosticType CR_MAKE_PUBLIC_MISSED_DECLARATION = | |
| 76 DiagnosticType.error("JSC_CR_MAKE_PUBLIC_MISSED_DECLARATION", | |
| 77 "Method \"{1}_\" exported by cr.makePublic() on \"{0}\" has
no declaration."); | |
| 78 | |
| 79 static final DiagnosticType CR_MAKE_PUBLIC_INVALID_SECOND_ARGUMENT = | |
| 80 DiagnosticType.error("JSC_CR_MAKE_PUBLIC_INVALID_SECOND_ARGUMENT", | |
| 81 "Invalid second argument passed to cr.makePublic(): should b
e array of " + | |
| 82 "strings."); | |
| 83 | |
| 84 public ChromePass(AbstractCompiler compiler) { | |
| 85 this.compiler = compiler; | |
| 86 // The global variable "cr" is declared in ui/webui/resources/js/cr.js. | |
| 87 this.createdObjects = new HashSet<>(Arrays.asList("cr")); | |
| 88 } | |
| 89 | |
| 90 @Override | |
| 91 public void process(Node externs, Node root) { | |
| 92 NodeTraversal.traverse(compiler, root, this); | |
| 93 } | |
| 94 | |
| 95 @Override | |
| 96 public void visit(NodeTraversal t, Node node, Node parent) { | |
| 97 if (node.isCall()) { | |
| 98 Node callee = node.getFirstChild(); | |
| 99 if (callee.matchesQualifiedName(CR_DEFINE)) { | |
| 100 visitNamespaceDefinition(node, parent); | |
| 101 compiler.reportCodeChange(); | |
| 102 } else if (callee.matchesQualifiedName(CR_EXPORT_PATH)) { | |
| 103 visitExportPath(node, parent); | |
| 104 compiler.reportCodeChange(); | |
| 105 } else if (callee.matchesQualifiedName(OBJECT_DEFINE_PROPERTY) || | |
| 106 callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) { | |
| 107 visitPropertyDefinition(node, parent); | |
| 108 compiler.reportCodeChange(); | |
| 109 } else if (callee.matchesQualifiedName(CR_MAKE_PUBLIC)) { | |
| 110 if (visitMakePublic(node, parent)) { | |
| 111 compiler.reportCodeChange(); | |
| 112 } | |
| 113 } | |
| 114 } | |
| 115 } | |
| 116 | |
| 117 private void visitPropertyDefinition(Node call, Node parent) { | |
| 118 Node callee = call.getFirstChild(); | |
| 119 String target = call.getChildAtIndex(1).getQualifiedName(); | |
| 120 if (callee.matchesQualifiedName(CR_DEFINE_PROPERTY) && !target.endsWith(
".prototype")) { | |
| 121 target += ".prototype"; | |
| 122 } | |
| 123 | |
| 124 Node property = call.getChildAtIndex(2); | |
| 125 | |
| 126 Node getPropNode = NodeUtil.newQName( | |
| 127 compiler, target + "." + property.getString()).srcrefTree(call); | |
| 128 | |
| 129 if (callee.matchesQualifiedName(CR_DEFINE_PROPERTY)) { | |
| 130 setJsDocWithType(getPropNode, getTypeByCrPropertyKind(call.getChildA
tIndex(3))); | |
| 131 } else { | |
| 132 setJsDocWithType(getPropNode, new Node(Token.QMARK)); | |
| 133 } | |
| 134 | |
| 135 Node definitionNode = IR.exprResult(getPropNode).srcref(parent); | |
| 136 | |
| 137 parent.getParent().addChildAfter(definitionNode, parent); | |
| 138 } | |
| 139 | |
| 140 private Node getTypeByCrPropertyKind(Node propertyKind) { | |
| 141 if (propertyKind == null || propertyKind.matchesQualifiedName("cr.Proper
tyKind.JS")) { | |
| 142 return new Node(Token.QMARK); | |
| 143 } | |
| 144 if (propertyKind.matchesQualifiedName("cr.PropertyKind.ATTR")) { | |
| 145 return IR.string("string"); | |
| 146 } | |
| 147 if (propertyKind.matchesQualifiedName("cr.PropertyKind.BOOL_ATTR")) { | |
| 148 return IR.string("boolean"); | |
| 149 } | |
| 150 compiler.report(JSError.make(propertyKind, CR_DEFINE_PROPERTY_INVALID_PR
OPERTY_KIND, | |
| 151 propertyKind.getQualifiedName())); | |
| 152 return null; | |
| 153 } | |
| 154 | |
| 155 private void setJsDocWithType(Node target, Node type) { | |
| 156 JSDocInfoBuilder builder = new JSDocInfoBuilder(false); | |
| 157 builder.recordType(new JSTypeExpression(type, "")); | |
| 158 target.setJSDocInfo(builder.build()); | |
| 159 } | |
| 160 | |
| 161 private boolean visitMakePublic(Node call, Node exprResult) { | |
| 162 boolean changesMade = false; | |
| 163 Node scope = exprResult.getParent(); | |
| 164 String className = call.getChildAtIndex(1).getQualifiedName(); | |
| 165 String prototype = className + ".prototype"; | |
| 166 Node methods = call.getChildAtIndex(2); | |
| 167 | |
| 168 if (methods == null || !methods.isArrayLit()) { | |
| 169 compiler.report(JSError.make(exprResult, CR_MAKE_PUBLIC_INVALID_SECO
ND_ARGUMENT)); | |
| 170 return changesMade; | |
| 171 } | |
| 172 | |
| 173 Set<String> methodNames = new HashSet<>(); | |
| 174 for (Node methodName: methods.children()) { | |
| 175 if (!methodName.isString()) { | |
| 176 compiler.report(JSError.make(methodName, CR_MAKE_PUBLIC_INVALID_
SECOND_ARGUMENT)); | |
| 177 return changesMade; | |
| 178 } | |
| 179 methodNames.add(methodName.getString()); | |
| 180 } | |
| 181 | |
| 182 for (Node child: scope.children()) { | |
| 183 if (isAssignmentToPrototype(child, prototype)) { | |
| 184 Node objectLit = child.getFirstChild().getChildAtIndex(1); | |
| 185 for (Node stringKey : objectLit.children()) { | |
| 186 String field = stringKey.getString(); | |
| 187 changesMade |= maybeAddPublicDeclaration(field, methodNames,
className, | |
| 188 stringKey, scope, e
xprResult); | |
| 189 } | |
| 190 } else if (isAssignmentToPrototypeMethod(child, prototype)) { | |
| 191 Node assignNode = child.getFirstChild(); | |
| 192 String qualifiedName = assignNode.getFirstChild().getQualifiedNa
me(); | |
| 193 String field = qualifiedName.substring(qualifiedName.lastIndexOf
('.') + 1); | |
| 194 changesMade |= maybeAddPublicDeclaration(field, methodNames, cla
ssName, | |
| 195 assignNode, scope, expr
Result); | |
| 196 } else if (isDummyPrototypeMethodDeclaration(child, prototype)) { | |
| 197 String qualifiedName = child.getFirstChild().getQualifiedName(); | |
| 198 String field = qualifiedName.substring(qualifiedName.lastIndexOf
('.') + 1); | |
| 199 changesMade |= maybeAddPublicDeclaration(field, methodNames, cla
ssName, | |
| 200 child.getFirstChild(),
scope, exprResult); | |
| 201 } | |
| 202 } | |
| 203 | |
| 204 for (String missedDeclaration : methodNames) { | |
| 205 compiler.report(JSError.make(exprResult, CR_MAKE_PUBLIC_MISSED_DECLA
RATION, className, | |
| 206 missedDeclaration)); | |
| 207 } | |
| 208 | |
| 209 return changesMade; | |
| 210 } | |
| 211 | |
| 212 private boolean isAssignmentToPrototype(Node node, String prototype) { | |
| 213 Node assignNode; | |
| 214 return node.isExprResult() && (assignNode = node.getFirstChild()).isAssi
gn() && | |
| 215 assignNode.getFirstChild().getQualifiedName().equals(prototype); | |
| 216 } | |
| 217 | |
| 218 private boolean isAssignmentToPrototypeMethod(Node node, String prototype) { | |
| 219 Node assignNode; | |
| 220 return node.isExprResult() && (assignNode = node.getFirstChild()).isAssi
gn() && | |
| 221 assignNode.getFirstChild().getQualifiedName().startsWith(prototy
pe + "."); | |
| 222 } | |
| 223 | |
| 224 private boolean isDummyPrototypeMethodDeclaration(Node node, String prototyp
e) { | |
| 225 Node getPropNode; | |
| 226 return node.isExprResult() && (getPropNode = node.getFirstChild()).isGet
Prop() && | |
| 227 getPropNode.getQualifiedName().startsWith(prototype + "."); | |
| 228 } | |
| 229 | |
| 230 private boolean maybeAddPublicDeclaration(String field, Set<String> publicAP
IStrings, | |
| 231 String className, Node jsDocSourceNode, Node scope, Node exprResult)
{ | |
| 232 boolean changesMade = false; | |
| 233 if (field.endsWith("_")) { | |
| 234 String publicName = field.substring(0, field.length() - 1); | |
| 235 if (publicAPIStrings.contains(publicName)) { | |
| 236 Node methodDeclaration = NodeUtil.newQName(compiler, className +
"." + publicName); | |
| 237 if (jsDocSourceNode.getJSDocInfo() != null) { | |
| 238 methodDeclaration.setJSDocInfo(jsDocSourceNode.getJSDocInfo(
)); | |
| 239 scope.addChildBefore( | |
| 240 IR.exprResult(methodDeclaration).srcrefTree(exprResu
lt), | |
| 241 exprResult); | |
| 242 changesMade = true; | |
| 243 } else { | |
| 244 compiler.report(JSError.make(jsDocSourceNode, CR_MAKE_PUBLIC
_HAS_NO_JSDOC)); | |
| 245 } | |
| 246 publicAPIStrings.remove(publicName); | |
| 247 } | |
| 248 } | |
| 249 return changesMade; | |
| 250 } | |
| 251 | |
| 252 private void visitExportPath(Node crExportPathNode, Node parent) { | |
| 253 if (crExportPathNode.getChildCount() < 2) { | |
| 254 compiler.report(JSError.make(crExportPathNode, CR_EXPORT_PATH_TOO_FE
W_ARGUMENTS)); | |
| 255 return; | |
| 256 } | |
| 257 | |
| 258 Node pathArg = crExportPathNode.getChildAtIndex(1); | |
| 259 if (pathArg.isString()) { | |
| 260 // TODO(dbeam): support cr.exportPath('ns').value. | |
| 261 createAndInsertObjectsForQualifiedName(parent, pathArg.getString()); | |
| 262 } | |
| 263 } | |
| 264 | |
| 265 private void createAndInsertObjectsForQualifiedName(Node scriptChild, String
namespace) { | |
| 266 List<Node> objectsForQualifiedName = createObjectsForQualifiedName(names
pace); | |
| 267 for (Node n : objectsForQualifiedName) { | |
| 268 scriptChild.getParent().addChildBefore(n, scriptChild); | |
| 269 } | |
| 270 } | |
| 271 | |
| 272 private void visitNamespaceDefinition(Node crDefineCallNode, Node parent) { | |
| 273 if (crDefineCallNode.getChildCount() != 3) { | |
| 274 compiler.report(JSError.make(crDefineCallNode, CR_DEFINE_WRONG_NUMBE
R_OF_ARGUMENTS)); | |
| 275 } | |
| 276 | |
| 277 Node namespaceArg = crDefineCallNode.getChildAtIndex(1); | |
| 278 Node function = crDefineCallNode.getChildAtIndex(2); | |
| 279 | |
| 280 if (!namespaceArg.isString()) { | |
| 281 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_FIRST_A
RGUMENT)); | |
| 282 return; | |
| 283 } | |
| 284 | |
| 285 // TODO(vitalyp): Check namespace name for validity here. It should be a
valid chain of | |
| 286 // identifiers. | |
| 287 String namespace = namespaceArg.getString(); | |
| 288 | |
| 289 createAndInsertObjectsForQualifiedName(parent, namespace); | |
| 290 | |
| 291 if (!function.isFunction()) { | |
| 292 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_SECOND_
ARGUMENT)); | |
| 293 return; | |
| 294 } | |
| 295 | |
| 296 Node returnNode, objectLit; | |
| 297 Node functionBlock = function.getLastChild(); | |
| 298 if ((returnNode = functionBlock.getLastChild()) == null || | |
| 299 !returnNode.isReturn() || | |
| 300 (objectLit = returnNode.getFirstChild()) == null || | |
| 301 !objectLit.isObjectLit()) { | |
| 302 compiler.report(JSError.make(namespaceArg, CR_DEFINE_INVALID_RETURN_
IN_FUNCTION)); | |
| 303 return; | |
| 304 } | |
| 305 | |
| 306 Map<String, String> exports = objectLitToMap(objectLit); | |
| 307 | |
| 308 NodeTraversal.traverse(compiler, functionBlock, new RenameInternalsToExt
ernalsCallback( | |
| 309 namespace, exports, functionBlock)); | |
| 310 } | |
| 311 | |
| 312 private Map<String, String> objectLitToMap(Node objectLit) { | |
| 313 Map<String, String> res = new HashMap<String, String>(); | |
| 314 | |
| 315 for (Node keyNode : objectLit.children()) { | |
| 316 String key = keyNode.getString(); | |
| 317 | |
| 318 Node valueNode = keyNode.getFirstChild(); | |
| 319 if (valueNode.isName()) { | |
| 320 String value = keyNode.getFirstChild().getString(); | |
| 321 res.put(value, key); | |
| 322 } | |
| 323 } | |
| 324 | |
| 325 return res; | |
| 326 } | |
| 327 | |
| 328 /** | |
| 329 * For a string "a.b.c" produce the following JS IR: | |
| 330 * | |
| 331 * <p><pre> | |
| 332 * var a = a || {}; | |
| 333 * a.b = a.b || {}; | |
| 334 * a.b.c = a.b.c || {};</pre> | |
| 335 */ | |
| 336 private List<Node> createObjectsForQualifiedName(String namespace) { | |
| 337 List<Node> objects = new ArrayList<>(); | |
| 338 String[] parts = namespace.split("\\."); | |
| 339 | |
| 340 createObjectIfNew(objects, parts[0], true); | |
| 341 | |
| 342 if (parts.length >= 2) { | |
| 343 StringBuilder currPrefix = new StringBuilder().append(parts[0]); | |
| 344 for (int i = 1; i < parts.length; ++i) { | |
| 345 currPrefix.append(".").append(parts[i]); | |
| 346 createObjectIfNew(objects, currPrefix.toString(), false); | |
| 347 } | |
| 348 } | |
| 349 | |
| 350 return objects; | |
| 351 } | |
| 352 | |
| 353 private void createObjectIfNew(List<Node> objects, String name, boolean need
Var) { | |
| 354 if (!createdObjects.contains(name)) { | |
| 355 objects.add(createJsNode((needVar ? "var " : "") + name + " = " + na
me + " || {};")); | |
| 356 createdObjects.add(name); | |
| 357 } | |
| 358 } | |
| 359 | |
| 360 private Node createJsNode(String code) { | |
| 361 // The parent node after parseSyntheticCode() is SCRIPT node, we need to
get rid of it. | |
| 362 return compiler.parseSyntheticCode(code).removeFirstChild(); | |
| 363 } | |
| 364 | |
| 365 private class RenameInternalsToExternalsCallback extends AbstractPostOrderCa
llback { | |
| 366 private final String namespaceName; | |
| 367 private final Map<String, String> exports; | |
| 368 private final Node namespaceBlock; | |
| 369 | |
| 370 public RenameInternalsToExternalsCallback(String namespaceName, | |
| 371 Map<String, String> exports, Node namespaceBlock) { | |
| 372 this.namespaceName = namespaceName; | |
| 373 this.exports = exports; | |
| 374 this.namespaceBlock = namespaceBlock; | |
| 375 } | |
| 376 | |
| 377 @Override | |
| 378 public void visit(NodeTraversal t, Node n, Node parent) { | |
| 379 if (n.isFunction() && parent == this.namespaceBlock && | |
| 380 this.exports.containsKey(n.getFirstChild().getString())) { | |
| 381 // It's a top-level function/constructor definition. | |
| 382 // | |
| 383 // Change | |
| 384 // | |
| 385 // /** Some doc */ | |
| 386 // function internalName() {} | |
| 387 // | |
| 388 // to | |
| 389 // | |
| 390 // /** Some doc */ | |
| 391 // my.namespace.name.externalName = function internalName() {}
; | |
| 392 // | |
| 393 // by looking up in this.exports for internalName to find the co
rrespondent | |
| 394 // externalName. | |
| 395 Node functionTree = n.cloneTree(); | |
| 396 Node exprResult = IR.exprResult( | |
| 397 IR.assign(buildQualifiedName(n.getFirstChild()), fun
ctionTree).srcref(n) | |
| 398 ).srcref(n); | |
| 399 | |
| 400 if (n.getJSDocInfo() != null) { | |
| 401 exprResult.getFirstChild().setJSDocInfo(n.getJSDocInfo()); | |
| 402 functionTree.removeProp(Node.JSDOC_INFO_PROP); | |
| 403 } | |
| 404 this.namespaceBlock.replaceChild(n, exprResult); | |
| 405 } else if (n.isName() && this.exports.containsKey(n.getString()) && | |
| 406 !parent.isFunction()) { | |
| 407 if (parent.isVar()) { | |
| 408 if (parent.getParent() == this.namespaceBlock) { | |
| 409 // It's a top-level exported variable definition (maybe
without an | |
| 410 // assignment). | |
| 411 // Change | |
| 412 // | |
| 413 // var enum = { 'one': 1, 'two': 2 }; | |
| 414 // | |
| 415 // to | |
| 416 // | |
| 417 // my.namespace.name.enum = { 'one': 1, 'two': 2 }; | |
| 418 Node varContent = n.removeFirstChild(); | |
| 419 Node exprResult; | |
| 420 if (varContent == null) { | |
| 421 exprResult = IR.exprResult(buildQualifiedName(n)).sr
cref(parent); | |
| 422 } else { | |
| 423 exprResult = IR.exprResult( | |
| 424 IR.assign(buildQualifiedName(n), varCont
ent).srcref(parent) | |
| 425 ).srcref(parent); | |
| 426 } | |
| 427 if (parent.getJSDocInfo() != null) { | |
| 428 exprResult.getFirstChild().setJSDocInfo(parent.getJS
DocInfo().clone()); | |
| 429 } | |
| 430 this.namespaceBlock.replaceChild(parent, exprResult); | |
| 431 } | |
| 432 } else { | |
| 433 // It's a local name referencing exported entity. Change to
its global name. | |
| 434 Node newNode = buildQualifiedName(n); | |
| 435 if (n.getJSDocInfo() != null) { | |
| 436 newNode.setJSDocInfo(n.getJSDocInfo().clone()); | |
| 437 } | |
| 438 | |
| 439 // If we alter the name of a called function, then it gets a
n explicit "this" | |
| 440 // value. | |
| 441 if (parent.isCall()) { | |
| 442 parent.putBooleanProp(Node.FREE_CALL, false); | |
| 443 } | |
| 444 | |
| 445 parent.replaceChild(n, newNode); | |
| 446 } | |
| 447 } | |
| 448 } | |
| 449 | |
| 450 private Node buildQualifiedName(Node internalName) { | |
| 451 String externalName = this.exports.get(internalName.getString()); | |
| 452 return NodeUtil.newQName(compiler, this.namespaceName + "." + extern
alName).srcrefTree( | |
| 453 internalName); | |
| 454 } | |
| 455 } | |
| 456 } | |
| OLD | NEW |