@@ -232,46 +232,130 @@ 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+ /**
273+ * Provides a mechanism for resolving file paths relative to a given directory.
274+ */
275+ module Resolve< ResolveSig Config> {
276+ private import Config
277+
235278 pragma [ nomagic]
236- private string getComponent ( string relativePath , int i ) {
237- shouldAppend ( _, relativePath ) and
238- result = relativePath .replaceAll ( "\\" , "/" ) .regexpFind ( "[^/]+ ", i , _ )
279+ private string getPathSegment ( string path , int n ) {
280+ shouldResolve ( _, path ) and
281+ result = path .replaceAll ( "\\" , "/" ) .splitAt ( "/ ", n )
239282 }
240283
241- private int getNumberOfComponents ( string relativePath ) {
242- result = strictcount ( int i | exists ( getComponent ( relativePath , i ) ) | i )
284+ pragma [ nomagic]
285+ private string getPathSegmentAsGlobRegexp ( string segment ) {
286+ allowGlobs ( ) and
287+ segment = getPathSegment ( _, _) and
288+ segment .matches ( "%*%" ) and
289+ not segment = [ "*" , "**" ] and // these are special-cased
290+ result = segment .regexpReplaceAll ( "[^a-zA-Z0-9*]" , "\\\\$0" ) .replaceAll ( "*" , ".*" )
291+ }
292+
293+ pragma [ nomagic]
294+ private int getNumPathSegment ( string path ) {
295+ result = strictcount ( int n | exists ( getPathSegment ( path , n ) ) )
296+ }
297+
298+ private Container getChild ( Container base , string name ) {
299+ result = getAChildContainer ( base , name )
243300 or
244- relativePath = "" and
245- result = 0
301+ result = getAnAdditionalChild ( base , name )
246302 }
247303
248304 pragma [ nomagic]
249- private Container appendStep ( Folder f , string relativePath , int i ) {
250- i = - 1 and
251- shouldAppend ( f , relativePath ) and
252- result = f
305+ private Container resolve ( Container base , string path , int n ) {
306+ shouldResolve ( base , path ) and n = 0 and result = base
253307 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 )
308+ exists ( Container current , string segment |
309+ current = resolve ( base , path , n - 1 ) and
310+ segment = getPathSegment ( path , n - 1 )
311+ |
312+ result = getChild ( current , segment )
313+ or
314+ segment = [ "." , "" ] and
315+ result = current
316+ or
317+ segment = ".." and
318+ result = current .getParentContainer ( )
319+ or
320+ isOptionalPathComponent ( segment ) and
321+ not exists ( getChild ( current , segment ) ) and
322+ result = current
323+ or
324+ allowGlobs ( ) and
325+ (
326+ segment = "*" and
327+ result = getChild ( current , _)
328+ or
329+ segment = "**" and // allow empty match
330+ result = current
331+ or
332+ exists ( string name |
333+ result = getChild ( current , name ) and
334+ name .regexpMatch ( getPathSegmentAsGlobRegexp ( segment ) )
335+ )
336+ )
337+ )
338+ or
339+ exists ( Container current , string segment |
340+ current = resolve ( base , path , n ) and
341+ segment = getPathSegment ( path , n )
342+ |
343+ // Follow child without advancing 'n'
344+ allowGlobs ( ) and
345+ segment = "**" and
346+ result = getChild ( current , _)
263347 )
264348 }
265349
266350 /**
267- * Gets the file or folder obtained by appending `relativePath` onto `f`.
351+ * Gets the file or folder that `path` resolves to when resolved from `base`.
352+ *
353+ * Only has results for the `(base, path)` pairs provided by `shouldResolve`
354+ * in the instantiation of this module.
268355 */
269356 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- )
357+ Container resolve ( Container base , string path ) {
358+ result = resolve ( base , path , getNumPathSegment ( path ) )
275359 }
276360 }
277361 }
0 commit comments