rule.js 7.69 KB
let path = require('path');
let fs = require('fs');
let Task = require('./task/task').Task;

// Split a task to two parts, name space and task name.
// For example, given 'foo:bin/a%.c', return an object with
// - 'ns'     : foo
// - 'name'   : bin/a%.c
function splitNs(task) {
  let parts = task.split(':');
  let name = parts.pop();
  let ns = resolveNs(parts);
  return {
    'name' : name,
    'ns'   : ns
  };
}

// Return the namespace based on an array of names.
// For example, given ['foo', 'baz' ], return the namespace
//
//   default -> foo -> baz
//
// where default is the global root namespace
// and -> means child namespace.
function resolveNs(parts) {
  let  ns = jake.defaultNamespace;
  for(let i = 0, l = parts.length; ns && i < l; i++) {
    ns = ns.childNamespaces[parts[i]];
  }
  return ns;
}

// Given a pattern p, say 'foo:bin/a%.c'
// Return an object with
// - 'ns'     : foo
// - 'dir'    : bin
// - 'prefix' : a
// - 'suffix' : .c
function resolve(p) {
  let task = splitNs(p);
  let name  = task.name;
  let ns    = task.ns;
  let split = path.basename(name).split('%');
  return {
    ns: ns,
    dir: path.dirname(name),
    prefix: split[0],
    suffix: split[1]
  };
}

// Test whether string a is a suffix of string b
function stringEndWith(a, b) {
  let l;
  return (l = b.lastIndexOf(a)) == -1 ? false : l + a.length == b.length;
}

// Replace the suffix a of the string s with b.
// Note that, it is assumed a is a suffix of s.
function stringReplaceSuffix(s, a, b) {
  return s.slice(0, s.lastIndexOf(a)) + b;
}

class Rule {
  constructor(opts) {
    this.pattern = opts.pattern;
    this.source = opts.source;
    this.prereqs = opts.prereqs;
    this.action = opts.action;
    this.opts = opts.opts;
    this.desc =  opts.desc;
    this.ns = opts.ns;
  }

  // Create a file task based on this rule for the specified
  // task-name
  // ======
  // FIXME: Right now this just throws away any passed-in args
  // for the synthsized task (taskArgs param)
  // ======
  createTask(fullName, level) {
    let self = this;
    let pattern;
    let source;
    let action;
    let opts;
    let prereqs;
    let valid;
    let src;
    let tNs;
    let createdTask;
    let name = Task.getBaseTaskName(fullName);
    let nsPath = Task.getBaseNamespacePath(fullName);
    let ns = this.ns.resolveNamespace(nsPath);

    pattern = this.pattern;
    source = this.source;

    if (typeof source == 'string') {
      src = Rule.getSource(name, pattern, source);
    }
    else {
      src = source(name);
    }

    // TODO: Write a utility function that appends a
    // taskname to a namespace path
    src = nsPath.split(':').filter(function (item) {
      return !!item;
    }).concat(src).join(':');

    // Generate the prerequisite for the matching task.
    //    It is the original prerequisites plus the prerequisite
    //    representing source file, i.e.,
    //
    //      rule( '%.o', '%.c', ['some.h'] ...
    //
    //    If the objective is main.o, then new task should be
    //
    //      file( 'main.o', ['main.c', 'some.h' ] ...
    prereqs = this.prereqs.slice(); // Get a copy to work with
    prereqs.unshift(src);

    // Prereq should be:
    // 1. an existing task
    // 2. an existing file on disk
    // 3. a valid rule (i.e., not at too deep a level)
    valid = prereqs.some(function (p) {
      let ns = self.ns;
      return ns.resolveTask(p) ||
        fs.existsSync(Task.getBaseTaskName(p)) ||
        jake.attemptRule(p, ns, level + 1);
    });

    // If any of the prereqs aren't valid, the rule isn't valid
    if (!valid) {
      return null;
    }
    // Otherwise, hunky-dory, finish creating the task for the rule
    else {
      // Create the action for the task
      action = function () {
        let task = this;
        self.action.apply(task);
      };

      opts = this.opts;

      // Insert the file task into Jake
      //
      // Since createTask function stores the task as a child task
      // of currentNamespace. Here we temporariliy switch the namespace.
      // FIXME: Should allow optional ns passed in instead of this hack
      tNs = jake.currentNamespace;
      jake.currentNamespace = ns;
      createdTask = jake.createTask('file', name, prereqs, action, opts);
      createdTask.source = src.split(':').pop();
      jake.currentNamespace = tNs;

      return createdTask;
    }
  }

