mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-28 14:37:04 +00:00
415 lines
14 KiB
Plaintext
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;
|
|
}
|