@@ -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