001/*-------------------------------------------------------------------------+ 002| | 003| Copyright (c) 2009-2017 CQSE GmbH | 004| | 005+-------------------------------------------------------------------------*/ 006package eu.cqse.check.framework.util.javascript; 007 008import static eu.cqse.check.framework.scanner.ETokenType.DOCUMENTATION_COMMENT; 009import static eu.cqse.check.framework.shallowparser.SubTypeNames.GOOG_MODULE; 010import static eu.cqse.check.framework.shallowparser.SubTypeNames.GOOG_PROVIDE; 011import static eu.cqse.check.framework.shallowparser.SubTypeNames.GOOG_REQUIRE; 012import static eu.cqse.check.framework.util.javascript.ClosurePatterns.CLOSURE_CONSTRUCTOR_PATTERN; 013import static eu.cqse.check.framework.util.javascript.ClosurePatterns.DEPENDENCY_NAMESPACE_GROUP_INDEX; 014import static eu.cqse.check.framework.util.javascript.ClosurePatterns.GOOG_INHERITS_PATTERN; 015import static eu.cqse.check.framework.util.javascript.ClosurePatterns.NAMESPACE_CONSTANT_PATTERN; 016 017import java.util.ArrayList; 018import java.util.HashSet; 019import java.util.List; 020import java.util.Objects; 021import java.util.Set; 022import java.util.function.Function; 023 024import org.conqat.lib.commons.assertion.CCSMAssert; 025import org.conqat.lib.commons.collections.SetMap; 026import org.conqat.lib.commons.string.StringUtils; 027 028import eu.cqse.check.framework.scanner.IToken; 029import eu.cqse.check.framework.shallowparser.framework.ShallowEntity; 030import eu.cqse.check.framework.util.tokens.TokenPattern; 031import eu.cqse.check.framework.util.tokens.TokenPatternMatch; 032import eu.cqse.check.framework.util.tokens.TokenStream; 033 034/** 035 * Extracts namespaces (i.e., JS 'class declarations') from JavaScript that uses 036 * Google Closure (https://developers.google.com/closure/) 037 */ 038public class ClosureJavaScriptDependencyExtractor { 039 040 /** 041 * The namespace (e.g. 'x.y.MyType') whose dependencies are currently being 042 * collected. Can be null, e.g. at the top of files or between two classes in 043 * one file. 044 */ 045 private String currentNamespace; 046 047 /** Collects the dependency results. */ 048 private ClosureDependencyResult result; 049 050 private TokenStream tokenStream; 051 052 /** 053 * Collects the real dependencies of the given entities, which might not match 054 * the ones declared with 'goog.require' in the file. The tokens are needed here 055 * because we also evaluate dependencies from comments. 056 */ 057 public ClosureDependencyResult getActualDependencies(List<ShallowEntity> entities, List<IToken> tokens) { 058 result = new ClosureDependencyResult(); 059 060 Set<String> validNamespaces = new HashSet<>(getDeclaredNamespaces(entities)); 061 if (validNamespaces.isEmpty()) { 062 return result; 063 } 064 065 addDependenciesFromGoogRequires(entities, validNamespaces); 066 067 tokenStream = new TokenStream(tokens, 0); 068 069 while (!tokenStream.isExhausted()) { 070 if (foundNewNamespaceDeclaration(validNamespaces)) { 071 continue; 072 } 073 if (currentNamespace == null || !consumedCurrentTokens()) { 074 tokenStream.advancePosition(1); 075 } 076 } 077 078 return result; 079 } 080 081 /** 082 * Stores the namespaces required via 'goog.require' in the current file (for 083 * each provided namespace). 084 * 085 * @param validNamespaces 086 * all namespaces declared (via 'goog.provide') in the current file 087 * (usually just one). 088 */ 089 private void addDependenciesFromGoogRequires(List<ShallowEntity> entities, Set<String> validNamespaces) { 090 SetMap<String, IToken> requiredNamespaces = getStatedDependencies(entities); 091 092 // This loop looks expensive, but validNamespaces and 093 // getCollection(requiredNamespace) should usually be equal to 1. 094 for (String requiredNamespace : requiredNamespaces.getKeys()) { 095 for (String providedNamespace : validNamespaces) { 096 for (IToken dependencyToken : requiredNamespaces.getCollection(requiredNamespace)) { 097 result.addDependency(providedNamespace, requiredNamespace, dependencyToken); 098 } 099 } 100 } 101 } 102 103 /** 104 * Whether we found the declaration of a new namespace in the current file at 105 * the beginning of {@link #tokenStream}. If there is a match, the corresponding 106 * tokens will be removed from the stream, and true is returned. 107 */ 108 private boolean foundNewNamespaceDeclaration(Set<String> validNamespaces) { 109 IToken firstToken = tokenStream.peekCurrent(); 110 String nextNamespace = getNextNamespaceOrNull(validNamespaces); 111 if (nextNamespace == null || Objects.equals(nextNamespace, currentNamespace)) { 112 return false; 113 } 114 result.addNamespace(nextNamespace, firstToken); 115 currentNamespace = nextNamespace; 116 tokenStream.advancePosition(1); 117 return true; 118 } 119 120 /** 121 * @return {@code true} if we found a dependency-related pattern at the 122 * beginning of {@link #tokenStream} (which will remove the pattern's 123 * tokens from the stream), {@code false} otherwise 124 */ 125 private boolean consumedCurrentTokens() { 126 return processedCommentToken() || processedConstructor() || processedGoogInherits() || processedMethodcall() 127 || processedConstant(); 128 } 129 130 /** 131 * Looks for a static method call of a foreign namespace and consumes the 132 * corresponding tokens, if applicable. See {@link #processedSimplePattern} 133 */ 134 private boolean processedMethodcall() { 135 return processedSimplePattern(ClosurePatterns.STATIC_METHOD_CALL_PATTERN, 136 dependencyNamespace -> StringUtils.removeLastPart(dependencyNamespace, '.')); 137 } 138 139 /** 140 * Looks for an constructor of a foreign namespace and consumes the 141 * corresponding tokens, if applicable. See {@link #processedSimplePattern} 142 */ 143 private boolean processedConstructor() { 144 return processedSimplePattern(ClosurePatterns.CONSTRUCTOR_PATTERN, null); 145 } 146 147 /** 148 * Looks for the given pattern and applies the given function to potential 149 * matches. If a match is found, stores the dependencies in {@link #result}, 150 * removes the match's tokens from {@link #tokenStream}, and returns 151 * {@code true}. Otherwise, {@code false} is returned. 152 */ 153 private boolean processedSimplePattern(TokenPattern dependencyPattern, 154 Function<String, String> dependencyTextModifier) { 155 TokenPatternMatch patternMatch = dependencyPattern.matchAtCurrentPosition(tokenStream); 156 if (patternMatch == null) { 157 return false; 158 } 159 String dependencyNamespace = patternMatch.groupString(DEPENDENCY_NAMESPACE_GROUP_INDEX); 160 if (dependencyTextModifier != null) { 161 dependencyNamespace = dependencyTextModifier.apply(dependencyNamespace); 162 } 163 handlePatternMatch(dependencyNamespace, patternMatch); 164 return true; 165 } 166 167 /** 168 * Tries to find dependencies from JS-doc comments, 169 * e.g. @type{x.y.OtherNamespace}. Also see 170 * {@link #processedSimplePattern(TokenPattern, Function)}. 171 */ 172 private boolean processedCommentToken() { 173 174 IToken currentToken = tokenStream.peekCurrent(); 175 if (currentToken.getType() != DOCUMENTATION_COMMENT) { 176 return false; 177 } 178 179 // These two JS doc parameters indicate that we right before the declaration of 180 // a constructor in the current file, which means that any dependencies found in 181 // this comment are dependencies of the *next* namespace. 182 boolean belongsToNextNamespace = currentToken.getText().contains("@extends") 183 || currentToken.getText().contains("@constructor"); 184 185 for (String commentDependency : ClosurePatterns.getDependenciesFromComment(currentToken)) { 186 if (belongsToNextNamespace) { 187 addDependencyForNextNamespace(commentDependency, currentToken); 188 } else { 189 addDependency(commentDependency, currentToken); 190 } 191 } 192 tokenStream.advancePosition(1); 193 return true; 194 } 195 196 /** 197 * Tries to find the use of a constant at the beginning of {@link #tokenStream}. 198 * Returns {@code true} if such an expression was found and processed. 199 */ 200 private boolean processedConstant() { 201 TokenPatternMatch namespaceConstantMatch = NAMESPACE_CONSTANT_PATTERN.matchAtCurrentPosition(tokenStream); 202 if (namespaceConstantMatch == null) { 203 return false; 204 } 205 String namespace = namespaceConstantMatch.groupString(DEPENDENCY_NAMESPACE_GROUP_INDEX); 206 String lastPart = StringUtils.getLastPart(namespace, '.'); 207 if (lastPart == null || !lastPart.matches("[A-Z0-9_]+")) { 208 return false; 209 } 210 String calledNamespace = StringUtils.removeLastPart(namespace, '.'); 211 handlePatternMatch(calledNamespace, namespaceConstantMatch); 212 return true; 213 } 214 215 /** 216 * Handles an actual dependency match by storing the dependency and removing the 217 * matches tokens from {@link #tokenStream}. 218 */ 219 private void handlePatternMatch(String dependencyNamespace, TokenPatternMatch patternMatch) { 220 List<IToken> groupTokens = patternMatch.groupTokens(DEPENDENCY_NAMESPACE_GROUP_INDEX); 221 addDependency(dependencyNamespace, groupTokens.get(0)); 222 removeTokensFromListStart(groupTokens); 223 } 224 225 /** 226 * Tries to find a goog.inherits() call at the beginning of 227 * {@link #tokenStream}. Returns {@code true} if such an expression was found 228 * and processed. 229 */ 230 private boolean processedGoogInherits() { 231 TokenPatternMatch googInheritsMatch = GOOG_INHERITS_PATTERN.matchAtCurrentPosition(tokenStream); 232 if (googInheritsMatch == null || !("goog.inherits".equals(googInheritsMatch.groupString(1)))) { 233 return false; 234 } 235 String inheritsFromNamespace = googInheritsMatch.groupString(DEPENDENCY_NAMESPACE_GROUP_INDEX); 236 handlePatternMatch(inheritsFromNamespace, googInheritsMatch); 237 return true; 238 } 239 240 /** 241 * Adds a dependency for the next namespace that will be declared in the current 242 * file. See 243 * {@link ClosureDependencyResult#addDependencyForNextNamespace(String, IToken)} 244 */ 245 private void addDependencyForNextNamespace(String dependencyNamespace, IToken token) { 246 if (dependencyNamespace.contains(".")) { 247 result.addDependencyForNextNamespace(dependencyNamespace, token); 248 } 249 } 250 251 /** 252 * Tries to identify the next namespace declared of the current file by looking 253 * for a Closure constructor. Can return null (e.g. between two classes) 254 */ 255 private String getNextNamespaceOrNull(Set<String> validNamespaces) { 256 TokenPatternMatch constructorMatch = CLOSURE_CONSTRUCTOR_PATTERN.matchAtCurrentPosition(tokenStream); 257 if (constructorMatch == null) { 258 return currentNamespace; 259 } 260 String newNamespace = constructorMatch.groupString(DEPENDENCY_NAMESPACE_GROUP_INDEX); 261 if (!validNamespaces.contains(newNamespace) || newNamespace.contains("prototype.") || (currentNamespace != null 262 && (newNamespace.contains(currentNamespace) || currentNamespace.contains(newNamespace)))) { 263 return null; 264 } 265 return newNamespace; 266 } 267 268 /** 269 * Removes the first n tokens from {@link #tokenStream}, where n is the size of 270 * tokensToRemove. 271 */ 272 private void removeTokensFromListStart(List<IToken> tokensToRemove) { 273 tokenStream.advancePosition(tokensToRemove.size()); 274 } 275 276 /** 277 * Adds the given dependecy namespace if it matches the general namespace format 278 * and is no sub-namespace of {@link #currentNamespace}. 279 */ 280 private void addDependency(String dependencyNamespace, IToken token) { 281 if (currentNamespace != null && !dependencyNamespace.contains(currentNamespace) 282 && dependencyNamespace.contains(".")) { 283 result.addDependency(currentNamespace, dependencyNamespace, token); 284 } 285 } 286 287 /** 288 * Returns the dependencies of the given entity that are delclared via 289 * 'goog.require'). Note that these might not be complete (e.g. do not include 290 * 'comment-only' dependencies), or include unused imports. 291 */ 292 public static SetMap<String, IToken> getStatedDependencies(List<ShallowEntity> entities) { 293 return collectGoogNamespaceStatements(entities, GOOG_REQUIRE); 294 } 295 296 /** 297 * Returns the declared namespaces (i.e., JS 'class declarations') in the given 298 * entities. 299 */ 300 public static List<String> getDeclaredNamespaces(List<ShallowEntity> entities) { 301 ArrayList<String> result = new ArrayList<>(); 302 result.addAll(collectGoogNamespaceStatements(entities, GOOG_PROVIDE).getKeys()); 303 result.addAll(collectGoogNamespaceStatements(entities, GOOG_MODULE).getKeys()); 304 return result; 305 } 306 307 /** @see #getDeclaredNamespaces(List) for the child entities. */ 308 public static List<String> getOwnNamespaces(ShallowEntity entity) { 309 return getDeclaredNamespaces(entity.getChildren()); 310 } 311 312 /** 313 * Collects the namespaces references (i.e. goog.requires, goog.provides, 314 * goog.module) from the given entities. The result contains the namespaces and 315 * the respective token of the reference. 316 */ 317 private static SetMap<String, IToken> collectGoogNamespaceStatements(List<ShallowEntity> entities, 318 String googSubType) { 319 320 CCSMAssert.isTrue( 321 GOOG_REQUIRE.equals(googSubType) || GOOG_PROVIDE.equals(googSubType) || GOOG_MODULE.equals(googSubType), 322 "Expected one of goog.require or goog.provide or goog.module but encountered " + googSubType); 323 324 SetMap<String, IToken> importAndTokens = new SetMap<>(); 325 for (ShallowEntity entity : entities) { 326 if (!entity.getSubtype().equals(googSubType)) { 327 continue; 328 } 329 String quotedImport = entity.getName(); 330 importAndTokens.add(quotedImport.substring(1, quotedImport.length() - 1), entity.ownStartTokens().get(0)); 331 } 332 return importAndTokens; 333 } 334}