Files
cappuccino/Tools/press/objj-analysis-tools.j
T

415 lines
14 KiB
Plaintext

var FILE = require("file");
var OBJJ = require("objective-j");
var Context = require("interpreter").Context;
function ObjectiveJRuntimeAnalyzer(rootPath)
{
this.rootPath = rootPath;
this.context = new Context();
this.scope = setupObjectiveJ(this.context);
this.require = this.context.global.require;
}
ObjectiveJRuntimeAnalyzer.prototype.initializeGlobalRecorder = function()
{
this.initializeGlobalRecorder = function(){}; // run once
this.ignore = cloneProperties(this.scope, true);
this.files = {};
var evaluatingPaths = [];
var before = null;
var currentFile = null;
var self = this;
// a function to record changes to the global, then reset the scope
function recordAndReset() {
// system.stderr.write(".").flush();
var after = cloneProperties(self.scope);
if (before) {
self.files[currentFile] = self.files[currentFile] || {};
self.files[currentFile].globals = self.files[currentFile].global || {};
diff({
before : before,
after : after,
ignore : self.ignore,
added : self.files[currentFile].globals,
changed : self.files[currentFile].globals
});
}
before = after;
}
var _OBJJ = this.require("objective-j");
var _fileExecuterForPath = _OBJJ.fileExecuterForPath;
_OBJJ.fileExecuterForPath = function(/*String*/ referencePath) {
var fileExecutor = _fileExecuterForPath.apply(this, arguments);
return function(/*String*/ aPath, /*BOOL*/ isLocal, /*BOOL*/ shouldForce) {
recordAndReset();
evaluatingPaths.push(currentFile);
// NOTE: we distinguish local and library imports using absolute and relative paths.
// we resolve the library paths later (in "mergeLibraryImports()") since doing it here seems
// to change the resulting recorded globals.
if (isLocal)
currentFile = FILE.normal(FILE.join(referencePath, aPath));
else
currentFile = aPath;
system.stderr.write(">").flush();
fileExecutor.apply(this, arguments);
system.stderr.write("<").flush();
recordAndReset();
currentFile = evaluatingPaths.pop();
};
}
}
ObjectiveJRuntimeAnalyzer.prototype.load = function(path)
{
this.require("objective-j").objj_eval(
"("+(function(path) {
fileImporterForPath("/")(path, true, function() {
print("Done importing and evaluating: " + path);
});
})+")"
)(path);
}
ObjectiveJRuntimeAnalyzer.prototype.finishLoading = function(path)
{
// run the "event loop"
this.require('browser/timeout').serviceTimeouts();
}
ObjectiveJRuntimeAnalyzer.prototype.mapGlobalsToFiles = function()
{
this.mergeLibraryImports();
// takes a hash mapping from file names to hashes of global names defined in each file
// globals = { fileName { globalName : true }}
// returns a hash mapping from global names to arrays of file names in which those globals are defined
// dependencies = { globalName : [fileName] }
var globals = {};
for (var fileName in this.files) {
for (var globalName in this.files[fileName].globals)
(globals[globalName] = globals[globalName] || []).push(fileName);
}
return globals;
}
ObjectiveJRuntimeAnalyzer.prototype.mapFilesToGlobals = function()
{
this.mergeLibraryImports();
var files = {};
for (var fileName in this.files) {
files[fileName] = {};
for (var globalName in this.files[fileName].globals)
files[fileName][globalName] = true;
}
return files;
}
// this method resolves library imports and merges their recorded globals with the canonical
// file object for each file
ObjectiveJRuntimeAnalyzer.prototype.mergeLibraryImports = function()
{
for (var relativePath in this.files) {
if (FILE.isRelative(relativePath)) {
var absolutePath = this.executableForImport(relativePath, false).path();
// CPLog.debug("Merging " + relativePath + " => " + absolutePath);
this.files[absolutePath] = this.files[absolutePath] || {};
this.files[absolutePath].globals = this.files[absolutePath].globals || {};
for (var global in this.files[relativePath].globals) {
this.files[absolutePath].globals[global] = true;
}
delete this.files[relativePath];
}
}
}
// returns an executable for the import path, or null if none exists
ObjectiveJRuntimeAnalyzer.prototype.executableForImport = function(path, isLocal)
{
if (isLocal === undefined) isLocal = true;
var _OBJJ = this.require("objective-j");
return new _OBJJ.FileExecutableSearch(path, isLocal).result();
}
/*
param context includes
scope: the objective-j scope
dependencies: hash mapping from paths to an array of global variables defined by that file
[importCallback]: callback function that is called for each imported file (takes importing file path, and imported file path parameters)
[referencedCallback]: callback function that is called for each referenced file (takes referencing file path, referenced file path parameters, and list of tokens)
[importedFiles]: hash that will contain a mapping of file names to a hash of imported files
[referencedFiles]: hash that will contain a mapping of file names to a hash of referenced files (which contains a hash of tokens referenced)
[processedFiles]: hash containing file paths which have already been analyzed
param file is an objj_file object containing path, fragments, content, bundle, etc
*/
ObjectiveJRuntimeAnalyzer.prototype.traverseDependencies = function(executable, context)
{
context = context || {};
context.processedFiles = context.processedFiles || {};
context.importedFiles = context.importedFiles || {};
context.referencedFiles = context.referencedFiles || {};
context.ignoredImports = context.ignoredImports || {};
var path = executable.path();
if (context.processedFiles[path])
return;
context.processedFiles[path] = true;
var ignoreImports = false;
if (context.ignoreAllImports)
{
// CPLog.warn("Ignoring all import fragments. ("+this.rootPath.relative(path)+")");
ignoreImports = true;
}
else if (context.ignoreFrameworkImports)
{
var matches = path.match(new RegExp("([^\\/]+)\\/([^\\/]+)\\.j$")); // Matches "ZZZ/ZZZ.j" (e.x. AppKit/AppKit.j and Foundation/Foundation.j)
if (matches && matches[1] === matches[2])
{
// CPLog.warn("Framework import file! Ignoring all import fragments. ("+this.rootPath.relative(path)+")");
ignoreImports = true;
}
}
var referencedFiles = {},
importedFiles = {};
if (context.progressCallback)
context.progressCallback(this.rootPath.relative(path), path);
// code
var code = executable.code();
var referencedTokens = uniqueTokens(code);
markFilesReferencedByTokens(referencedTokens, this.mapGlobalsToFiles(), referencedFiles);
delete referencedFiles[path];
// imports
if (ignoreImports)
{
if (context.ignoreImportsCallback)
context.ignoreImportsCallback(this.rootPath.relative(path), path);
context.ignoredImports[path] = true;
}
else
{
executable.fileDependencies().forEach(function(fileDependency) {
var dependencyExecutable = null;
if (fileDependency.isLocal())
dependencyExecutable = this.executableForImport(FILE.normal(FILE.join(FILE.dirname(path), fileDependency.path())), true);
else
dependencyExecutable = this.executableForImport(fileDependency.path(), false);
if (dependencyExecutable)
{
var importedFile = dependencyExecutable.path();
// should never import self, but just in case?
if (importedFile !== path)
importedFiles[importedFile] = true;
else
CPLog.error("Ignoring self import (why are you importing yourself?!): " + this.rootPath.relative(importedFile));
}
else
CPLog.error("Couldn't find file for import " + fileDependency.path() + " ("+fileDependency.isLocal()+")");
}, this);
}
// check each imported file
this.checkImported(context, path, importedFiles);
context.importedFiles[path] = importedFiles;
// check each referenced file
this.checkReferenced(context, path, referencedFiles);
context.referencedFiles[path] = referencedFiles;
return context;
}
ObjectiveJRuntimeAnalyzer.prototype.checkImported = function(context, path, importedFiles) {
for (var importedFile in importedFiles)
{
if (importedFile !== path)
{
if (context.importCallback)
context.importCallback(path, importedFile);
var executable = this.executableForImport(importedFile, true);
if (executable)
this.traverseDependencies(executable, context);
else
CPLog.error("Missing imported file: " + importedFile);
}
}
}
ObjectiveJRuntimeAnalyzer.prototype.checkReferenced = function(context, path, referencedFiles) {
for (var referencedFile in referencedFiles)
{
if (referencedFile !== path)
{
if (context.referenceCallback)
context.referenceCallback(path, referencedFile, referencedFiles[referencedFile]);
var executable = this.executableForImport(referencedFile, true);
if (executable)
this.traverseDependencies(executable, context);
else
CPLog.error("Missing referenced file: " + referencedFile);
}
}
}
ObjectiveJRuntimeAnalyzer.prototype.fileExecutables = function() {
var _OBJJ = this.require("objective-j");
return _OBJJ.FileExecutablesForPaths;
}
// returns a unique list of tokens for a piece of code.
// ideally this should return identifiers only
function uniqueTokens(code) {
// FIXME: this breaks for indentifiers containing "$" since it's considered a distinct token by the lexer
var lexer = new OBJJ.Lexer(code, null);
var token, tokens = {};
while (token = lexer.skip_whitespace()) {
tokens[token] = true;
}
return Object.keys(tokens);
}
/*
params:
tokens (in): list of tokens to mark as required
globalsToFiles (in): map from tokens to files which define those tokens
referencedFiles (out): map of required files (to map of tokens defined in that file)
*/
function markFilesReferencedByTokens(tokens, globalsToFiles, referencedFiles) {
tokens.forEach(function(token) {
if (globalsToFiles.hasOwnProperty(token))
{
var files = globalsToFiles[token];
for (var i = 0; i < files.length; i++)
{
referencedFiles[files[i]] = referencedFiles[files[i]] || {};
referencedFiles[files[i]][token] = true;
}
}
});
}
@implementation PressBundleDelgate : CPObject
{
Function didFinishLoadingCallback;
}
- (id)initWithCallback:(Function)aCallback
{
if (self = [super init]) {
didFinishLoadingCallback = aCallback;
}
return self;
}
- (void)bundleDidFinishLoading:(CPBundle)aBundle
{
print("didFinishLoading: "+aBundle);
if (didFinishLoadingCallback)
didFinishLoadingCallback(aBundle);
}
@end
// create a new scope loaded with Narwhal and Objective-J
function setupObjectiveJ(context)
{
// set these properties required for Narwhal bootstrapping
context.global.NARWHAL_HOME = system.prefix;
context.global.NARWHAL_ENGINE_HOME = FILE.join(system.prefix, "engines", "rhino");
// load the bootstrap.js for narwhal-rhino
var bootstrapPath = FILE.join(context.global.NARWHAL_ENGINE_HOME, "bootstrap.js");
context.evalFile(bootstrapPath);
context.global.require("browser");
// get the Objective-J module from this scope, return the window object.
var OBJJ = context.global.require("objective-j");
// TODO: move this to browserjs and/or remove browser dependency in Objective-J/AppKit
addMockBrowserEnvironment(OBJJ.window);
return OBJJ.window;
}
// add a mock browser environment to the provided scope
function addMockBrowserEnvironment(scope)
{
// TODO: complete this. or use env.js?
if (!scope.window)
scope.window = scope;
if (!scope.location)
scope.location = {};
if (!scope.location.href)
scope.location.href = "";
if (!scope.Element)
scope.Element = function() {
this.style = {}
}
if (!scope.document)
scope.document = {
createElement : function() {
return new scope.Element();
}
}
}
// does a shallow copy of an object. if onlyList is true, it sets each property to "true" instead of the actual value
function cloneProperties(object, onlyList)
{
var results = {}
for (var memeber in object)
results[memeber] = onlyList ? true : object[memeber];
return results;
}
function diff(o)
{
for (var i in o.after)
if (o.added && !o.ignore[i] && typeof o.before[i] == "undefined")
o.added[i] = true;
for (var i in o.after)
if (o.changed && !o.ignore[i] && typeof o.before[i] != "undefined" && typeof o.after[i] != "undefined" && o.before[i] !== o.after[i])
o.changed[i] = true;
for (var i in o.before)
if (o.deleted && !o.ignore[i] && typeof o.after[i] == "undefined")
o.deleted[i] = true;
}