  match(name) {
    return Rule.match(this.pattern, name);
  }

  // Test wether the a prerequisite matchs the pattern.
  // The arg 'pattern' does not have namespace as prefix.
  // For example, the following tests are true
  //
  //   pattern      |    name
  //   bin/%.o      |    bin/main.o
  //   bin/%.o      |    foo:bin/main.o
  //
  // The following tests are false (trivally)
  //
  //   pattern      |    name
  //   bin/%.o      |    foobin/main.o
  //   bin/%.o      |    bin/main.oo
  static match(pattern, name) {
    let p;
    let task;
    let obj;
    let filename;

    if (pattern instanceof RegExp) {
      return pattern.test(name);
    }
    else if (pattern.indexOf('%') == -1) {
      // No Pattern. No Folder. No Namespace.
      // A Simple Suffix Rule. Just test suffix
      return stringEndWith(pattern, name);
    }
    else {
      // Resolve the dir, prefix and suffix of pattern
      p = resolve(pattern);

      // Resolve the namespace and task-name
      task = splitNs(name);
      name = task.name;

      // Set the objective as the task-name
      obj = name;

      // Namespace is already matched.

      // Check dir
      if (path.dirname(obj) != p.dir) {
        return false;
      }

      filename = path.basename(obj);

      // Check file name length
      if ((p.prefix.length + p.suffix.length + 1) > filename.length) {
        // Length does not match.
        return false;
      }

      // Check prefix
      if (filename.indexOf(p.prefix) !== 0) {
        return false;
      }

      // Check suffix
      if (!stringEndWith(p.suffix, filename)) {
        return false;
      }

      // OK. Find a match.
      return true;
    }
  }

  // Generate the source based on
  //  - name    name for the synthesized task
  //  - pattern    pattern for the objective
  //  - source    pattern for the source
  //
  // Return the source with properties
  //  - dep      the prerequisite of source
  //             (with the namespace)
  //
  //  - file     the file name of source
  //             (without the namespace)
  //
  // For example, given
  //
  //  - name   foo:bin/main.o
  //  - pattern    bin/%.o
  //  - source    src/%.c
  //
  //    return 'foo:src/main.c',
  //
  static getSource(name, pattern, source) {
    let dep;
    let pat;
    let match;
    let file;
    let src;

    // Regex pattern -- use to look up the extension
    if (pattern instanceof RegExp) {
      match = pattern.exec(name);
      if (match) {
        if (typeof source == 'function') {
          src = source(name);
        }
        else {
          src = stringReplaceSuffix(name, match[0], source);
        }
      }
    }
    // Assume string
    else {
      // Simple string suffix replacement
      if (pattern.indexOf('%') == -1) {
        if (typeof source == 'function') {
          src = source(name);
        }
        else {
          src = stringReplaceSuffix(name, pattern, source);
        }
      }
      // Percent-based substitution
      else {
        pat = pattern.replace('%', '(.*?)');
        pat = new RegExp(pat);
        match = pat.exec(name);
        if (match) {
          if (typeof source == 'function') {
            src = source(name);
          }
          else {
            file = match[1];
            file = source.replace('%', file);
            dep = match[0];
            src = name.replace(dep, file);
          }
        }
      }
    }

    return src;
  }
}


exports.Rule = Rule;