@@ -232,46 +232,143 @@ module Make<InputSig Input> {
232232
233233 /** Provides the `append` predicate for appending a relative path onto a folder. */
234234 module Append< shouldAppendSig / 2 shouldAppend> {
235+ private module Config implements ResolveSig {
236+ predicate shouldResolve ( Container c , string name ) { shouldAppend ( c , name ) }
237+ }
238+
239+ predicate append = Resolve< Config > :: resolve / 2 ;
240+ }
241+
242+ /**
243+ * Signature for modules to pass to `Resolve`.
244+ */
245+ signature module ResolveSig {
246+ /**
247+ * Holds if `path` should be resolved to a file or folder, relative to `base`.
248+ */
249+ predicate shouldResolve ( Container base , string path ) ;
250+
251+ /**
252+ * Gets an additional file or folder to consider a child of `base`.
253+ */
254+ default Container getAnAdditionalChild ( Container base , string name ) { none ( ) }
255+
256+ /**
257+ * Holds if `component` may be treated as `.` if it does not match a child.
258+ */
259+ default predicate isOptionalPathComponent ( string component ) { none ( ) }
260+
261+ /**
262+ * Holds if globs should be interpreted in the paths being resolved.
263+ *
264+ * The following types of globs are supported:
265+ * - `*` (matches any child)
266+ * - `**` (matches any child recursively)
267+ * - Complex patterns like `foo-*.txt` are also supported
268+ */
269+ default predicate allowGlobs ( ) { none ( ) }
270+
271+ /**
272+ * Gets an alternative path segment to try if `segment` did not match a child.
273+ *
274+ * The motivating use-case is to map compiler-generated file names back to their sources files,
275+ * for example, `foo.min.js` could be mapped to `foo.ts`.
276+ */
277+ bindingset [ segment]
278+ default string rewritePathSegment ( string segment ) { none ( ) }
279+ }
280+
281+ /**
282+ * Provides a mechanism for resolving file paths relative to a given directory.
283+ */
284+ module Resolve< ResolveSig Config> {
285+ private import Config
286+
235287 pragma [ nomagic]
236- private string getComponent ( string relativePath , int i ) {
237- shouldAppend ( _, relativePath ) and
238- result = relativePath .replaceAll ( "\\" , "/" ) .regexpFind ( "[^/]+ ", i , _ )
288+ private string getPathSegment ( string path , int n ) {
289+ shouldResolve ( _, path ) and
290+ result = path .replaceAll ( "\\" , "/" ) .splitAt ( "/ ", n )
239291 }
240292
241- private int getNumberOfComponents ( string relativePath ) {
242- result = strictcount ( int i | exists ( getComponent ( relativePath , i ) ) | i )
293+ pragma [ nomagic]
294+ private string getPathSegmentAsGlobRegexp ( string segment ) {
295+ allowGlobs ( ) and
296+ segment = getPathSegment ( _, _) and
297+ segment .matches ( "%*%" ) and
298+ not segment = [ "*" , "**" ] and // these are special-cased
299+ result = segment .regexpReplaceAll ( "[^a-zA-Z0-9*]" , "\\\\$0" ) .replaceAll ( "*" , ".*" )
300+ }
301+
302+ pragma [ nomagic]
303+ private int getNumPathSegment ( string path ) {
304+ result = strictcount ( int n | exists ( getPathSegment ( path , n ) ) )
305+ }
306+
307+ private Container getChild ( Container base , string name ) {
308+ result = getAChildContainer ( base , name )
243309 or
244- relativePath = "" and
245- result = 0
310+ result = getAnAdditionalChild ( base , name )
246311 }
247312
248313 pragma [ nomagic]
249- private Container appendStep ( Folder f , string relativePath , int i ) {
250- i = - 1 and
251- shouldAppend ( f , relativePath ) and
252- result = f
314+ private Container resolve ( Container base , string path , int n ) {
315+ shouldResolve ( base , path ) and n = 0 and result = base
316+ or
317+ exists ( Container current , string segment |
318+ current = resolve ( base , path , n - 1 ) and
319+ segment = getPathSegment ( path , n - 1 )
320+ |
321+ result = getChild ( current , segment )
322+ or
323+ segment = [ "." , "" ] and
324+ result = current
325+ or
326+ segment = ".." and
327+ result = current .getParentContainer ( )
328+ or
329+ not exists ( getChild ( current , segment ) ) and
330+ (
331+ isOptionalPathComponent ( segment ) and
332+ result = current
333+ or
334+ result = getChild ( current , rewritePathSegment ( segment ) )
335+ )
336+ or
337+ allowGlobs ( ) and
338+ (
339+ segment = "*" and
340+ result = getChild ( current , _)
341+ or
342+ segment = "**" and // allow empty match
343+ result = current
344+ or
345+ exists ( string name |
346+ result = getChild ( current , name ) and
347+ name .regexpMatch ( getPathSegmentAsGlobRegexp ( segment ) )
348+ )
349+ )
350+ )
253351 or
254- exists ( Container mid , string comp |
255- mid = appendStep ( f , relativePath , i - 1 ) and
256- comp = getComponent ( relativePath , i ) and
257- if comp = ".."
258- then result = mid .getParentContainer ( )
259- else
260- if comp = "."
261- then result = mid
262- else result = getAChildContainer ( mid , comp )
352+ exists ( Container current , string segment |
353+ current = resolve ( base , path , n ) and
354+ segment = getPathSegment ( path , n )
355+ |
356+ // Follow child without advancing 'n'
357+ allowGlobs ( ) and
358+ segment = "**" and
359+ result = getChild ( current , _)
263360 )
264361 }
265362
266363 /**
267- * Gets the file or folder obtained by appending `relativePath` onto `f`.
364+ * Gets the file or folder that `path` resolves to when resolved from `base`.
365+ *
366+ * Only has results for the `(base, path)` pairs provided by `shouldResolve`
367+ * in the instantiation of this module.
268368 */
269369 pragma [ nomagic]
270- Container append ( Folder f , string relativePath ) {
271- exists ( int last |
272- last = getNumberOfComponents ( relativePath ) - 1 and
273- result = appendStep ( f , relativePath , last )
274- )
370+ Container resolve ( Container base , string path ) {
371+ result = resolve ( base , path , getNumPathSegment ( path ) )
275372 }
276373 }
277374 }
0 commit comments