diff --git a/Jakefile b/Jakefile index 83ca1c030..47a96d224 100644 --- a/Jakefile +++ b/Jakefile @@ -263,6 +263,8 @@ task("test-only", function() var code = OS.system(serializedENV() + " " + cmd.map(OS.enquote).join(" ")); if (code !== 0) OS.exit(code); + + OS.system(serializedENV() + " " + ["js", "Tests/DetectMissingImports.js"].map(OS.enquote).join(" ")); }); task("push-packages", ["push-cappuccino", "push-objective-j"]); diff --git a/Tests/DetectMissingImports.js b/Tests/DetectMissingImports.js new file mode 100644 index 000000000..a5604b52b --- /dev/null +++ b/Tests/DetectMissingImports.js @@ -0,0 +1,220 @@ +var FILE = require("file"), + FileList = require("jake").FileList, + OS = require("os"), + stream = require("narwhal/term").stream; + +var cPreprocessedFileContents = function(aFilePath) +{ + var gcc = OS.popen("gcc -E -x c -P -DPLATFORM_COMMONJS " + OS.enquote(aFilePath), { charset:"UTF-8" }), + chunk, + fileContents = ""; + + while (chunk = gcc.stdout.read()) + fileContents += chunk; + + return fileContents; +}; + +var checkImportsForFile = function (fileName) +{ + var classNames = [], + errorMessageHashSet = {}, + matches = fileName.match(new RegExp("([^\\/]+)\\/([^\\/]+)\\.j$")); + + if (!FILE.exists(fileName) || + FILE.isDirectory(fileName) || + fileName.indexOf("+") !== -1 || + (matches && matches[1] === matches[2])) + return; + + // this is a hackish way to clear the previous file's @imports + for (var g in global) + if (g !== "SyntaxError") + delete global[g]; + + // reload Objective-J and get a reference to its StaticResource export at + // the same time + global.StaticResource = require.force("objective-j").StaticResource; + + ObjectiveJ.OBJJ_INCLUDE_PATHS = [FILE.canonical(".")]; + + var og_resolveResourceAtURL = StaticResource.resolveResourceAtURL; + + // this function gets called for all "File.j" imports + // in case the import can't be resolved, use JAKE.FileList to try and find + // it. + StaticResource.resolveResourceAtURL = function(/*CFURL|String*/ aURL, /*BOOL*/ isDirectory, /*Function*/ aCallback) + { + var og_callback = arguments[2]; + arguments[2] = function (resource) + { + if (!resource) + { + var basename = FILE.basename(aURL.toString()), + newURL = (new FileList("Foundation/**/" + basename)).include("AppKit/**/" + basename).items()[0]; + + + if (newURL) + return og_resolveResourceAtURL(FILE.canonical(newURL), isDirectory, og_callback) + else + og_callback(resource); + } + else + og_callback(resource); + + }; + + og_resolveResourceAtURL.apply(this, arguments); + }; + + // this function gets called for imports + // since this script looks at the un-compiled frameworks, + // some trickery for finding the correct file is sometimes necessary + var resolveResourceAtURLSearchingIncludeURLs = function (/*CFURL*/ aURL, /*Number*/ anIndex, /*Function*/ aCallback) + { + var includeURLs = StaticResource.includeURLs(), + searchURL = new CFURL(aURL, includeURLs[anIndex]).absoluteURL(); + + og_resolveResourceAtURL(searchURL, NO, function(/*StaticResource*/ aStaticResource) + { + if (!aStaticResource) + { + if (anIndex + 1 < includeURLs.length) + resolveResourceAtURLSearchingIncludeURLs(aURL, anIndex + 1, aCallback); + else + { + var basename = FILE.basename(aURL), + newURL = (new FileList("Foundation/**/" + basename)).include("AppKit/**/" + basename).items()[0]; + + if (newURL) + resolveResourceAtURLSearchingIncludeURLS(FILE.canonical(newURL), anIndex, aCallback); + else + aCallback(NULL); + } + + return; + } + + aCallback(aStaticResource); + }); + } + StaticResource.resolveResourceAtURLSearchingIncludeURLs = function(/*CFURL*/ aURL, /*Function*/ aCallback) + { + resolveResourceAtURLSearchingIncludeURLs(aURL, 0, aCallback); + }; + + // find all the @implementations by running through the CPP and then + // calling ObjectiveJ.preprocess with a decorated @implementation handler + // on the Preprocessor + var fileNameContents = cPreprocessedFileContents(fileName); + var og_objj_implementation = ObjectiveJ.Preprocessor.prototype.implementation; + ObjectiveJ.Preprocessor.prototype.implementation = function (tokens) + { + var className = tokens.skip_whitespace(); + if (classNames.indexOf(className) === -1) + classNames.push(className); + + tokens.skip_whitespace(YES); + + og_objj_implementation.apply(this, arguments); + }; + + ObjectiveJ.preprocess(fileNameContents); + + // reset the preprocessor's @implementation handler + ObjectiveJ.Preprocessor.prototype.implementation = og_objj_implementation; + + // do automatic CPP runs on @imported files + // to resolve #includes and conditional compilation + CFHTTPRequest.prototype.responseText = function () + { + if (this._URL.slice(-2) === ".j") + { + var tmpFile = FILE.canonical(FILE.join(FILE.dirname(this._URL.split(":")[1]), ".detect_missing_imports")); + + // hackishly give the file a window object + FILE.write(tmpFile, "window = {};\n" + this._nativeRequest.responseText); + var result = cPreprocessedFileContents(tmpFile); + FILE.remove(tmpFile); + } + else + result = this._nativeRequest.responseText; + return result; + }; + + try + { + objj_importFile(FILE.canonical(fileName), YES); + } + catch (e) + { + if (e.name === "ReferenceError") + stream.print("\0yellow(Detected missing @import in \"" + fileName + "\": " + e.message + "\0)"); + else + throw e; + } + + for(var j = 0, jj = classNames.length; j < jj; ++j) + { + var className = classNames[j], + cls = objj_getClass(className); + if (cls && cls.super_class) + { + // instance methods + var method_dtable = cls.method_dtable; + for (var methodName in method_dtable) + { + if (method_dtable.hasOwnProperty(methodName)) + { + try + { + var instance = objj_msgSend(cls, "alloc"); + objj_msgSend(instance, methodName, method_dtable[methodName].types.slice(1)); + } + catch (e) + { + if (e.name === "ReferenceError") + { + var hashKey = fileName + e.message; + if (!errorMessageHashSet[hashKey]) + { + stream.print("\0yellow(Detected missing @import in \"" + fileName + "\": " + e.message + " (Context: [" + className + " " + methodName + "])\0)"); + errorMessageHashSet[hashKey] = true; + } + } + } + } + } + + // class methods + method_dtable = cls.isa.method_dtable; + for (var methodName in method_dtable) + { + if (method_dtable.hasOwnProperty(methodName)) + { + try + { + objj_msgSend(cls, methodName); + } + catch (e) + { + if (e.name === "ReferenceError") + { + var hashKey = fileName + e.message; + if (!errorMessageHashSet[hashKey]) + { + stream.print("\0yellow(Detected missing @import in \"" + fileName + "\": " + e.message + " (Context: [" + className + " " + methodName + "])\0)"); + errorMessageHashSet[hashKey] = true; + } + } + } + } + } + } + } +}; + +(new FileList("Foundation/**.j").include("AppKit/**.j")).forEach(function (aFile) +{ + checkImportsForFile(aFile); +});