Skip to content

Commit 5659a8a

Browse files
committed
JS: Add template resolution logic
1 parent 1474c07 commit 5659a8a

1 file changed

Lines changed: 182 additions & 0 deletions

File tree

javascript/ql/src/semmle/javascript/frameworks/Templating.qll

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ module Templating {
137137

138138
/** Gets a data flow node that refers to an object whose properties become variables in the template. */
139139
DataFlow::Node getTemplateParamsNode() { result = range.getTemplateParamsNode() }
140+
141+
/** Gets the template file instantiated here, if any. */
142+
TemplateFile getTemplateFile() {
143+
result = getTemplateFileNode().(TemplateFileReference).getTemplateFile()
144+
}
140145
}
141146

142147
/** Companion module to the `TemplateInstantiation` class. */
@@ -155,4 +160,181 @@ module Templating {
155160
abstract DataFlow::Node getTemplateParamsNode();
156161
}
157162
}
163+
164+
/** A file that can be referenced by a template instantiation. */
165+
abstract class TemplateFile extends File { }
166+
167+
/** Any HTML file, seen as a possible target for template instantiation. */
168+
private class TemplateFileByExtension extends TemplateFile {
169+
TemplateFileByExtension() { getFileType().isHtml() }
170+
}
171+
172+
/**
173+
* A data flow node whose string value refers to a template file.
174+
*
175+
* This is similar to `PathExpr` with two main differences:
176+
* - It is not used for import resolution, and so can be computed in a later stage.
177+
* - The root folder is considered unknown, and so a heuristic is used to guess the most
178+
* likely template file being referenced.
179+
*/
180+
abstract class TemplateFileReference extends DataFlow::Node {
181+
/** Gets the value that identifies the template. Defaults to `getStringValue()`. */
182+
string getValue() { result = getStringValue() }
183+
184+
/** Gets the template file referenced by this node. */
185+
final TemplateFile getTemplateFile() {
186+
result = this.getValue().(TemplateFileReferenceString).getTemplateFile()
187+
}
188+
}
189+
190+
/** Get file argument of a template instantiation, seen as a template file reference. */
191+
private class DefaultTemplateFileReference extends TemplateFileReference {
192+
DefaultTemplateFileReference() {
193+
this = any(TemplateInstantiaton inst).getTemplateFileNode()
194+
}
195+
}
196+
197+
/**
198+
* A string that refers to a template file, and should be resolved as such.
199+
*
200+
* The string must be normalized, with backslashes replaced by forward slashes.
201+
*
202+
* This is automatically populated for instances of `TemplateFileReference`, but additional subclasses
203+
* may be added if other strings need to be resolved.
204+
*
205+
* This is similar to `PathString` with two main differences:
206+
* - It is not used for import resolution, and so can be computed in a later stage.
207+
* - The root folder is considered unknown, and so a heuristic is used to guess the most
208+
* likely template file being referenced.
209+
*/
210+
abstract class TemplateFileReferenceString extends string {
211+
bindingset[this]
212+
TemplateFileReferenceString() { this = this }
213+
214+
/** Gets the folder in which this string occurs, used to determine which file it could refer to. */
215+
abstract Folder getContextFolder();
216+
217+
/**
218+
* Gets the base name of the string, similar to `Container.getBaseName`.
219+
*/
220+
string getBaseName() { result = this.regexpReplaceAll(".*/", "") }
221+
222+
/**
223+
* Gets the stem, similar to `Container.getStem`.
224+
*/
225+
string getStem() { result = getBaseName().regexpCapture("(.*?)(?:\\.([^.]*))?", 1) }
226+
227+
/** Gets the template file referenced by this string. */
228+
final TemplateFile getTemplateFile() { result = getBestMatchingTarget(this) }
229+
}
230+
231+
/** The value of a template reference node, as a template reference string. */
232+
private class DefaultTemplateReferenceString extends TemplateFileReferenceString {
233+
TemplateFileReference r;
234+
235+
DefaultTemplateReferenceString() { this = r.getValue().replaceAll("\\", "/") }
236+
237+
override Folder getContextFolder() { result = r.getFile().getParentContainer() }
238+
}
239+
240+
/**
241+
* Gets a "fingerprint" for the given template file, which is used to references
242+
* that might refer to it (for pruning purposes only).
243+
*/
244+
pragma[nomagic]
245+
private string getTemplateFileFingerprint(TemplateFile file) {
246+
result = file.getStem()
247+
or
248+
file.getStem() = "index" and
249+
result = file.getParentContainer().getBaseName()
250+
}
251+
252+
/**
253+
* Gets a "fingerprint" for the given string, which must match one of the fingerprints of
254+
* the referenced file (for pruning purposes only).
255+
*/
256+
pragma[nomagic]
257+
private string getTemplateRefFingerprint(TemplateFileReferenceString ref) {
258+
result = ref.getStem() and not result = ["index", ""]
259+
or
260+
ref.getStem() = ["index", ""] and
261+
result = ref.regexpCapture("(?:.*)?/([^/]*)/(?:index|)(?:\\.[^.]*)?", 1)
262+
or
263+
// If the reference is just 'index', it could potentially refer to any template file
264+
// called 'index' -- we can't use the parent directory to prune candidates early.
265+
ref = "index" and result = "index"
266+
}
267+
268+
/**
269+
* Gets a template file that could potentially be the target of `ref`, based on
270+
* them having the same fingerprint.
271+
*
272+
* This is only used to speed up `getAMatchingTarget` by pruning out pairs that can't match.
273+
*/
274+
pragma[nomagic]
275+
private TemplateFile getAPotentialTarget(TemplateFileReferenceString ref) {
276+
getTemplateFileFingerprint(result) = getTemplateRefFingerprint(ref)
277+
}
278+
279+
/**
280+
* Gets a template file whose path matches the given `ref`.
281+
*
282+
* A file matches if all path components of `ref` are part of the suffix of the file path,
283+
* and in case a file extension is mentioned in `ref`, this must match that of the file.
284+
*
285+
* For example, a file at `src/foo/views/bar/baz.html` would be matched by the following:
286+
* - `bar.html`
287+
* - `bar/baz.html`
288+
* - `views/bar/baz.html`
289+
* - `baz` (extension can be omitted)
290+
* - `bar/baz`
291+
*
292+
* But would not be matched by:
293+
* - `foo/baz.html` (folder does not match)
294+
* - `baz.ejs` (extension does not match)
295+
*
296+
* Additionally, a file whose stem is `index` matches if `ref` would match the parent folder by
297+
* the above rules. For example: `bar` matches `src/bar/index.html`.
298+
*/
299+
pragma[nomagic]
300+
private TemplateFile getAMatchingTarget(TemplateFileReferenceString ref) {
301+
result = getAPotentialTarget(ref) and
302+
result.getAbsolutePath() =
303+
any(string s) + ref + ["", "/index"] + ["", "." + result.getExtension()]
304+
}
305+
306+
/**
307+
* Gets the length of the longest common prefix between `file` and `ref`.
308+
*
309+
* This is used to rank all the possible files that `ref` could refer to.
310+
* Picking the one with the highest rank ensures that the file most closely related
311+
* in the file hierarchy is picked.
312+
*
313+
* For example, given the files:
314+
* ```
315+
* A/components/foo.js
316+
* A/views/list.html
317+
* B/components/foo.js
318+
* B/views/list.html
319+
* ```
320+
* The string `list` in `A/components/foo.js` will resolve to `A/views/list.html`,
321+
* and vice versa in `B/components/foo.js`.
322+
*/
323+
pragma[nomagic]
324+
private int getRankOfMatchingTarget(TemplateFile file, TemplateFileReferenceString ref) {
325+
file = getAMatchingTarget(ref) and
326+
exists(string filePath, string refPath |
327+
// Pad each file name to ensure they differ at some index, in case one was a prefix of the other
328+
filePath = file.getRelativePath() + "!" and
329+
refPath = ref.getContextFolder().getRelativePath() + "@" and
330+
result = min(int i | filePath.charAt(i) != refPath.charAt(i))
331+
)
332+
}
333+
334+
/**
335+
* Gets the template file referred to by `ref`.
336+
*/
337+
private TemplateFile getBestMatchingTarget(TemplateFileReferenceString ref) {
338+
result = max(getAMatchingTarget(ref) as f order by getRankOfMatchingTarget(f, ref))
339+
}
158340
}

0 commit comments

Comments
 (0)