diff --git a/Atomicity.xcodeproj/project.pbxproj b/Atomicity.xcodeproj/project.pbxproj index f23de5f52..a9871801b 100644 --- a/Atomicity.xcodeproj/project.pbxproj +++ b/Atomicity.xcodeproj/project.pbxproj @@ -61,6 +61,15 @@ 8359A69213FE1FE900AC37E3 /* worker-javascript.js in Resources */ = {isa = PBXBuildFile; fileRef = 8359A63413FE1FCA00AC37E3 /* worker-javascript.js */; }; 8359A6B413FE24B300AC37E3 /* atomicity.js in Sources */ = {isa = PBXBuildFile; fileRef = 8359A6B313FE24B300AC37E3 /* atomicity.js */; }; 8359A6B713FE24BC00AC37E3 /* atomicity.js in Resources */ = {isa = PBXBuildFile; fileRef = 8359A6B313FE24B300AC37E3 /* atomicity.js */; }; + 8359A6EE13FE26DA00AC37E3 /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8359A6ED13FE26DA00AC37E3 /* JavaScriptCore.framework */; }; + 8359A72213FE270F00AC37E3 /* BridgeSupportController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8359A6F913FE270F00AC37E3 /* BridgeSupportController.m */; }; + 8359A72313FE270F00AC37E3 /* class.js in Sources */ = {isa = PBXBuildFile; fileRef = 8359A6FA13FE270F00AC37E3 /* class.js */; }; + 8359A72F13FE270F00AC37E3 /* JSCocoaController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8359A71813FE270F00AC37E3 /* JSCocoaController.m */; }; + 8359A73013FE270F00AC37E3 /* JSCocoaFFIArgument.m in Sources */ = {isa = PBXBuildFile; fileRef = 8359A71A13FE270F00AC37E3 /* JSCocoaFFIArgument.m */; }; + 8359A73113FE270F00AC37E3 /* JSCocoaFFIClosure.m in Sources */ = {isa = PBXBuildFile; fileRef = 8359A71C13FE270F00AC37E3 /* JSCocoaFFIClosure.m */; }; + 8359A73213FE270F00AC37E3 /* JSCocoaLib.m in Sources */ = {isa = PBXBuildFile; fileRef = 8359A71E13FE270F00AC37E3 /* JSCocoaLib.m */; }; + 8359A73313FE270F00AC37E3 /* JSCocoaPrivateObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 8359A72013FE270F00AC37E3 /* JSCocoaPrivateObject.m */; }; + 8359A73413FE270F00AC37E3 /* jslint-jscocoa.js in Sources */ = {isa = PBXBuildFile; fileRef = 8359A72113FE270F00AC37E3 /* jslint-jscocoa.js */; }; 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; @@ -128,6 +137,22 @@ 8359A63313FE1FCA00AC37E3 /* worker-css.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = "worker-css.js"; path = "HTML/ace/worker-css.js"; sourceTree = ""; }; 8359A63413FE1FCA00AC37E3 /* worker-javascript.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = "worker-javascript.js"; path = "HTML/ace/worker-javascript.js"; sourceTree = ""; }; 8359A6B313FE24B300AC37E3 /* atomicity.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = atomicity.js; path = HTML/atomicity.js; sourceTree = ""; }; + 8359A6ED13FE26DA00AC37E3 /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + 8359A6F813FE270F00AC37E3 /* BridgeSupportController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BridgeSupportController.h; sourceTree = ""; }; + 8359A6F913FE270F00AC37E3 /* BridgeSupportController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BridgeSupportController.m; sourceTree = ""; }; + 8359A6FA13FE270F00AC37E3 /* class.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = class.js; sourceTree = ""; }; + 8359A71313FE270F00AC37E3 /* JSCocoa.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSCocoa.h; sourceTree = ""; }; + 8359A71713FE270F00AC37E3 /* JSCocoaController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSCocoaController.h; sourceTree = ""; }; + 8359A71813FE270F00AC37E3 /* JSCocoaController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSCocoaController.m; sourceTree = ""; }; + 8359A71913FE270F00AC37E3 /* JSCocoaFFIArgument.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSCocoaFFIArgument.h; sourceTree = ""; }; + 8359A71A13FE270F00AC37E3 /* JSCocoaFFIArgument.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSCocoaFFIArgument.m; sourceTree = ""; }; + 8359A71B13FE270F00AC37E3 /* JSCocoaFFIClosure.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSCocoaFFIClosure.h; sourceTree = ""; }; + 8359A71C13FE270F00AC37E3 /* JSCocoaFFIClosure.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSCocoaFFIClosure.m; sourceTree = ""; }; + 8359A71D13FE270F00AC37E3 /* JSCocoaLib.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSCocoaLib.h; sourceTree = ""; }; + 8359A71E13FE270F00AC37E3 /* JSCocoaLib.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSCocoaLib.m; sourceTree = ""; }; + 8359A71F13FE270F00AC37E3 /* JSCocoaPrivateObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSCocoaPrivateObject.h; sourceTree = ""; }; + 8359A72013FE270F00AC37E3 /* JSCocoaPrivateObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSCocoaPrivateObject.m; sourceTree = ""; }; + 8359A72113FE270F00AC37E3 /* jslint-jscocoa.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "jslint-jscocoa.js"; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Atomicity-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Atomicity-Info.plist"; sourceTree = ""; }; 8D1107320486CEB800E47090 /* Atomicity.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Atomicity.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -139,6 +164,7 @@ files = ( 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, 83599F9B13FE0F8400AC37E3 /* WebKit.framework in Frameworks */, + 8359A6EE13FE26DA00AC37E3 /* JavaScriptCore.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -159,6 +185,7 @@ children = ( 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, 83599F9A13FE0F8400AC37E3 /* WebKit.framework */, + 8359A6ED13FE26DA00AC37E3 /* JavaScriptCore.framework */, ); name = "Linked Frameworks"; sourceTree = ""; @@ -188,6 +215,7 @@ 080E96DDFE201D6D7F000001 /* Classes */, 29B97315FDCFA39411CA2CEA /* Other Sources */, 29B97317FDCFA39411CA2CEA /* Resources */, + 8359A6F713FE270F00AC37E3 /* JSCocoa */, 29B97323FDCFA39411CA2CEA /* Frameworks */, 19C28FACFE9D520D11CA2CBB /* Products */, ); @@ -287,6 +315,28 @@ name = ace; sourceTree = ""; }; + 8359A6F713FE270F00AC37E3 /* JSCocoa */ = { + isa = PBXGroup; + children = ( + 8359A6F813FE270F00AC37E3 /* BridgeSupportController.h */, + 8359A6F913FE270F00AC37E3 /* BridgeSupportController.m */, + 8359A6FA13FE270F00AC37E3 /* class.js */, + 8359A71313FE270F00AC37E3 /* JSCocoa.h */, + 8359A71713FE270F00AC37E3 /* JSCocoaController.h */, + 8359A71813FE270F00AC37E3 /* JSCocoaController.m */, + 8359A71913FE270F00AC37E3 /* JSCocoaFFIArgument.h */, + 8359A71A13FE270F00AC37E3 /* JSCocoaFFIArgument.m */, + 8359A71B13FE270F00AC37E3 /* JSCocoaFFIClosure.h */, + 8359A71C13FE270F00AC37E3 /* JSCocoaFFIClosure.m */, + 8359A71D13FE270F00AC37E3 /* JSCocoaLib.h */, + 8359A71E13FE270F00AC37E3 /* JSCocoaLib.m */, + 8359A71F13FE270F00AC37E3 /* JSCocoaPrivateObject.h */, + 8359A72013FE270F00AC37E3 /* JSCocoaPrivateObject.m */, + 8359A72113FE270F00AC37E3 /* jslint-jscocoa.js */, + ); + path = JSCocoa; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -402,6 +452,14 @@ 8D11072D0486CEB800E47090 /* main.m in Sources */, 256AC3DA0F4B6AC300CF3369 /* AtomicityAppDelegate.m in Sources */, 8359A6B413FE24B300AC37E3 /* atomicity.js in Sources */, + 8359A72213FE270F00AC37E3 /* BridgeSupportController.m in Sources */, + 8359A72313FE270F00AC37E3 /* class.js in Sources */, + 8359A72F13FE270F00AC37E3 /* JSCocoaController.m in Sources */, + 8359A73013FE270F00AC37E3 /* JSCocoaFFIArgument.m in Sources */, + 8359A73113FE270F00AC37E3 /* JSCocoaFFIClosure.m in Sources */, + 8359A73213FE270F00AC37E3 /* JSCocoaLib.m in Sources */, + 8359A73313FE270F00AC37E3 /* JSCocoaPrivateObject.m in Sources */, + 8359A73413FE270F00AC37E3 /* jslint-jscocoa.js in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -432,6 +490,10 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)\"", + ); GCC_DYNAMIC_NO_PIC = NO; GCC_ENABLE_FIX_AND_CONTINUE = YES; GCC_MODEL_TUNING = G5; @@ -449,6 +511,10 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)\"", + ); GCC_MODEL_TUNING = G5; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = Atomicity_Prefix.pch; @@ -467,6 +533,10 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNUSED_VARIABLE = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ( + "-lxml2", + "-lffi", + ); PREBINDING = NO; SDKROOT = macosx10.6; }; @@ -479,6 +549,10 @@ GCC_C_LANGUAGE_STANDARD = gnu99; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNUSED_VARIABLE = YES; + OTHER_LDFLAGS = ( + "-lxml2", + "-lffi", + ); PREBINDING = NO; SDKROOT = macosx10.6; }; diff --git a/JSCocoa/BridgeSupportController.h b/JSCocoa/BridgeSupportController.h new file mode 100644 index 000000000..5dbab0f07 --- /dev/null +++ b/JSCocoa/BridgeSupportController.h @@ -0,0 +1,43 @@ +// +// BridgeSupportController.h +// JSCocoa +// +// Created by Patrick Geiller on 08/07/08. +// Copyright 2008 __MyCompanyName__. All rights reserved. +// + +#if !TARGET_IPHONE_SIMULATOR && !TARGET_OS_IPHONE +#import +#endif + +@interface BridgeSupportController : NSObject { + + + NSMutableArray* paths; + NSMutableArray* xmlDocuments; + + NSMutableDictionary* hash; + NSMutableDictionary* variadicSelectors; + NSMutableDictionary* variadicFunctions; +} + ++ (id)sharedController; + +- (BOOL)loadBridgeSupport:(NSString*)path; +- (BOOL)isBridgeSupportLoaded:(NSString*)path; +- (NSUInteger)bridgeSupportIndexForString:(NSString*)string; + +- (NSMutableDictionary*)variadicSelectors; +- (NSMutableDictionary*)variadicFunctions; + +/* +- (NSString*)query:(NSString*)name withType:(NSString*)type; +- (NSString*)query:(NSString*)name withType:(NSString*)type inBridgeSupportFile:(NSString*)file; +*/ +- (NSString*)queryName:(NSString*)name; +- (NSString*)queryName:(NSString*)name type:(NSString*)type; + +- (NSArray*)keys; + + +@end diff --git a/JSCocoa/BridgeSupportController.m b/JSCocoa/BridgeSupportController.m new file mode 100644 index 000000000..82976be77 --- /dev/null +++ b/JSCocoa/BridgeSupportController.m @@ -0,0 +1,254 @@ +// +// BridgeSupportController.m +// JSCocoa +// +// Created by Patrick Geiller on 08/07/08. +// Copyright 2008 __MyCompanyName__. All rights reserved. +// + +#import "BridgeSupportController.h" + + +@implementation BridgeSupportController + + ++ (id)sharedController +{ + static id singleton; + @synchronized(self) + { + if (!singleton) + singleton = [[BridgeSupportController alloc] init]; + return singleton; + } + return singleton; +} + +- (id)init +{ + self = [super init]; + + paths = [[NSMutableArray alloc] init]; + xmlDocuments = [[NSMutableArray alloc] init]; + hash = [[NSMutableDictionary alloc] init]; + variadicSelectors = [[NSMutableDictionary alloc] init]; + variadicFunctions = [[NSMutableDictionary alloc] init]; + + return self; +} + +- (void)dealloc +{ + [variadicFunctions release]; + [variadicSelectors release]; + [hash release]; + [paths release]; + [xmlDocuments release]; + + [super dealloc]; +} + +// +// Load a bridgeSupport file into a hash as { name : xmlTagString } +// +- (BOOL)loadBridgeSupport:(NSString*)path +{ + NSError* error = nil; + /* + Adhoc parser + NSXMLDocument is too slow + loading xml document as string then querying on-demand is too slow + can't get CFXMLParserRef to work + don't wan't to delve into expat + -> ad hoc : load file, build a hash of { name : xmlTagString } + */ + NSString* xmlDocument = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error) return NSLog(@"loadBridgeSupport : %@", error), NO; + + char* c = (char*)[xmlDocument UTF8String]; +#ifdef __OBJC_GC__ + char* originalC = c; + [[NSGarbageCollector defaultCollector] disableCollectorForPointer:originalC]; +#endif + +// double t0 = CFAbsoluteTimeGetCurrent(); + // Start parsing + for (; *c; c++) + { + if (*c == '<') + { + char startTagChar = c[1]; + if (startTagChar == 0) return NO; + + // 'co' constant + // 'cl' class + // 'e' enum + // 'fu' function + // 'st' struct + if ((c[1] == 'c' && (c[2] == 'o' || c[2] == 'l')) || c[1] == 'e' || (c[1] == 'f' && c[2] == 'u') || (c[1] == 's' && c[2] == 't')) + { + // Extract name + char* tagStart = c; + for (; *c && *c != '\''; c++); + c++; + char* c0 = c; + for (; *c && *c != '\''; c++); + + id name = [[NSString alloc] initWithBytes:c0 length:c-c0 encoding:NSUTF8StringEncoding]; + + // Move to tag end + BOOL foundEndTag = NO; + BOOL foundOpenTag = NO; + c++; + for (; *c && !foundEndTag; c++) + { + if (*c == '<') foundOpenTag = YES; + else + if (*c == '/') + { + if (!foundOpenTag) + { + if(c[1] == '>') foundEndTag = YES, c++; + } + else + { + if (startTagChar == c[1]) + { + foundEndTag = YES; + // Skip to end of tag + for (; *c && *c != '>'; c++); + } + } + } + else + // Variadic parsing + if (c[0] == 'v' && c[1] == 'a' && c[2] == 'r') + { + if (strncmp(c, "variadic", 8) == 0) + { + // Skip back to tag start + c0 = c; + for (; *c0 != '<'; c0--); + + // Tag name starts with 'm' : variadic method + // + if (c0[1] == 'm') + { + c = c0; + id variadicMethodName = nil; + // Extract selector name + for (; *c != '>'; c++) + { + if (c[0] == ' ' && c[1] == 's' && c[2] == 'e' && c[3] == 'l') + { + for (; *c && *c != '\''; c++); + c++; + c0 = c; + for (; *c && *c != '\''; c++); + variadicMethodName = [[[NSString alloc] initWithBytes:c0 length:c-c0 encoding:NSUTF8StringEncoding] autorelease]; + } + } + [variadicSelectors setValue:@"true" forKey:variadicMethodName]; +// NSLog(@"SELECTOR %@", name); + } + else + // Variadic function + // + { + [variadicFunctions setValue:@"true" forKey:name]; +// NSLog(@"function %@", name); + } + } + } + } + + c0 = tagStart; + id value = [[NSString alloc] initWithBytes:c0 length:c-c0 encoding:NSUTF8StringEncoding]; + + [hash setValue:value forKey:name]; + [value release]; + [name release]; + } + } + } +// double t1 = CFAbsoluteTimeGetCurrent(); +// NSLog(@"BridgeSupport %@ parsed in %f", [[path lastPathComponent] stringByDeletingPathExtension], t1-t0); +#ifdef __OBJC_GC__ + [[NSGarbageCollector defaultCollector] enableCollectorForPointer:originalC]; +#endif + [paths addObject:path]; + [xmlDocuments addObject:xmlDocument]; + + return YES; +} + + +- (BOOL)isBridgeSupportLoaded:(NSString*)path +{ + NSUInteger idx = [self bridgeSupportIndexForString:path]; + return idx == NSNotFound ? NO : YES; +} + +// +// bridgeSupportIndexForString +// given 'AppKit', return index of '/System/Library/Frameworks/AppKit.framework/Versions/C/Resources/BridgeSupport/AppKitFull.bridgesupport' +// +- (NSUInteger)bridgeSupportIndexForString:(NSString*)string +{ + NSUInteger i, l = [paths count]; + for (i=0; i +#import +#define MACOSX +#import +#endif +#import "BridgeSupportController.h" +#import "JSCocoaPrivateObject.h" +#import "JSCocoaFFIArgument.h" +#import "JSCocoaFFIClosure.h" + + +// JS value container, used by methods wanting a straight JSValue and not a converted JS->ObjC value. +struct JSValueRefAndContextRef +{ + JSValueRef value; + JSContextRef ctx; +}; +typedef struct JSValueRefAndContextRef JSValueRefAndContextRef; + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE +#import "iPhone/libffi/ffi.h" +#import "iPhone/BurksPool.h" +#endif + + +// +// JSCocoaController +// +@interface JSCocoaController : NSObject { + + JSGlobalContextRef ctx; + BOOL ownsContext; + id _delegate; + + // + // Split call + // Allows calling multi param ObjC messages with a jQuery-like syntax. + // + // obj.do({ this : 'hello', andThat : 'world' }) + // instead of + // obj.dothis_andThat_('hello', 'world') + // + BOOL useSplitCall; + + // JSLint : used for ObjJ syntax, class syntax, return if + BOOL useJSLint; + + // Auto call zero arg methods : allow NSWorkspace.sharedWorkspace instead of NSWorkspace.sharedWorkspace() + BOOL useAutoCall; + // Allow setting javascript values on boxed objects (which are collected after nulling all references to them) + BOOL canSetOnBoxedObjects; + // Allow calling obj.method(...) instead of obj.method_(...) + BOOL callSelectorsMissingTrailingSemicolon; + + // Log all exceptions to NSLog, even if they're caught later by downstream Javascript (in f(g()), log even if f catches after g threw) + BOOL logAllExceptions; + + // + // Safe dealloc (For ObjC classes written in Javascript) + // - (void)dealloc cannot be overloaded as it is called during JS GC, which forbids new JS code execution. + // As the js dealloc method cannot be called, safe dealloc allows it to be executed during the next run loop cycle + // NOTE : upon destroying a JSCocoaController, safe dealloc is disabled + // + BOOL useSafeDealloc; + + + NSMutableDictionary* boxedObjects; + + +} + +@property (assign) id delegate; +@property BOOL useSafeDealloc, useSplitCall, useJSLint, useAutoCall, callSelectorsMissingTrailingSemicolon, canSetOnBoxedObjects, logAllExceptions; + + +- (id)init; +- (id)initWithGlobalContext:(JSGlobalContextRef)ctx; + ++ (id)sharedController; ++ (id)controllerFromContext:(JSContextRef)ctx; ++ (BOOL)hasSharedController; +- (JSGlobalContextRef)ctx; ++ (void)hazardReport; ++ (NSString*)runningArchitecture; ++ (void)updateCustomCallPaths; +- (void)accomodateWebKitInspector; + +// +// Evaluation +// +- (id)eval:(NSString*)script; +- (id)callFunction:(NSString*)name; +- (id)callFunction:(NSString*)name withArguments:(NSArray*)arguments; +- (BOOL)hasFunction:(NSString*)name; +- (BOOL)isSyntaxValid:(NSString*)script; + +- (BOOL)evalJSFile:(NSString*)path; +- (BOOL)evalJSFile:(NSString*)path toJSValueRef:(JSValueRef*)returnValue; +- (JSValueRef)evalJSString:(NSString*)script; +- (JSValueRef)evalJSString:(NSString*)script withScriptPath:(NSString*)path; +- (JSValueRef)callJSFunction:(JSValueRef)function withArguments:(NSArray*)arguments; +- (JSValueRef)callJSFunctionNamed:(NSString*)functionName withArguments:arguments, ... NS_REQUIRES_NIL_TERMINATION; +- (JSValueRef)callJSFunctionNamed:(NSString*)functionName withArgumentsArray:(NSArray*)arguments; +- (JSObjectRef)JSFunctionNamed:(NSString*)functionName; +- (BOOL)hasJSFunctionNamed:(NSString*)functionName; +- (NSString*)expandJSMacros:(NSString*)script path:(NSString*)path; +- (NSString*)expandJSMacros:(NSString*)script path:(NSString*)path errors:(NSMutableArray*)array; +- (BOOL)isSyntaxValid:(NSString*)script error:(NSString**)error; +- (BOOL)setObject:(id)object withName:(NSString*)name; +- (BOOL)setObject:(id)object withName:(NSString*)name attributes:(JSPropertyAttributes)attributes; +- (BOOL)setObjectNoRetain:(id)object withName:(NSString*)name attributes:(JSPropertyAttributes)attributes; +- (id)objectWithName:(NSString*)name; +- (BOOL)removeObjectWithName:(NSString*)name; +// Get ObjC and raw values from Javascript +- (id)unboxJSValueRef:(JSValueRef)jsValue; +- (BOOL)toBool:(JSValueRef)value; +- (double)toDouble:(JSValueRef)value; +- (int)toInt:(JSValueRef)value; +- (NSString*)toString:(JSValueRef)value; +// Wrapper for unboxJSValueRef +- (id)toObject:(JSValueRef)value; + + +// +// Framework +// +- (BOOL)loadFrameworkWithName:(NSString*)name; +- (BOOL)loadFrameworkWithName:(NSString*)frameworkName inPath:(NSString*)path; + +// +// Garbage collection +// ++ (void)garbageCollect; +- (void)garbageCollect; +- (void)unlinkAllReferences; ++ (void)upJSCocoaPrivateObjectCount; ++ (void)downJSCocoaPrivateObjectCount; ++ (int)JSCocoaPrivateObjectCount; + ++ (void)upJSValueProtectCount; ++ (void)downJSValueProtectCount; ++ (int)JSValueProtectCount; + ++ (void)logInstanceStats; +- (id)instanceStats; +- (void)logBoxedObjects; + +// +// Class inspection (shortcuts to JSCocoaLib) +// ++ (id)rootclasses; ++ (id)classes; ++ (id)protocols; ++ (id)imageNames; ++ (id)methods; ++ (id)runtimeReport; ++ (id)explainMethodEncoding:(id)encoding; + +// +// Class handling +// ++ (BOOL)overloadInstanceMethod:(NSString*)methodName class:(Class)class jsFunction:(JSValueRefAndContextRef)valueAndContext; ++ (BOOL)overloadClassMethod:(NSString*)methodName class:(Class)class jsFunction:(JSValueRefAndContextRef)valueAndContext; + ++ (BOOL)addClassMethod:(NSString*)methodName class:(Class)class jsFunction:(JSValueRefAndContextRef)valueAndContext encoding:(char*)encoding; ++ (BOOL)addInstanceMethod:(NSString*)methodName class:(Class)class jsFunction:(JSValueRefAndContextRef)valueAndContext encoding:(char*)encoding; + +// Tests +- (int)runTests:(NSString*)path; +- (int)runTests:(NSString*)path withSelector:(SEL)sel; + +// +// Autorelease pool +// ++ (void)allocAutoreleasePool; ++ (void)deallocAutoreleasePool; + +// +// Boxing : each object gets only one box, stored in boxedObjects +// +//+ (JSObjectRef)boxedJSObject:(id)o inContext:(JSContextRef)ctx; +- (JSObjectRef)boxObject:(id)o; +- (BOOL)isObjectBoxed:(id)o; +- (void)deleteBoxOfObject:(id)o; +//+ (void)downBoxedJSObjectCount:(id)o; + + +// +// Various internals +// +//+ (JSObjectRef)jsCocoaPrivateObjectInContext:(JSContextRef)ctx; +- (JSObjectRef)newPrivateObject; +- (JSObjectRef)newPrivateFunction; ++ (NSMutableArray*)parseObjCMethodEncoding:(const char*)typeEncoding; ++ (NSMutableArray*)parseCFunctionEncoding:(NSString*)xml functionName:(NSString**)functionNamePlaceHolder; + +//+ (void)ensureJSValueIsObjectAfterInstanceAutocall:(JSValueRef)value inContext:(JSContextRef)ctx; +- (NSString*)formatJSException:(JSValueRef)exception; +- (id)selectorForJSFunction:(JSObjectRef)function; + + +- (const char*)typeEncodingOfMethod:(NSString*)methodName class:(NSString*)className; ++ (const char*)typeEncodingOfMethod:(NSString*)methodName class:(NSString*)className; + + + +@end + + +// +// JSCocoa delegate methods +// + +// +// Error reporting +// +@interface NSObject (JSCocoaControllerDelegateMethods) +- (void) JSCocoa:(JSCocoaController*)controller hadError:(NSString*)error onLineNumber:(NSInteger)lineNumber atSourceURL:(id)url; +- (void) safeDealloc; + +// +// Getting +// +// Check if getting property is allowed +- (BOOL) JSCocoa:(JSCocoaController*)controller canGetProperty:(NSString*)propertyName ofObject:(id)object inContext:(JSContextRef)ctx exception:(JSValueRef*)exception; +// Custom handler for getting properties +// Return a custom JSValueRef to bypass JSCocoa +// Return NULL to let JSCocoa handle getProperty +// Return JSValueMakeNull() to return a Javascript null +- (JSValueRef) JSCocoa:(JSCocoaController*)controller getProperty:(NSString*)propertyName ofObject:(id)object inContext:(JSContextRef)ctx exception:(JSValueRef*)exception; + +// +// Setting +// +// Check if setting property is allowed +- (BOOL) JSCocoa:(JSCocoaController*)controller canSetProperty:(NSString*)propertyName ofObject:(id)object toValue:(JSValueRef)value inContext:(JSContextRef)ctx exception:(JSValueRef*)exception; +// Custom handler for setting properties +// Return YES to indicate you handled setting +// Return NO to let JSCocoa handle setProperty +- (BOOL) JSCocoa:(JSCocoaController*)controller setProperty:(NSString*)propertyName ofObject:(id)object toValue:(JSValueRef)value inContext:(JSContextRef)ctx exception:(JSValueRef*)exception; + +// +// Calling +// +// Check if calling a C function is allowed +- (BOOL) JSCocoa:(JSCocoaController*)controller canCallFunction:(NSString*)functionName argumentCount:(size_t)argumentCount arguments:(JSValueRef*)arguments inContext:(JSContextRef)ctx exception:(JSValueRef*)exception; +// Check if calling an ObjC method is allowed +- (BOOL) JSCocoa:(JSCocoaController*)controller canCallMethod:(NSString*)methodName ofObject:(id)object argumentCount:(size_t)argumentCount arguments:(JSValueRef*)arguments inContext:(JSContextRef)ctx exception:(JSValueRef*)exception; +// Custom handler for calling +// Return YES to indicate you handled calling +// Return NO to let JSCocoa handle calling +- (JSValueRef) JSCocoa:(JSCocoaController*)controller callMethod:(NSString*)methodName ofObject:(id)callee privateObject:(JSCocoaPrivateObject*)thisPrivateObject argumentCount:(size_t)argumentCount arguments:(JSValueRef*)arguments inContext:(JSContextRef)localCtx exception:(JSValueRef*)exception; + +// +// Getting global properties (classes, structures, C function names, enums via OSXObject_getProperty) +// +// Check if getting property is allowed +- (BOOL) JSCocoa:(JSCocoaController*)controller canGetGlobalProperty:(NSString*)propertyName inContext:(JSContextRef)ctx exception:(JSValueRef*)exception; +// Custom handler for getting properties +// Return a custom JSValueRef to bypass JSCocoa +// Return NULL to let JSCocoa handle getProperty +// Return JSValueMakeNull() to return a Javascript null +- (JSValueRef) JSCocoa:(JSCocoaController*)controller getGlobalProperty:(NSString*)propertyName inContext:(JSContextRef)ctx exception:(JSValueRef*)exception; + +// +// Returning values to Javascript +// +// Called before returning any value to Javascript : return a new value or the original one +//- (JSValueRef) JSCocoa:(JSCocoaController*)controller willReturnValue:(JSValueRef)value inContext:(JSContextRef)ctx exception:(JSValueRef*)exception; + +// +// Evaling +// +// Check if file can be loaded +- (BOOL)JSCocoa:(JSCocoaController*)controller canLoadJSFile:(NSString*)path; +// Check if script can be evaluated +- (BOOL)JSCocoa:(JSCocoaController*)controller canEvaluateScript:(NSString*)script; +// Called before evalJSString, used to modify script about to be evaluated +// Return a custom NSString (eg a macro expanded version of the source) +// Return NULL to let JSCocoa handle evaluation +- (NSString*)JSCocoa:(JSCocoaController*)controller willEvaluateScript:(NSString*)script; + +@end + + +// +// JSCocoa shorthand +// +@interface JSCocoa : JSCocoaController +@end + +// +// Boxed object cache : holds one JSObjectRef for each reference to a pointer to an ObjC object +// +@interface BoxedJSObject : NSObject { + JSObjectRef jsObject; +} +- (void)setJSObject:(JSObjectRef)o; +- (JSObjectRef)jsObject; + +@end + +// +// Helpers +// +id NSStringFromJSValue(JSContextRef ctx, JSValueRef value); +//void* malloc_autorelease(size_t size); + +// Convert values between contexts (eg user context and webkit page context) +JSValueRef valueToExternalContext(JSContextRef ctx, JSValueRef value, JSContextRef externalCtx); + +// valueOf() is called by Javascript on objects, eg someObject + ' someString' +JSValueRef valueOfCallback(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception); + +// +// From PyObjC : when to call objc_msgSend_stret, for structure return +// Depending on structure size & architecture, structures are returned as function first argument (done transparently by ffi) or via registers +// + +#if defined(__ppc__) +# define SMALL_STRUCT_LIMIT 4 +#elif defined(__ppc64__) +# define SMALL_STRUCT_LIMIT 8 +#elif defined(__i386__) +# define SMALL_STRUCT_LIMIT 8 +#elif defined(__x86_64__) +# define SMALL_STRUCT_LIMIT 16 +#elif TARGET_OS_IPHONE +// TOCHECK +# define SMALL_STRUCT_LIMIT 4 +#else +# error "Unsupported MACOSX platform" +#endif + + +// Stored in boxedobjects to access a list of methods, properties, ... +#define RuntimeInformationPropertyName "info" + + + +/* +Some more doc + + __jsHash + __jsCocoaController + Instance variables set on ObjC classes written in Javascript. + These variables enable classes to store Javascript values in them. + +*/ + + diff --git a/JSCocoa/JSCocoaController.m b/JSCocoa/JSCocoaController.m new file mode 100644 index 000000000..bb2a472f4 --- /dev/null +++ b/JSCocoa/JSCocoaController.m @@ -0,0 +1,5035 @@ +// +// JSCocoa.m +// JSCocoa +// +// Created by Patrick Geiller on 09/07/08. +// Copyright 2008 __MyCompanyName__. All rights reserved. +// + + +#import "JSCocoaController.h" +#import "JSCocoaLib.h" + +#pragma mark JS objects forward definitions + +// Global object +static JSValueRef OSXObject_getProperty(JSContextRef, JSObjectRef, JSStringRef, JSValueRef*); +static void OSXObject_getPropertyNames(JSContextRef, JSObjectRef, JSPropertyNameAccumulatorRef); + +// Private JS object callbacks +static void jsCocoaObject_initialize(JSContextRef, JSObjectRef); +static void jsCocoaObject_finalize(JSObjectRef); +static JSValueRef jsCocoaObject_callAsFunction(JSContextRef, JSObjectRef, JSObjectRef, size_t, const JSValueRef [], JSValueRef*); +//static bool jsCocoaObject_hasProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName); +static JSValueRef jsCocoaObject_getProperty(JSContextRef, JSObjectRef, JSStringRef, JSValueRef*); +static bool jsCocoaObject_setProperty(JSContextRef, JSObjectRef, JSStringRef, JSValueRef, JSValueRef*); +static bool jsCocoaObject_deleteProperty(JSContextRef, JSObjectRef, JSStringRef, JSValueRef*); +static void jsCocoaObject_getPropertyNames(JSContextRef, JSObjectRef, JSPropertyNameAccumulatorRef); +static JSObjectRef jsCocoaObject_callAsConstructor(JSContextRef, JSObjectRef, size_t, const JSValueRef [], JSValueRef*); +static JSValueRef jsCocoaObject_convertToType(JSContextRef ctx, JSObjectRef object, JSType type, JSValueRef* exception); +static bool jsCocoaObject_hasInstance(JSContextRef ctx, JSObjectRef constructor, JSValueRef possibleInstance, JSValueRef* exception); + +static JSValueRef jsCocoaInfo_getProperty(JSContextRef, JSObjectRef, JSStringRef, JSValueRef*); +static void jsCocoaInfo_getPropertyNames(JSContextRef, JSObjectRef, JSPropertyNameAccumulatorRef); + +// Set on valueOf callback property of objects +#define JSCocoaInternalAttribute kJSPropertyAttributeDontEnum + +// These will be destroyed when the last JSCocoa instance dies +static JSClassRef OSXObjectClass = NULL; +static JSClassRef jsCocoaObjectClass = NULL; +static JSClassRef jsCocoaFunctionClass= NULL; +static JSClassRef jsCocoaInfoClass = NULL; +static JSClassRef hashObjectClass = NULL; + +// Convenience method to throw a Javascript exception +static void throwException(JSContextRef ctx, JSValueRef* exception, NSString* reason); + + +// iPhone specifics +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE +const JSClassDefinition kJSClassDefinitionEmpty = { 0, 0, + NULL, NULL, + NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }; +#import "GDataDefines.h" +#import "GDataXMLNode.h" +#endif + +// Appended to swizzled method names +#define OriginalMethodPrefix @"original" + + + + + + + +// +// JSCocoaController +// +#pragma mark - +#pragma mark JSCocoaController + +@interface JSCocoaController (Private) +- (void) callDelegateForException:(JSValueRef)exception; +@end + +@implementation JSCocoaController + + +// Instance properties +@synthesize delegate=_delegate; +@synthesize useSafeDealloc, useSplitCall, useJSLint, useAutoCall, callSelectorsMissingTrailingSemicolon, canSetOnBoxedObjects, logAllExceptions; + +// Shared data + + // Given a jsFunction, retrieve its closure (jsFunction's pointer address is used as key) + static id closureHash; + // Given a jsFunction, retrieve its selector + static id jsFunctionSelectors; + // Given a jsFunction, retrieve which class it's attached to + static id jsFunctionClasses; + // Given a class, return the parent class implementing JSCocoaHolder method + static id jsClassParents; + // List of all ObjC classes written in Javascript + static id jsClasses; + + // Given a class + methodName, retrieve its jsFunction + static id jsFunctionHash; + + // Split call cache + static id splitCallCache; + + // Shared instance stats + static id sharedInstanceStats = nil; + + // Boxed objects +// static id boxedObjects; + + + // Auto call zero arg methods : allow NSWorkspace.sharedWorkspace instead of NSWorkspace.sharedWorkspace() +// static BOOL useAutoCall; + // Allow calling obj.method(...) instead of obj.method_(...) +// static BOOL callSelectorsMissingTrailingSemicolon; + // Allows setting javascript values on boxed objects (which are collected after nulling all references to them) +// static BOOL canSetOnBoxedObjects; + + // If true, all exceptions will be sent to NSLog, event if they're caught later on by some Javascript core +// static BOOL logAllExceptions; + // Is speaking when throwing exceptions +// static BOOL isSpeaking; + + // Controller count + static int controllerCount = 0; + + // Hash used to quickly check for variadic methods, Original, Super, toString, valueOf ... + NSMutableDictionary* customCallPaths; + BOOL customCallPathsCacheIsClean; + + // Javascript functions defined for ObjC classes are stored in this hash + // __globalJSFunctionRepository__[className][propertyName] + +// +// Init +// +- (id)initWithGlobalContext:(JSGlobalContextRef)_ctx +{ +// NSLog(@"JSCocoa : %p spawning with context %p", self, _ctx); + self = [super init]; + controllerCount++; + + useAutoCall = YES; + callSelectorsMissingTrailingSemicolon = YES; + canSetOnBoxedObjects= NO; + logAllExceptions = NO; + boxedObjects = [NSMutableDictionary new]; + + @synchronized(self) + { + if (!sharedInstanceStats) + { + sharedInstanceStats = [NSMutableDictionary new]; + closureHash = [NSMutableDictionary new]; + jsFunctionSelectors = [NSMutableDictionary new]; + jsFunctionClasses = [NSMutableDictionary new]; + jsFunctionHash = [NSMutableDictionary new]; + splitCallCache = [NSMutableDictionary new]; + jsClassParents = [NSMutableDictionary new]; +// boxedObjects = [NSMutableDictionary new]; + jsClasses = [NSMutableArray new]; + customCallPathsCacheIsClean = NO; + customCallPaths = nil; + } + } + + // + // Javascript classes with our callbacks + // + if (!OSXObjectClass) { + // + // OSX object javascript definition + // + JSClassDefinition OSXObjectDefinition = kJSClassDefinitionEmpty; + OSXObjectDefinition.className = "OSX"; + OSXObjectDefinition.getProperty = OSXObject_getProperty; + OSXObjectDefinition.getPropertyNames = OSXObject_getPropertyNames; + OSXObjectClass = JSClassCreate(&OSXObjectDefinition); + + + // + // Private object, used for holding references to objects, classes, structs + // + JSClassDefinition jsCocoaObjectDefinition = kJSClassDefinitionEmpty; + jsCocoaObjectDefinition.className = "JSCocoa box"; + jsCocoaObjectDefinition.initialize = jsCocoaObject_initialize; + jsCocoaObjectDefinition.finalize = jsCocoaObject_finalize; +// jsCocoaObjectDefinition.hasProperty = jsCocoaObject_hasProperty; + jsCocoaObjectDefinition.getProperty = jsCocoaObject_getProperty; + jsCocoaObjectDefinition.setProperty = jsCocoaObject_setProperty; + jsCocoaObjectDefinition.deleteProperty = jsCocoaObject_deleteProperty; + jsCocoaObjectDefinition.getPropertyNames = jsCocoaObject_getPropertyNames; +// jsCocoaObjectDefinition.callAsFunction = jsCocoaObject_callAsFunction; + jsCocoaObjectDefinition.callAsConstructor = jsCocoaObject_callAsConstructor; +// jsCocoaObjectDefinition.hasInstance = jsCocoaObject_hasInstance; + jsCocoaObjectDefinition.convertToType = jsCocoaObject_convertToType; + jsCocoaObjectClass = JSClassCreate(&jsCocoaObjectDefinition); + + + // + // Second kind of private object, used to hold method and function names + // Separated from the object because "typeof NSDate.date" gave "function" instead of object, preventing enumeration in WebKit inspector + // + JSClassDefinition jsCocoaFunctionDefinition = kJSClassDefinitionEmpty; + jsCocoaFunctionDefinition.className = "JSCocoa box"; + jsCocoaFunctionDefinition.parentClass = jsCocoaObjectClass; + jsCocoaFunctionDefinition.callAsFunction = jsCocoaObject_callAsFunction; + jsCocoaFunctionClass = JSClassCreate(&jsCocoaFunctionDefinition); + + + // + // Holds __info in objects + // + JSClassDefinition jsCocoaInfoDefinition = kJSClassDefinitionEmpty; + jsCocoaInfoDefinition.className = "Runtime info"; + jsCocoaInfoDefinition.getProperty = jsCocoaInfo_getProperty; + jsCocoaInfoDefinition.getPropertyNames = jsCocoaInfo_getPropertyNames; + jsCocoaInfoClass = JSClassCreate(&jsCocoaInfoDefinition); + + + // + // Private Hash of derived classes, storing js values + // + JSClassDefinition jsCocoaHashObjectDefinition = kJSClassDefinitionEmpty; + hashObjectClass = JSClassCreate(&jsCocoaHashObjectDefinition); + } + + // + // Start context + // + + // Starting from our own context + if (!_ctx) + { + ctx = JSGlobalContextCreate(OSXObjectClass); + } + // Starting from an existing context + else + { + ctx = _ctx; + //JSGlobalContextRetain(ctx); + JSObjectRef o = JSObjectMake(ctx, OSXObjectClass, NULL); + // Set a global var named 'OSX' which will fulfill the usual role of JSCocoa's global object + JSStringRef jsName = JSStringCreateWithUTF8CString("OSX"); + JSObjectSetProperty(ctx, JSContextGetGlobalObject(ctx), jsName, o, kJSPropertyAttributeDontDelete, NULL); + JSStringRelease(jsName); + + [self accomodateWebKitInspector]; + } + +#if !TARGET_IPHONE_SIMULATOR && !TARGET_OS_IPHONE + [self loadFrameworkWithName:@"AppKit"]; + [self loadFrameworkWithName:@"CoreFoundation"]; + [self loadFrameworkWithName:@"Foundation"]; + [self loadFrameworkWithName:@"CoreGraphics" inPath:@"/System/Library/Frameworks/ApplicationServices.framework/Frameworks"]; +#endif + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + [BurksPool setJSFunctionHash:jsFunctionHash]; +#endif + // Create a reference to ourselves, and make it read only, don't enum, don't delete + [self setObjectNoRetain:self withName:@"__jsc__" attributes:kJSPropertyAttributeReadOnly|kJSPropertyAttributeDontEnum|kJSPropertyAttributeDontDelete]; + + // Load class kit + if (!_ctx) + { + useJSLint = NO; + id lintPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"jslint-jscocoa" ofType:@"js"]; + if ([[NSFileManager defaultManager] fileExistsAtPath:lintPath]) { + BOOL b = [self evalJSFile:lintPath]; + if (!b) + NSLog(@"[JSCocoa initWithGlobalContext:] JSLint not loaded"); + } + id classKitPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"class" ofType:@"js"]; + if ([[NSFileManager defaultManager] fileExistsAtPath:classKitPath]) [self evalJSFile:classKitPath]; + } + + // Objects can use their own dealloc, normally used up by JSCocoa + // JSCocoa registers 'safeDealloc' in place of 'dealloc' and calls it in the next run loop cycle. + // (Dealloc might be called by JS GC, and running JS fails at this time) + // useSafeDealloc will be turned to NO upon JSCocoaController dealloc + useSafeDealloc = YES; + // Yep ! + useJSLint = YES; + // ObjJ syntax renders split call moot + useSplitCall = NO; + ownsContext = NO; + + [JSCocoa updateCustomCallPaths]; + return self; +} + +- (id)init +{ + id o = [self initWithGlobalContext:nil]; + ownsContext = YES; + return o; +} + + +// +// Dealloc +// +- (void)cleanUp +{ +// NSLog(@"JSCocoa : %p dying (ownsContext=%d)", self, ownsContext); + [self setUseSafeDealloc:NO]; + + // Cleanup if we created the JavascriptCore context. + // If not, let user do it. In a WebView, this method will be called during JS GC, + // and trying to execute more JS code will fail. + // User must clean up manually by calling unlinkAllReferences then destroying the webView + + + if (ownsContext) { + [self unlinkAllReferences]; + JSGarbageCollect(ctx); + [self setObjectNoRetain:self withName:@"__jsc__" attributes:kJSPropertyAttributeReadOnly|kJSPropertyAttributeDontEnum|kJSPropertyAttributeDontDelete]; + } + + controllerCount--; + if (controllerCount == 0) + { + if (OSXObjectClass) { + JSClassRelease(OSXObjectClass); + JSClassRelease(jsCocoaObjectClass); + JSClassRelease(jsCocoaFunctionClass); + JSClassRelease(jsCocoaInfoClass); + JSClassRelease(hashObjectClass); + OSXObjectClass = nil; + jsCocoaObjectClass = nil; + jsCocoaFunctionClass = nil; + jsCocoaInfoClass = nil; + hashObjectClass = nil; + } + + // We need to nil these all out, since they are static + // and if we make another JSCocoaController after this- they will + // still be around and that's kinda bad (like crashing bad). + [sharedInstanceStats release]; + sharedInstanceStats = nil; + [closureHash release]; + closureHash = nil; + [jsFunctionSelectors release]; + jsFunctionSelectors = nil; + [jsFunctionClasses release]; + jsFunctionClasses = nil; + [jsFunctionHash release]; + jsFunctionHash = nil; + [splitCallCache release]; + splitCallCache = nil; + [jsClassParents release]; + jsClassParents = nil; +// [boxedObjects release]; +// boxedObjects = nil; + [customCallPaths release]; + customCallPaths = nil; + + + // Remove classes : go backwards to remove child classes first + for (id class in [jsClasses reverseObjectEnumerator]) + objc_disposeClassPair([class pointerValue]); + + [jsClasses release]; + jsClasses = nil; + } + + [self removeObjectWithName:@"__jsc__"]; + if (ownsContext) + JSGlobalContextRelease(ctx); + + [boxedObjects release]; +} + +- (void)dealloc +{ + [self cleanUp]; + [super dealloc]; +} +- (void)finalize +{ + [self cleanUp]; + [super finalize]; +} + + +// +// Shared instance +// +static id JSCocoaSingleton = NULL; + ++ (id)sharedController +{ + @synchronized(self) + { + if (!JSCocoaSingleton) + { + // 1. alloc + // 2. store pointer + // 3. call init + // + // Why ? if init is calling sharedController, the pointer won't have been set and it will call itself over and over again. + // + JSCocoaSingleton = [self alloc]; +// NSLog(@"JSCocoa : allocating shared instance %p", JSCocoaSingleton); + [JSCocoaSingleton init]; + } + } + return JSCocoaSingleton; +} ++ (BOOL)hasSharedController +{ + return !!JSCocoaSingleton; +} + +// Retrieves the __jsc__ variable from a context and unbox it ++ (id)controllerFromContext:(JSContextRef)ctx +{ + JSStringRef jsName = JSStringCreateWithUTF8CString("__jsc__"); + JSValueRef jsValue = JSObjectGetProperty(ctx, JSContextGetGlobalObject(ctx), jsName, NULL); + JSStringRelease(jsName); + id jsc = nil; + [JSCocoaFFIArgument unboxJSValueRef:jsValue toObject:&jsc inContext:ctx]; + // Commented as it falsely reports failure when controller is cleaning up while being deallocated +// if (!jsc) NSLog(@"controllerFromContext couldn't find found the JSCocoaController in ctx %p", ctx); + return jsc; +} + +// Report if we're running a nightly JavascriptCore, with GC ++ (void)hazardReport +{ + Dl_info info; + // Get info about a JavascriptCore symbol + dladdr(dlsym(RTLD_DEFAULT, "JSClassCreate"), &info); + + BOOL runningFromSystemLibrary = [[NSString stringWithUTF8String:info.dli_fname] hasPrefix:@"/System"]; + if (!runningFromSystemLibrary) NSLog(@"***Running a nightly JavascriptCore***"); +#if !TARGET_OS_IPHONE + if ([NSGarbageCollector defaultCollector]) NSLog(@"***Running with ObjC Garbage Collection***"); +#endif +} +// Report what we're running on ++ (NSString*)runningArchitecture +{ +#if defined(__ppc__) + return @"PPC"; +// Unsupported +//#elif defined(__ppc64__) +// return @"PPC64"; +#elif defined(__i386__) + return @"i386"; +#elif defined(__x86_64__) + return @"x86_64"; +#elif TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR + return @"iPhone"; +#elif TARGET_OS_IPHONE && TARGET_IPHONE_SIMULATOR + return @"iPhone Simulator"; +#else + return @"unknown architecture"; +#endif +} + +// Replace the toString function with our own +- (void)accomodateWebKitInspector +{ + // The inspector uses Object's toString to extract the class name and print it, + // we replace that class name with valueOf when called for JSCocoa boxes + char* script = "\ + var _old_toString = Object.prototype.toString \n\ + Object.prototype.toString = function () \n\ + { \n\ + var str = _old_toString.call(this) \n\ + if (!str.match(/JSCocoa/)) \n\ + return str \n\ + return '[Object ' + (this.valueOf()) + ']' \n\ + } \n\ + "; + JSStringRef scriptJS = JSStringCreateWithCFString((CFStringRef)[NSString stringWithUTF8String:script]); + JSEvaluateScript(ctx, scriptJS, NULL, NULL, 1, NULL); + JSStringRelease(scriptJS); +} + + +#pragma mark Script evaluation + +// +// Quick eval of strings and functions returning ObjC objects +// +- (id)eval:(NSString*)script { return [self toObject:[self evalJSString:script]]; } +- (id)callFunction:(NSString*)name { return [self toObject:[self callJSFunctionNamed:name withArgumentsArray:nil]]; } +- (id)callFunction:(NSString*)name withArguments:(NSArray*)arguments { return [self toObject:[self callJSFunctionNamed:name withArgumentsArray:arguments]]; } +- (BOOL)hasFunction:(NSString*)name { return [self hasJSFunctionNamed:name]; } + +- (BOOL)isSyntaxValid:(NSString*)script { return [self isSyntaxValid:script error:nil]; } + + +// +// Eval of strings, functions, files, returning JavascriptCore objects +// +#pragma mark Script evaluation returning JavascriptCore objects + +// +// Evaluate a file +// +- (BOOL)evalJSFile:(NSString*)path toJSValueRef:(JSValueRef*)returnValue +{ + NSError* error; + id script = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + // Skip .DS_Store and directories + if (script == nil) return NSLog(@"evalJSFile could not open %@ (%@) — Check file encoding (should be UTF8) and file build phase (should be in \"Copy Bundle Resources\")", path, error), NO; + + // + // Delegate canLoadJSFile + // + if (_delegate && [_delegate respondsToSelector:@selector(JSCocoa:canLoadJSFile:)] && ![_delegate JSCocoa:self canLoadJSFile:path]) return NO; + + // Expand macros + script = [self expandJSMacros:script path:path]; + if (!script) { + NSLog(@"evalJSFile:toJSValueRef: expandJSMacros returned null on %@", path); + return NO; + } + + // + // Delegate canEvaluateScript, willEvaluateScript + // + if (_delegate) + { + if ([_delegate respondsToSelector:@selector(JSCocoa:canEvaluateScript:)] && ![_delegate JSCocoa:self canEvaluateScript:script]) return NO; + if ([_delegate respondsToSelector:@selector(JSCocoa:willEvaluateScript:)]) script = [_delegate JSCocoa:self willEvaluateScript:script]; + } + + if (!customCallPathsCacheIsClean) [JSCocoa updateCustomCallPaths]; + + // Convert script and script URL to js strings + // JSStringRef scriptJS = JSStringCreateWithUTF8CString([script UTF8String]); + // Using CreateWithUTF8 yields wrong results on PPC + JSStringRef scriptJS = JSStringCreateWithCFString((CFStringRef)script); + JSStringRef scriptPath = JSStringCreateWithUTF8CString([path UTF8String]); + + // Eval ! + JSValueRef exception = NULL; + JSValueRef result = JSEvaluateScript(ctx, scriptJS, NULL, scriptPath, 1, &exception); + if (returnValue) *returnValue = result; + // Release + JSStringRelease(scriptPath); + JSStringRelease(scriptJS); + if (exception) + { +// NSLog(@"JSException - %@", [self formatJSException:exception]); + [self callDelegateForException:exception]; + return NO; + } + return YES; +} + + +// +// Evaluate a file, without caring about return result +// +- (BOOL)evalJSFile:(NSString*)path +{ + return [self evalJSFile:path toJSValueRef:nil]; +} + +// +// Evaluate a string +// +- (JSValueRef)evalJSString:(NSString*)script withScriptPath:(NSString*)path +{ + if (!script) return NULL; + + // Expand macros + id expandedScript = [self expandJSMacros:script path:path]; + if (expandedScript) + script = expandedScript; + + // + // Delegate canEvaluateScript, willEvaluateScript + // + if (_delegate) + { + if ([_delegate respondsToSelector:@selector(JSCocoa:canEvaluateScript:)] && ![_delegate JSCocoa:self canEvaluateScript:script]) return NULL; + if ([_delegate respondsToSelector:@selector(JSCocoa:willEvaluateScript:)]) script = [_delegate JSCocoa:self willEvaluateScript:script]; + } + + if (!script) + return NSLog(@"evalJSString has nothing to eval"), NULL; + + if (!customCallPathsCacheIsClean) [JSCocoa updateCustomCallPaths]; + + JSStringRef scriptJS = JSStringCreateWithCFString((CFStringRef)script); + JSValueRef exception = NULL; + JSStringRef scriptPath = path ? JSStringCreateWithUTF8CString([path UTF8String]) : NULL; + JSValueRef result = JSEvaluateScript(ctx, scriptJS, NULL, scriptPath, 1, &exception); + JSStringRelease(scriptJS); + if (path) JSStringRelease(scriptPath); + + if (exception) + { + [self callDelegateForException:exception]; + return NULL; + } + + return result; +} + +// Evaluate a string, no script path +- (JSValueRef)evalJSString:(NSString*)script +{ + return [self evalJSString:script withScriptPath:nil]; +} + + + +// +// Call a Javascript function by function reference (JSValueRef) +// +- (JSValueRef)callJSFunction:(JSValueRef)function withArguments:(NSArray*)arguments +{ + JSObjectRef jsFunction = JSValueToObject(ctx, function, NULL); + // Return if function is not of function type + if (!jsFunction) return NSLog(@"callJSFunction : value is not a function"), NULL; + + // Convert arguments + JSValueRef* jsArguments = NULL; + NSUInteger argumentCount = [arguments count]; + if (argumentCount) + { + jsArguments = malloc(sizeof(JSValueRef)*argumentCount); + for (int i=0; i= '0' && *argsParser <= '9') continue; + else + // Skip ObjC 'const', 'oneway' markers + if (*argsParser == 'r' || *argsParser == 'V') continue; + else + if (*argsParser == '{') + { + // Parse structure encoding + NSInteger count = 0; + [JSCocoaFFIArgument typeEncodingsFromStructureTypeEncoding:[NSString stringWithUTF8String:argsParser] parsedCount:&count]; + + id encoding = [[NSString alloc] initWithBytes:argsParser length:count encoding:NSUTF8StringEncoding]; + id argumentEncoding = [[JSCocoaFFIArgument alloc] init]; + // Set return value + if ([argumentEncodings count] == 0) [argumentEncoding setIsReturnValue:YES]; + [argumentEncoding setStructureTypeEncoding:encoding]; + [argumentEncodings addObject:argumentEncoding]; + [argumentEncoding release]; + + [encoding release]; + argsParser += count-1; + } + else + { + // Custom handling for pointers as they're not one char long. +// char type = *argsParser; + char* typeStart = argsParser; + if (*argsParser == '^') + while (*argsParser && !(*argsParser >= '0' && *argsParser <= '9')) argsParser++; + + id argumentEncoding = [[JSCocoaFFIArgument alloc] init]; + // Set return value + if ([argumentEncodings count] == 0) [argumentEncoding setIsReturnValue:YES]; + + // If pointer, copy pointer type (^i, ^{NSRect}) to the argumentEncoding + if (*typeStart == '^') + { + id encoding = [[NSString alloc] initWithBytes:typeStart length:argsParser-typeStart encoding:NSUTF8StringEncoding]; + [argumentEncoding setPointerTypeEncoding:encoding]; + [encoding release]; + } + else + { + BOOL didSet = [argumentEncoding setTypeEncoding:*typeStart]; + if (!didSet) + { + [argumentEncoding release]; + return nil; + } + // Blocks are '@?', skip '?' + if (typeStart[0] == _C_ID && typeStart[1] == _C_UNDEF) + argsParser++; + } + + [argumentEncodings addObject:argumentEncoding]; + [argumentEncoding release]; + } + if (!*argsParser) break; + } + return argumentEncodings; +} + +// +// This is parsed from BridgeSupport's xml +// ++ (NSMutableArray*)parseCFunctionEncoding:(NSString*)xml functionName:(NSString**)functionNamePlaceHolder +{ + id argumentEncodings = [NSMutableArray array]; + id xmlDocument = [[NSXMLDocument alloc] initWithXMLString:xml options:0 error:nil]; + [xmlDocument autorelease]; + + id rootElement = [xmlDocument rootElement]; + *functionNamePlaceHolder = [[rootElement attributeForName:@"name"] stringValue]; + + // Parse children and return value + NSUInteger i, numChildren = [rootElement childCount]; + id returnValue = NULL; + for (i=0; i introduced because under GC, NSData gets collected early. + +*/ ++ (BOOL)trySplitCall:(id*)_methodName class:(Class)class argumentCount:(size_t*)_argumentCount arguments:(JSValueRef**)_arguments ctx:(JSContextRef)c +{ + id methodName = *_methodName; + size_t argumentCount = *_argumentCount; + JSValueRef* arguments = *_arguments; + if (argumentCount != 1) return NO; + + // Get property array + JSObjectRef o = JSValueToObject(c, arguments[0], NULL); + if (!o) return NO; + JSPropertyNameArrayRef jsNames = JSObjectCopyPropertyNames(c, o); + + // Convert js names to NSString names : { jsName1 : value1, jsName2 : value 2 } -> NSArray[name1, name2] + id names = [NSMutableArray array]; + size_t i, nameCount = JSPropertyNameArrayGetCount(jsNames); + // Length of target selector = length of method + length of each (argument + ':') + NSUInteger targetSelectorLength = [methodName length]; + // Actual arguments + JSValueRef* actualArguments = malloc(sizeof(JSValueRef)*nameCount); + for (i=0; i (JS)JSObjectRef(o) --> (ObjC)BoxedJSObject(JSObjectRef(o)), + // ^stored in the boxedObjects hash to always return the same box for the same object + + // + // Create a new ObjC box around the JSValueRef boxing the JSObject + // + // We are returning an ObjC object to Javascript. + // That ObjC object is boxed in a Javascript object. + // For all boxing requests of the same ObjC object, that Javascript object needs to be unique for object comparisons to work : + // NSApplication.sharedApplication == NSApplication.sharedApplication + // (JavascriptCore has no hook for object to object comparison, that's why objects need to be unique) + // To guarantee unicity, we keep a cache of boxed objects. + // As boxed objects are JSObjectRef not derived from NSObject, we box them in an ObjC object. + // + + // Box the ObjC object in a JSObjectRef +// JSObjectRef jsObject = [JSCocoa jsCocoaPrivateObjectInContext:ctx]; + JSObjectRef jsObject = [self newPrivateObject]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(jsObject); + private.type = @"@"; + [private setObject:o]; + + // Box the JSObjectRef in our ObjC object + value = [[BoxedJSObject alloc] init]; + [value setJSObject:jsObject]; + + // Add to dictionary and make it sole owner + [boxedObjects setValue:value forKey:key]; + [value release]; + return jsObject; +} + +- (BOOL)isObjectBoxed:(id)o { + id key = [NSString stringWithFormat:@"%p", o]; + return !![boxedObjects valueForKey:key]; +} + +- (void)deleteBoxOfObject:(id)o { + id key = [NSString stringWithFormat:@"%p", o]; + id value= [boxedObjects valueForKey:key]; + if (!value) + return; + [boxedObjects removeObjectForKey:key]; +} + +/* ++ (void)downBoxedJSObjectCount:(id)o +{ + id key = [NSString stringWithFormat:@"%p", o]; + id value = [boxedObjects valueForKey:key]; + if (!value) + return; + + [boxedObjects removeObjectForKey:key]; +} + ++ (id)boxedObjects +{ + return boxedObjects; +} +*/ +#pragma mark Helpers +- (id)selectorForJSFunction:(JSObjectRef)function +{ + return [jsFunctionSelectors valueForKey:[NSString stringWithFormat:@"%p", function]]; +} + +- (id)classForJSFunction:(JSObjectRef)function +{ + return [jsFunctionClasses valueForKey:[NSString stringWithFormat:@"%p", function]]; +} + +// +// Given an exception, get its line number, source URL, error message and return them in a NSString +// When throwing an exception from Javascript, throw an object instead of a string. +// This way, JavascriptCore will add line and sourceURL. +// (throw new String('error') instead of throw 'error') +// ++ (NSString*)formatJSException:(JSValueRef)exception inContext:(JSContextRef)context +{ + if (!exception) + return @"formatJSException:(null)"; + // Convert exception to string + JSStringRef resultStringJS = JSValueToStringCopy(context, exception, NULL); + NSString* b = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, resultStringJS); + JSStringRelease(resultStringJS); + [NSMakeCollectable(b) autorelease]; + + // Only objects contain line and source URL + if (JSValueGetType(context, exception) != kJSTypeObject) return b; + + // Iterate over all properties of the exception + JSObjectRef jsObject = JSValueToObject(context, exception, NULL); + JSPropertyNameArrayRef jsNames = JSObjectCopyPropertyNames(context, jsObject); + size_t i, nameCount = JSPropertyNameArrayGetCount(jsNames); + id line = nil, sourceURL = nil; + for (i=0; i>>evaling %@", filePath); + + id evaled = nil; + @try { + evaled = [self performSelector:sel withObject:filePath]; +// NSLog(@">>>EVALED %d, %@", evaled, filePath); + } @catch (id e) { + NSLog(@"(Test exception from %@) %@", file, e); + evaled = nil; + } + if (!evaled) { + id error = [NSString stringWithFormat:@"test %@ failed (Ran %d out of %d tests)", file, count+1, [files count]]; + [JSCocoaController log:error]; + return NO; + } + count ++; + [self garbageCollect]; + } +#endif + return count; +} +- (int)runTests:(NSString*)path { + return [self runTests:path withSelector:@selector(evalJSFile:)]; +} + +#pragma mark Autorelease pool +static id autoreleasePool; ++ (void)allocAutoreleasePool { + autoreleasePool = [[NSAutoreleasePool alloc] init]; +} + ++ (void)deallocAutoreleasePool { + [autoreleasePool release]; +} + + +#pragma mark Garbage Collection +// +// Collect on top of the run loop, not in some JS function +// ++ (void)garbageCollect { + NSLog(@"*** Deprecated — call garbageCollect on an instance ***"); /*JSGarbageCollect(NULL);*/ +} +- (void)garbageCollect { + JSGarbageCollect(ctx); +} + +// +// Make all root Javascript variables point to null +// +- (void)unlinkAllReferences +{ + // Null and delete every reference to every live object +// [self evalJSString:@"for (var i in this) { log('DELETE ' + i); this[i] = null; delete this[i]; }"]; +// [self evalJSString:@"for (var i in this) { this[i] = null; delete this[i]; }"]; + +// id del = @"var keys = Object.keys(this); var c = keys.length; for (var i=0; i", + [self class], + self, + ((id)self == (id)[self class]) ? @"Class" : @"", + [boxedObject class], + boxedObject, + retainCount]; +*/ + + } +// NSLog(@"%@", boxedObjects); +} + +#pragma mark Class inspection ++ (id)rootclasses +{ + return [JSCocoaLib rootclasses]; +} ++ (id)classes +{ + return [JSCocoaLib classes]; +} ++ (id)protocols +{ + return [JSCocoaLib protocols]; +} ++ (id)imageNames +{ + return [JSCocoaLib imageNames]; +} ++ (id)methods +{ + return [JSCocoaLib methods]; +} ++ (id)runtimeReport +{ + return [JSCocoaLib runtimeReport]; +} ++ (id)explainMethodEncoding:(id)encoding +{ + id argumentEncodings = [JSCocoaController parseObjCMethodEncoding:[encoding UTF8String]]; + id explication = [NSMutableArray array]; + for (id arg in argumentEncodings) + [explication addObject:[arg typeDescription] + ]; + + return explication; +} + + + + +// JSCocoa : handle setting with callMethod +// object.width = 100 +// -> +// [object setWidth:100] +// +- (BOOL)JSCocoa:(JSCocoaController*)controller setProperty:(NSString*)propertyName ofObject:(id)object toValue:(JSValueRef)value inContext:(JSContextRef)localCtx exception:(JSValueRef*)exception +{ + // FIXME: this doesn't actually work with objc properties, and we can't always rely that this method will exist either... + // it should probably be moved up into the JSCocoa layer. + + NSString* setterName = [NSString stringWithFormat:@"set%@%@:", + [[propertyName substringWithRange:NSMakeRange(0,1)] capitalizedString], + [propertyName substringWithRange:NSMakeRange(1, [propertyName length]-1)]]; + + if ([self JSCocoa:controller callMethod:setterName ofObject:object privateObject:nil argumentCount:1 arguments:&value inContext:localCtx exception:exception]) { + return YES; + } + + return NO; +} +#pragma mark Distant Object Handling (DO) +// +// NSDistantObject call using NSInvocation +// +- (JSValueRef)JSCocoa:(JSCocoaController*)controller callMethod:(NSString*)methodName ofObject:(id)callee privateObject:(JSCocoaPrivateObject*)thisPrivateObject argumentCount:(size_t)argumentCount arguments:(JSValueRef*)arguments inContext:(JSContextRef)localCtx exception:(JSValueRef*)exception +{ + SEL selector = NSSelectorFromString(methodName); + if (class_getInstanceMethod([callee class], selector) || class_getClassMethod([callee class], selector)) { + return nil; + } + + NSMethodSignature *signature = [callee methodSignatureForSelector:selector]; + + if (!signature) { + return nil; + } + + // we need to do all this for NSDistantObject , since JSCocoa doesn't handle it natively. + + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:selector]; + NSUInteger argIndex = 0; + while (argIndex < argumentCount) { + + id arg = 0x00; + + [JSCocoaFFIArgument unboxJSValueRef:arguments[argIndex] toObject:&arg inContext:localCtx]; + + const char *type = [signature getArgumentTypeAtIndex:argIndex + 2]; + // Structure argument + if (type && type[0] == '{') + { + id structureType = [NSString stringWithUTF8String:type]; + id fullStructureType = [JSCocoaFFIArgument structureFullTypeEncodingFromStructureTypeEncoding:structureType]; + + int size = [JSCocoaFFIArgument sizeOfStructure:structureType]; + JSObjectRef jsObject = JSValueToObject(ctx, arguments[argIndex], NULL); + if (size && fullStructureType && jsObject) + { + // Alloc structure size and let NSData deallocate it + void* source = malloc(size); + memset(source, 0, size); + [NSData dataWithBytesNoCopy:source length:size freeWhenDone:YES]; + + void* p = source; + NSInteger numParsed = [JSCocoaFFIArgument structureFromJSObjectRef:jsObject inContext:ctx inParentJSValueRef:NULL fromCString:(char*)[fullStructureType UTF8String] fromStorage:&p]; + if (numParsed) [invocation setArgument:source atIndex:argIndex+2]; + } + } + else + if ([arg isKindOfClass:[NSNumber class]]) { + +// const char *type = [signature getArgumentTypeAtIndex:argIndex + 2]; + if (strcmp(type, @encode(BOOL)) == 0) { + BOOL b = [arg boolValue]; + [invocation setArgument:&b atIndex:argIndex + 2]; + } + else if (strcmp(type, @encode(unsigned int)) == 0) { + unsigned int i = [arg unsignedIntValue]; + [invocation setArgument:&i atIndex:argIndex + 2]; + } + else if (strcmp(type, @encode(int)) == 0) { + int i = [arg intValue]; + [invocation setArgument:&i atIndex:argIndex + 2]; + } + else if (strcmp(type, @encode(unsigned long)) == 0) { + unsigned long l = [arg unsignedLongValue]; + [invocation setArgument:&l atIndex:argIndex + 2]; + } + else if (strcmp(type, @encode(long)) == 0) { + long l = [arg longValue]; + [invocation setArgument:&l atIndex:argIndex + 2]; + } + else if (strcmp(type, @encode(float)) == 0) { + float f = [arg floatValue]; + [invocation setArgument:&f atIndex:argIndex + 2]; + } + else if (strcmp(type, @encode(double)) == 0) { + double d = [arg doubleValue]; + [invocation setArgument:&d atIndex:argIndex + 2]; + } + else { // just do int for all else. + int i = [arg intValue]; + [invocation setArgument:&i atIndex:argIndex + 2]; + } + + } + else { + [invocation setArgument:&arg atIndex:argIndex + 2]; + } + + argIndex++; + } + + @try { + [invocation invokeWithTarget:callee]; + } + @catch (NSException * e) { + NSLog(@"Exception while calling %@. %@", methodName, [e reason]); + + if ([[e reason] isEqualToString:@"connection went invalid while waiting for a reply"]) { + // whoops? + // also, how do we not look for some funky localized string here? + // also also, can we now make whatever is pointing to this value, nil? + + if (thisPrivateObject) { + NSLog(@"Connection terminated, removing reference to object"); + thisPrivateObject.object = [NSNull null]; + [thisPrivateObject setJSValueRef:JSValueMakeNull(localCtx) ctx:localCtx]; + } + } + } + + JSValueRef jsReturnValue = NULL; + const char *type = [signature methodReturnType]; + if (strcmp(type, @encode(id)) == 0 || strcmp(type, @encode(Class)) == 0) { + id result = 0x00; + [invocation getReturnValue:&result]; + if (!result) return JSValueMakeNull(localCtx); + [JSCocoaFFIArgument boxObject:result toJSValueRef:&jsReturnValue inContext:localCtx]; + } +/* + case _C_CHR: + case _C_UCHR: + case _C_SHT: + case _C_USHT: + case _C_INT: + case _C_UINT: + case _C_LNG: + case _C_ULNG: + case _C_LNG_LNG: + case _C_ULNG_LNG: + case _C_FLT: + case _C_DBL: +*/ + else if (strcmp(type, @encode(char)) == 0) { + char result; + [invocation getReturnValue:&result]; + if (!result) return JSValueMakeNull(localCtx); + [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(char)[0] fullTypeEncoding:NULL fromStorage:&result]; + } + else if (strcmp(type, @encode(unsigned char)) == 0) { + unsigned char result; + [invocation getReturnValue:&result]; + if (!result) return JSValueMakeNull(localCtx); + [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(unsigned char)[0] fullTypeEncoding:NULL fromStorage:&result]; + } + else if (strcmp(type, @encode(short)) == 0) { + short result; + [invocation getReturnValue:&result]; + if (!result) return JSValueMakeNull(localCtx); + [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(short)[0] fullTypeEncoding:NULL fromStorage:&result]; + } + else if (strcmp(type, @encode(unsigned short)) == 0) { + unsigned short result; + [invocation getReturnValue:&result]; + if (!result) return JSValueMakeNull(localCtx); + [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(unsigned short)[0] fullTypeEncoding:NULL fromStorage:&result]; + } + else if (strcmp(type, @encode(int)) == 0) { + int result; + [invocation getReturnValue:&result]; + if (!result) return JSValueMakeNull(localCtx); + [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(int)[0] fullTypeEncoding:NULL fromStorage:&result]; + } + else if (strcmp(type, @encode(unsigned int)) == 0) { + unsigned int result; + [invocation getReturnValue:&result]; + if (!result) return JSValueMakeNull(localCtx); + [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(unsigned int)[0] fullTypeEncoding:NULL fromStorage:&result]; + } + else if (strcmp(type, @encode(long)) == 0) { + long result; + [invocation getReturnValue:&result]; + if (!result) return JSValueMakeNull(localCtx); + [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(long)[0] fullTypeEncoding:NULL fromStorage:&result]; + } + else if (strcmp(type, @encode(unsigned long)) == 0) { + unsigned long result; + [invocation getReturnValue:&result]; + if (!result) return JSValueMakeNull(localCtx); + [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(unsigned long)[0] fullTypeEncoding:NULL fromStorage:&result]; + } + else if (strcmp(type, @encode(float)) == 0) { + float result; + [invocation getReturnValue:&result]; + if (!result) return JSValueMakeNull(localCtx); + [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(float)[0] fullTypeEncoding:NULL fromStorage:&result]; + } + else if (strcmp(type, @encode(double)) == 0) { + double result; + [invocation getReturnValue:&result]; + if (!result) return JSValueMakeNull(localCtx); + [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(double)[0] fullTypeEncoding:NULL fromStorage:&result]; + } + // Structure return + else if (type && type[0] == '{') + { + id structureType = [NSString stringWithUTF8String:type]; + id fullStructureType = [JSCocoaFFIArgument structureFullTypeEncodingFromStructureTypeEncoding:structureType]; + + int size = [JSCocoaFFIArgument sizeOfStructure:structureType]; + if (size) + { + void* result = malloc(size); + [invocation getReturnValue:result]; + + // structureToJSValueRef will advance the pointer in place, overwriting its original value + void* ptr = result; + NSInteger numParsed = [JSCocoaFFIArgument structureToJSValueRef:&jsReturnValue inContext:localCtx fromCString:(char*)[fullStructureType UTF8String] fromStorage:&ptr]; + if (!numParsed) jsReturnValue = NULL; + free(result); + } + } + if (!jsReturnValue) return JSValueMakeNull(localCtx); + return jsReturnValue; +} + +@end + + + + + + + +#pragma mark Javascript setter functions +// Give ObjC classes written in Javascript extra abilities like storing extra javascript variables in an internal __jsHash. +// The following methods handle that. JSCocoaMethodHolder is a dummy class to hold them. +@interface JSCocoaMethodHolder : NSObject +@end +@implementation JSCocoaMethodHolder +- (BOOL)setJSValue:(JSValueRefAndContextRef)valueAndContext forJSName:(JSValueRefAndContextRef)nameAndContext +{ + if (class_getInstanceVariable([self class], "__jsHash")) + { + JSContextRef c = valueAndContext.ctx; + JSStringRef name = JSValueToStringCopy(c, nameAndContext.value, NULL); + + JSObjectRef hash = NULL; + object_getInstanceVariable(self, "__jsHash", (void**)&hash); + if (!hash) + { + // Retrieve controller + id jsc = [JSCocoaController controllerFromContext:c]; + c = [jsc ctx]; + + hash = JSObjectMake(c, hashObjectClass, NULL); + // Same as copyWithZone: + object_setInstanceVariable(self, "__jsHash", (void*)hash); + object_setInstanceVariable(self, "__jsCocoaController", (void*)jsc); + JSValueProtect(c, hash); + [JSCocoaController upJSValueProtectCount]; + [JSCocoaController upJSCocoaHashCount]; + } + + JSObjectSetProperty(c, hash, name, valueAndContext.value, kJSPropertyAttributeNone, NULL); + JSStringRelease(name); + return YES; + } + return NO; +} +- (JSValueRefAndContextRef)JSValueForJSName:(JSValueRefAndContextRef)nameAndContext +{ + JSValueRefAndContextRef valueAndContext = { JSValueMakeNull(nameAndContext.ctx), NULL }; + if (class_getInstanceVariable([self class], "__jsHash")) + { + JSContextRef c = nameAndContext.ctx; + JSStringRef name = JSValueToStringCopy(c, nameAndContext.value, NULL); + + JSObjectRef hash = NULL; + object_getInstanceVariable(self, "__jsHash", (void**)&hash); + if (!hash || !JSObjectHasProperty(c, hash, name)) + { + JSStringRelease(name); + return valueAndContext; + } + valueAndContext.ctx = c; + valueAndContext.value = JSObjectGetProperty(c, hash, name, NULL); + + JSStringRelease(name); + return valueAndContext; + } + return valueAndContext; +} + +- (BOOL)deleteJSValueForJSName:(JSValueRefAndContextRef)nameAndContext +{ + if (class_getInstanceVariable([self class], "__jsHash")) + { + JSContextRef c = nameAndContext.ctx; + JSStringRef name = JSValueToStringCopy(c, nameAndContext.value, NULL); + + JSObjectRef hash = NULL; + object_getInstanceVariable(self, "__jsHash", (void**)&hash); + if (!hash || !JSObjectHasProperty(c, hash, name)) + { + JSStringRelease(name); + return NO; + } + bool r = JSObjectDeleteProperty(c, hash, name, NULL); + JSStringRelease(name); + return r; + } + return NO; +} + + +// Instance count debug ++ (id)allocWithZone:(NSZone*)zone +{ + // Dynamic super call + id parentClass = [JSCocoaController parentObjCClassOfClassName:[NSString stringWithUTF8String:class_getName(self)]]; + id supermetaclass = objc_getMetaClass(class_getName(parentClass)); + struct objc_super superData = { self, supermetaclass }; + id o = objc_msgSendSuper(&superData, @selector(allocWithZone:), zone); + + [JSCocoaController upInstanceCount:o]; + return o; +} + +// Called by -(id)copy +- (id)copyWithZone:(NSZone *)zone +{ + // Dynamic super call + id parentClass = [JSCocoaController parentObjCClassOfClassName:[NSString stringWithUTF8String:class_getName([self class])]]; + struct objc_super superData = { self, parentClass }; + id o = objc_msgSendSuper(&superData, @selector(copyWithZone:), zone); + + // + // Copy hash by making a new copy + // + + // Return if var has no controller + id jsc = nil; + object_getInstanceVariable(self, "__jsCocoaController", (void**)&jsc); + if (!jsc) return o; + + + JSContextRef ctx = [jsc ctx]; + + + JSObjectRef hash1 = NULL; + JSObjectRef hash2 = NULL; + object_getInstanceVariable(self, "__jsHash", (void**)&hash1); + object_getInstanceVariable(o, "__jsHash", (void**)&hash2); + + // Return if hash does not exist + if (!hash1) return o; + + + // Copy hash + JSStringRef scriptJS = JSStringCreateWithUTF8CString("var hash1 = arguments[0]; var hash2 = {}; for (var i in hash1) hash2[i] = hash1[i]; return hash2"); + JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); + JSValueRef result = JSObjectCallAsFunction(ctx, fn, NULL, 1, (JSValueRef*)&hash1, NULL); + JSStringRelease(scriptJS); + + // Convert hash to object + JSObjectRef hashCopy = JSValueToObject(ctx, result, NULL); + object_getInstanceVariable(o, "__jsHash", (void**)&hash2); + + // Same as setJSValue:forJSName: + // Set new hash + object_setInstanceVariable(o, "__jsHash", (void*)hashCopy); + object_setInstanceVariable(o, "__jsCocoaController", (void*)jsc); + JSValueProtect(ctx, hashCopy); + [JSCocoaController upJSValueProtectCount]; + [JSCocoaController upJSCocoaHashCount]; + + [JSCocoaController upInstanceCount:o]; + return o; +} + + +// Dealloc : unprotect js hash +- (void)deallocAndCleanupJS +{ + JSObjectRef hash = NULL; + object_getInstanceVariable(self, "__jsHash", (void**)&hash); + if (hash) + { + id jsc = NULL; + object_getInstanceVariable(self, "__jsCocoaController", (void**)&jsc); + JSValueUnprotect([jsc ctx], hash); + [JSCocoaController downJSCocoaHashCount]; + } + [JSCocoaController downInstanceCount:self]; + + // Dynamic super call + id parentClass = [JSCocoaController parentObjCClassOfClassName:[NSString stringWithUTF8String:class_getName([self class])]]; + struct objc_super superData = { self, parentClass }; + objc_msgSendSuper(&superData, @selector(dealloc)); +} + +// Finalize - same as dealloc +static BOOL __warningSuppressorAsFinalizeIsCalledBy_objc_msgSendSuper = NO; +- (void)finalize +{ + JSObjectRef hash = NULL; + object_getInstanceVariable(self, "__jsHash", (void**)&hash); + if (hash) + { + id jsc = NULL; + object_getInstanceVariable(self, "__jsCocoaController", (void**)&jsc); + JSValueUnprotect([jsc ctx], hash); + [JSCocoaController downJSCocoaHashCount]; + } + [JSCocoaController downInstanceCount:self]; + + // Dynamic super call + id parentClass = [JSCocoaController parentObjCClassOfClassName:[NSString stringWithUTF8String:class_getName([self class])]]; + struct objc_super superData = { self, parentClass }; + objc_msgSendSuper(&superData, @selector(finalize)); + + // Ignore warning about missing [super finalize] as the call IS made via objc_msgSendSuper + if (__warningSuppressorAsFinalizeIsCalledBy_objc_msgSendSuper) [super finalize]; +} + + + +@end + + + + + + +#pragma mark Common instance method +// Class.instance == class.alloc.init + release (jsObject retains object) +// Class.instance( { withA : ... andB : ... } ) == class.alloc.initWithA:... andB:... + release +@implementation NSObject(CommonInstance) ++ (JSValueRef)instanceWithContext:(JSContextRef)ctx argumentCount:(size_t)argumentCount arguments:(JSValueRef*)arguments exception:(JSValueRef*)exception +{ + id methodName = @"init"; + JSValueRef* argumentsToFree = NULL; + // Recover init method + if (argumentCount == 1) + { + id splitMethodName = @"init"; + BOOL isSplitCall = [JSCocoaController trySplitCall:&splitMethodName class:self argumentCount:&argumentCount arguments:&arguments ctx:ctx]; + if (isSplitCall) + { + methodName = splitMethodName; + argumentsToFree = arguments; + } + else return throwException(ctx, exception, @"Instance split call did not find an init method"), NULL; + } +// NSLog(@"=>Called instance on %@ with init=%@", self, methodName); + + // Allocate new instance + id newInstance = [self alloc]; + + // Set it as new object +// JSObjectRef thisObject = [JSCocoaController jsCocoaPrivateObjectInContext:ctx]; + id jsc = [JSCocoa controllerFromContext:ctx]; + JSObjectRef thisObject = [jsc newPrivateObject]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(thisObject); + private.type = @"@"; + [private setObjectNoRetain:newInstance]; + // No — will retain allocated object and trigger "did you forget to call init" warning + // Object will be automatically boxed when returned to Javascript by +// JSObjectRef thisObject = [JSCocoaController boxedJSObject:newInstance inContext:ctx]; + + // Create function object boxing our init method +// JSObjectRef function = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; + JSObjectRef function = [jsc newPrivateFunction]; + private = JSObjectGetPrivate(function); + private.type = @"method"; + private.methodName = methodName; + + // Call callAsFunction on our new instance with our init method + JSValueRef exceptionFromInitCall = NULL; + JSValueRef returnValue = jsCocoaObject_callAsFunction(ctx, function, thisObject, argumentCount, arguments, &exceptionFromInitCall); + free(argumentsToFree); + if (exceptionFromInitCall) return *exception = exceptionFromInitCall, NULL; + + // Release object + JSObjectRef returnObject = JSValueToObject(ctx, returnValue, NULL); + // We can get nil when initWith... fails. (eg var image = NSImage.instance({withContentsOfFile:'DOESNOTEXIST'}) + // Return nil then. + if (returnObject == nil) return JSValueMakeNull(ctx); + private = JSObjectGetPrivate(returnObject); + id boxedObject = [private object]; + [boxedObject release]; + + // Register our context in there so that safeDealloc finds it. + if ([boxedObject respondsToSelector:@selector(safeDealloc)]) + { +// id jsc = [JSCocoaController controllerFromContext:ctx]; +// object_setInstanceVariable(boxedObject, "__jsCocoaController", (void*)jsc); + } + return returnValue; +} + + +@end + + + + + + +#pragma mark - +#pragma mark JavascriptCore callbacks +#pragma mark - +#pragma mark JavascriptCore OSX object + +// +// +// Global resolver : main class used as 'this' in Javascript's global scope. Name requests go through here. +// +// +JSValueRef OSXObject_getProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyNameJS, JSValueRef* exception) +{ + NSString* propertyName = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, propertyNameJS); + [NSMakeCollectable(propertyName) autorelease]; + + if ([propertyName isEqualToString:@"__jsc__"]) return NULL; + +// NSLog(@"Asking for global property %@", propertyName); + JSCocoaController* jsc = [JSCocoaController controllerFromContext:ctx]; + id delegate = jsc.delegate; + // + // Delegate canGetGlobalProperty, getGlobalProperty + // + if (delegate) + { + // Check if getting is allowed + if ([delegate respondsToSelector:@selector(JSCocoa:canGetGlobalProperty:inContext:exception:)]) + { + BOOL canGetGlobal = [delegate JSCocoa:jsc canGetGlobalProperty:propertyName inContext:ctx exception:exception]; + if (!canGetGlobal) + { + if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow getting global property %@", propertyName]); + return NULL; + } + } + // Check if delegate handles getting + if ([delegate respondsToSelector:@selector(JSCocoa:getGlobalProperty:inContext:exception:)]) + { + JSValueRef delegateGetGlobal = [delegate JSCocoa:jsc getGlobalProperty:propertyName inContext:ctx exception:exception]; + if (delegateGetGlobal) return delegateGetGlobal; + } + } + + // + // ObjC class + // + Class objCClass = NSClassFromString(propertyName); + if (objCClass && ![propertyName isEqualToString:@"Object"]) + { + JSValueRef ret = [jsc boxObject:objCClass]; + return ret; + } + + id xml; + id type = nil; + // + // Query BridgeSupport for property + // + xml = [[BridgeSupportController sharedController] queryName:propertyName]; + if (xml) + { + id error = nil; + id xmlDocument = [[NSXMLDocument alloc] initWithXMLString:xml options:0 error:&error]; + if (error) return NSLog(@"(OSX_getPropertyCallback) malformed xml while getting property %@ of type %@ : %@", propertyName, type, error), NULL; + [xmlDocument autorelease]; + + type = [[xmlDocument rootElement] name]; + + // + // Function + // + if ([type isEqualToString:@"function"]) + { +// JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; + JSObjectRef o = [jsc newPrivateFunction]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(o); + private.type = @"function"; + private.xml = xml; + return o; + } + + // + // Struct + // + else + if ([type isEqualToString:@"struct"]) + { +// JSObjectRef o = [JSCocoaController jsCocoaPrivateObjectInContext:ctx]; + JSObjectRef o = [jsc newPrivateObject]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(o); + private.type = @"struct"; + private.xml = xml; + return o; + } + + // + // Constant + // + else + if ([type isEqualToString:@"constant"]) + { + // ##fix : NSZeroPoint, NSZeroRect, NSZeroSize would need special (struct) + type64 handling + // Check if constant's declared_type is NSString* + id declared_type = [[xmlDocument rootElement] attributeForName:@"declared_type"]; + if (!declared_type) declared_type = [[xmlDocument rootElement] attributeForName:@"type"]; + if (!declared_type || !([[declared_type stringValue] isEqualToString:@"NSString*"] + || [[declared_type stringValue] isEqualToString:@"@"] + || [[declared_type stringValue] isEqualToString:@"^{__CFString=}"] + )) + return NSLog(@"(OSX_getPropertyCallback) %@ not a NSString* constant : %@", propertyName, xml), NULL; + + // Grab symbol + void* symbol = dlsym(RTLD_DEFAULT, [propertyName UTF8String]); + if (!symbol) return NSLog(@"(OSX_getPropertyCallback) symbol %@ not found", propertyName), NULL; + + // ObjC objects, like NSApp : pointer to NSApplication.sharedApplication + if ([[declared_type stringValue] isEqualToString:@"@"]) + { + id o = *(id*)symbol; + return [jsc boxObject:o]; + } + + // Return symbol as a Javascript string + NSString* str = *(NSString**)symbol; + JSStringRef jsName = JSStringCreateWithUTF8CString([str UTF8String]); + JSValueRef jsString = JSValueMakeString(ctx, jsName); + JSStringRelease(jsName); + return jsString; + } + + // + // Enum + // + else + if ([type isEqualToString:@"enum"]) + { + // Check if constant's declared_type is NSString* + id value = [[xmlDocument rootElement] attributeForName:@"value"]; + if (!value) + { + value = [[xmlDocument rootElement] attributeForName:@"value64"]; + if (!value) + return NSLog(@"(OSX_getPropertyCallback) %@ enum has no value set", propertyName), NULL; + } + + // Try parsing value + double doubleValue = 0; + value = [value stringValue]; + if (![[NSScanner scannerWithString:value] scanDouble:&doubleValue]) return NSLog(@"(OSX_getPropertyCallback) scanning %@ enum failed", propertyName), NULL; + return JSValueMakeNumber(ctx, doubleValue); + } + } + + // Describe ourselves + if ([propertyName isEqualToString:@"toString"] || [propertyName isEqualToString:@"valueOf"]) + { + JSStringRef scriptJS = JSStringCreateWithUTF8CString("return '(JSCocoa global object)'"); + JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); + JSStringRelease(scriptJS); + return fn; + } + + return NULL; +} + + +static void OSXObject_getPropertyNames(JSContextRef ctx, JSObjectRef object, JSPropertyNameAccumulatorRef propertyNames) +{ + // Move to a definition object +/* + NSArray* keys = [[BridgeSupportController sharedController] keys]; + for (id key in keys) + { + JSStringRef jsString = JSStringCreateWithUTF8CString([key UTF8String]); + JSPropertyNameAccumulatorAddName(propertyNames, jsString); + JSStringRelease(jsString); + } +*/ +} + + + + + + +#pragma mark JavascriptCore JSCocoa object + +// +// Below lie the Javascript callbacks for all Javascript objects created by JSCocoa, used to pass ObjC data to and fro Javascript. +// + + +// +// From PyObjC : when to call objc_msgSend_stret, for structure return +// Depending on structure size & architecture, structures are returned as function first argument (done transparently by ffi) or via registers +// +BOOL isUsingStret(id argumentEncodings) +{ + int resultSize = 0; + char returnEncoding = [[argumentEncodings objectAtIndex:0] typeEncoding]; + if (returnEncoding == _C_STRUCT_B) resultSize = [JSCocoaFFIArgument sizeOfStructure:[[argumentEncodings objectAtIndex:0] structureTypeEncoding]]; + if (returnEncoding == _C_STRUCT_B && + //#ifdef __ppc64__ + // ffi64_stret_needs_ptr(signature_to_ffi_return_type(rettype), NULL, NULL) + // + //#else /* !__ppc64__ */ + (resultSize > SMALL_STRUCT_LIMIT + #ifdef __i386__ + /* darwin/x86 ABI is slightly odd ;-) */ + || (resultSize != 1 + && resultSize != 2 + && resultSize != 4 + && resultSize != 8) + #endif + #ifdef __x86_64__ + /* darwin/x86-64 ABI is slightly odd ;-) */ + || (resultSize != 1 + && resultSize != 2 + && resultSize != 4 + && resultSize != 8 + && resultSize != 16 + ) + #endif + ) + //#endif /* !__ppc64__ */ + ) { +// callAddress = objc_msgSend_stret; +// usingStret = YES; + return YES; + } + return NO; +} + +// +// Return the correct objc_msgSend* variety according to encodings +// +void* getObjCCallAddress(id argumentEncodings) +{ + BOOL usingStret = isUsingStret(argumentEncodings); + void* callAddress = objc_msgSend; + if (usingStret) callAddress = objc_msgSend_stret; + + +#if __i386__ // || TARGET_OS_IPHONE no, iPhone uses objc_msgSend + char returnEncoding = [[argumentEncodings objectAtIndex:0] typeEncoding]; + if (returnEncoding == 'f' || returnEncoding == 'd') + { + callAddress = objc_msgSend_fpret; + } +#endif + + return callAddress; +} + +// +// Convert FROM a webView context to a local context (called by valueOf(), toString()) +// +JSValueRef valueFromExternalContext(JSContextRef externalCtx, JSValueRef value, JSContextRef ctx) +{ + int type = JSValueGetType(externalCtx, value); + switch (type) + { + case kJSTypeUndefined: + { + return JSValueMakeUndefined(ctx); + } + + case kJSTypeNull: + { + return JSValueMakeNull(ctx); + } + + case kJSTypeBoolean: + { + bool b = JSValueToBoolean(externalCtx, value); + return JSValueMakeBoolean(ctx, b); + } + + case kJSTypeNumber: + { + double d = JSValueToNumber(externalCtx, value, NULL); + return JSValueMakeNumber(ctx, d); + } + + // Make strings and objects show up only as strings + case kJSTypeString: + case kJSTypeObject: + { + // Add an (externalContext) suffix to distinguish boxed JSValues from a WebView + JSStringRef jsString = JSValueToStringCopy(externalCtx, value, NULL); + + NSString* string = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, jsString); + NSString* idString; + + // Mark only objects as (externalContext), not raw strings + if (type == kJSTypeObject) idString = [NSString stringWithFormat:@"%@ (externalContext)", string]; + else idString = [NSString stringWithFormat:@"%@", string]; + [string release]; + JSStringRelease(jsString); + + jsString = JSStringCreateWithUTF8CString([idString UTF8String]); + JSValueRef returnValue = JSValueMakeString(ctx, jsString); + JSStringRelease(jsString); + + return returnValue; + } + } + return JSValueMakeNull(ctx); +} + +// +// Convert TO a webView context from a local context +// +JSValueRef valueToExternalContext(JSContextRef ctx, JSValueRef value, JSContextRef externalCtx) +{ + int type = JSValueGetType(ctx, value); + switch (type) + { + case kJSTypeUndefined: + { + return JSValueMakeUndefined(externalCtx); + } + + case kJSTypeNull: + { + return JSValueMakeNull(externalCtx); + } + + case kJSTypeBoolean: + { + bool b = JSValueToBoolean(ctx, value); + return JSValueMakeBoolean(externalCtx, b); + } + + case kJSTypeNumber: + { + double d = JSValueToNumber(ctx, value, NULL); + return JSValueMakeNumber(externalCtx, d); + } + + case kJSTypeString: + { + JSStringRef jsString = JSValueToStringCopy(ctx, value, NULL); + JSValueRef returnValue = JSValueMakeString(externalCtx, jsString); + JSStringRelease(jsString); + return returnValue; + } + case kJSTypeObject: + { + JSObjectRef o = JSValueToObject(ctx, value, NULL); + if (!o) return JSValueMakeNull(externalCtx); + JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(o); + if (![privateObject.type isEqualToString:@"externalJSValueRef"]) + { + id object = [privateObject object]; + if ([object isKindOfClass:[NSString class]]) + { + JSStringRef jsName = JSStringCreateWithUTF8CString([object UTF8String]); + JSValueRef jsString = JSValueMakeString(externalCtx, jsName); + JSStringRelease(jsName); + return jsString; + } + if ([object isKindOfClass:[NSNumber class]]) + { + return JSValueMakeNumber(externalCtx, [object doubleValue]); + } +// NSLog(@"Object (%@) converted to undefined", o ); + return JSValueMakeUndefined(externalCtx); + } + return [privateObject jsValueRef]; + } + } + return JSValueMakeNull(externalCtx); +} + +JSValueRef boxedValueFromExternalContext(JSContextRef externalCtx, JSValueRef value, JSContextRef ctx) +{ + if (JSValueGetType(externalCtx, value) < kJSTypeObject) return valueFromExternalContext(externalCtx, value, ctx); + + // If value is function ... + JSStringRef scriptJS= JSStringCreateWithUTF8CString("return (typeof arguments[0]) == 'function' ? true : null"); + JSObjectRef fn = JSObjectMakeFunction(externalCtx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); + JSValueRef result = JSObjectCallAsFunction(externalCtx, fn, NULL, 1, (JSValueRef*)&value, NULL); + JSStringRelease(scriptJS); + + // ... use the function boxer + JSObjectRef o; + if (JSValueIsBoolean(externalCtx, result)) +// o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; + o = [[JSCocoa controllerFromContext:ctx] newPrivateFunction]; + else +// o = [JSCocoaController jsCocoaPrivateObjectInContext:ctx]; + o = [[JSCocoa controllerFromContext:ctx] newPrivateFunction]; + + JSCocoaPrivateObject* private = JSObjectGetPrivate(o); + private.type = @"externalJSValueRef"; + [private setExternalJSValueRef:value ctx:externalCtx]; + return o; +} + + +// +// valueOf : from a boxed ObjC object, returns a primitive javascript value (number or string) +// that JavascriptCore can use in expressions (eg boxedObject + 'this', boxedObject < 4) +// +// The returned value is temporary and does not affect the boxed object. +// +JSValueRef valueOfCallback(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception) +{ + // Holding a native JS value ? Return it + JSCocoaPrivateObject* thisPrivateObject = JSObjectGetPrivate(thisObject); + if ([thisPrivateObject.type isEqualToString:@"jsValueRef"]) + { + return [thisPrivateObject jsValueRef]; + } + + // External jsValueRef from WebView + if ([thisPrivateObject.type isEqualToString:@"externalJSValueRef"]) + { + JSContextRef externalCtx = [thisPrivateObject ctx]; + JSValueRef externalJSValueRef = [thisPrivateObject jsValueRef]; + JSStringRef scriptJS= JSStringCreateWithUTF8CString("return arguments[0].valueOf()"); + JSObjectRef fn = JSObjectMakeFunction(externalCtx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); + JSValueRef result = JSObjectCallAsFunction(externalCtx, fn, NULL, 1, (JSValueRef*)&externalJSValueRef, NULL); + JSStringRelease(scriptJS); + + return valueFromExternalContext(externalCtx, result, ctx); + } + + // NSNumber special case + if ([thisPrivateObject.object isKindOfClass:[NSNumber class]]) + return JSValueMakeNumber(ctx, [thisPrivateObject.object doubleValue]); + + // Convert to string + id toString = [thisPrivateObject description]; + + // Object + if ([thisPrivateObject.type isEqualToString:@"@"]) + { + // Holding an out value ? + if ([thisPrivateObject.object isKindOfClass:[JSCocoaOutArgument class]]) + { + JSValueRef outValue = [(JSCocoaOutArgument*)thisPrivateObject.object outJSValueRefInContext:ctx]; + if (!outValue) + { + JSStringRef jsName = JSStringCreateWithUTF8CString("Unitialized outArgument"); + JSValueRef r = JSValueMakeString(ctx, jsName); + JSStringRelease(jsName); + return r; + } + // Holding an object ? Call valueOf on it + if (JSValueGetType(ctx, outValue) == kJSTypeObject) + return valueOfCallback(ctx, NULL, JSValueToObject(ctx, outValue, NULL), 0, NULL, NULL); + // Return raw JSValueRef + return outValue; + } + else + toString = [NSString stringWithFormat:@"%@", [[thisPrivateObject object] description]]; + } + + // Struct + if ([thisPrivateObject.type isEqualToString:@"struct"]) + { + id structDescription = nil; + id self = [JSCocoaController controllerFromContext:ctx]; + if ([self hasJSFunctionNamed:@"describeStruct"]) + { + JSStringRef scriptJS = JSStringCreateWithUTF8CString("return describeStruct(arguments[0])"); + JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); + JSValueRef jsValue = JSObjectCallAsFunction(ctx, fn, NULL, 1, (JSValueRef*)&thisObject, NULL); + JSStringRelease(scriptJS); + + [JSCocoaFFIArgument unboxJSValueRef:jsValue toObject:&structDescription inContext:ctx]; + } + + toString = [NSString stringWithFormat:@"<%@ %@>", thisPrivateObject.structureName, structDescription]; + } + + // Return a number is the whole string (no spaces, no others chars) is a number + // This emulates the javascript behaviour '4'*2 -> 8 when '4' is a string or an NSString + NSScanner* scan = [NSScanner scannerWithString:toString]; + [scan setCharactersToBeSkipped:nil]; + double v = 0; + [scan scanDouble:&v]; + if ([scan isAtEnd]) + return JSValueMakeNumber(ctx, v); + + // Convert to string and return + JSStringRef jsToString = JSStringCreateWithCFString((CFStringRef)toString); + JSValueRef jsValueToString = JSValueMakeString(ctx, jsToString); + JSStringRelease(jsToString); + return jsValueToString; +} + +// +// initialize +// retain boxed object +// +static void jsCocoaObject_initialize(JSContextRef ctx, JSObjectRef object) +{ + id o = JSObjectGetPrivate(object); + [o retain]; +} + +// +// finalize +// release boxed object +// +static void jsCocoaObject_finalize(JSObjectRef object) +{ +// NSLog(@"finalizing %p", object); + + // If dealloc is overloaded, releasing now will trigger JS code and fail + // As we're being called by GC, KJS might assert() in operationInProgress == NoOperation + JSCocoaPrivateObject* private = JSObjectGetPrivate(object); + + // Clean up the object now as WebKit calls us twice while cleaning __jsc__ (20110730) + JSObjectSetPrivate(object, NULL); + id jsc = nil; + JSContextRef ctx = [private ctx]; + + if (ctx) + jsc = [JSCocoa controllerFromContext:ctx]; + // We will be called during garbage collection before dealloc occurs. + // The __jsc__ variable will be gone, therefore controllerFromContext will yield 0. + // Not a problem since it's only used to remove the object from the boxedObjects hash, + // and dealloc will occur soon after. + + // + // If a boxed object is being destroyed, remove it from the cache + // + id boxedObject = [private object]; + if (boxedObject) { + if ([jsc isObjectBoxed:boxedObject]) { + // Safe dealloc ? + if ([boxedObject retainCount] == 1) { + if ([boxedObject respondsToSelector:@selector(safeDealloc)]) { + jsc = NULL; + object_getInstanceVariable(boxedObject, "__jsCocoaController", (void**)&jsc); + // Call safeDealloc if enabled (will be disabled upon last JSCocoaController release, to make sure the ) + if (jsc) { + if ([jsc useSafeDealloc]) + [jsc performSelector:@selector(safeDeallocInstance:) withObject:boxedObject afterDelay:0]; + } else + NSLog(@"safeDealloc could not find the context attached to %@.%p - allocate this object with instance, or add a Javascript variable to it (obj.hello = 'world')", [boxedObject class], boxedObject); + } + } + [jsc deleteBoxOfObject:boxedObject]; + } + } + + // Immediate release if dealloc is not overloaded + [private release]; + +#ifdef __OBJC_GC__ + // Mark internal object as collectable + [[NSGarbageCollector defaultCollector] enableCollectorForPointer:private]; +#endif +} + +/* +// +// Not needed as getProperty can return NULL to indicate property inexistance. +// +// log('doesNotExist' in object) +// getProperty returning undefined would mean the key is defined and has an undefined value. +// getProperty therefore returns NULL and the in operator returns false. +// -> hasProperty not needed. +// +static bool jsCocoaObject_hasProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyNameJS) +{ + NSString* propertyName = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, propertyNameJS); + [NSMakeCollectable(propertyName) autorelease]; + NSLog(@"hasProperty %@", propertyName); + return jsCocoaObject_getProperty(ctx, object, propertyNameJS, NULL); + return YES; +} +*/ + +// +// getProperty +// Return property in object's internal Javascript hash if its contains propertyName +// else ... +// Get objC method matching propertyName, autocall it +// else ... +// method may be a split call -> return a private object +// +// At method start, handle special cases for arrays (integers, length) and dictionaries +// +static JSValueRef jsCocoaObject_getProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyNameJS, JSValueRef* exception) +{ + NSString* propertyName = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, propertyNameJS); + [NSMakeCollectable(propertyName) autorelease]; + + JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(object); +// NSLog(@"Asking for property %@ %@(%@)", propertyName, privateObject, privateObject.type); + + // Get delegate + JSCocoaController* jsc = [JSCocoaController controllerFromContext:ctx]; + id delegate = jsc.delegate; + + if ([privateObject.type isEqualToString:@"@"]) + { +call: + // + // Delegate canGetProperty, getProperty + // + if (delegate) + { + // Check if getting is allowed + if ([delegate respondsToSelector:@selector(JSCocoa:canGetProperty:ofObject:inContext:exception:)]) + { + BOOL canGet = [delegate JSCocoa:jsc canGetProperty:propertyName ofObject:privateObject.object inContext:ctx exception:exception]; + if (!canGet) + { + if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow getting %@.%@", privateObject.object, propertyName]); + return NULL; + } + } + // Check if delegate handles getting + if ([delegate respondsToSelector:@selector(JSCocoa:getProperty:ofObject:inContext:exception:)]) + { + JSValueRef delegateGet = [delegate JSCocoa:jsc getProperty:propertyName ofObject:privateObject.object inContext:ctx exception:exception]; + if (delegateGet) return delegateGet; + } + } + + // Special case for NSMutableArray get and Javascript array methods +// if ([privateObject.object isKindOfClass:[NSArray class]]) + // Use respondsToSelector for custom indexed access + if ([privateObject.object respondsToSelector:@selector(objectAtIndex:)]) + { + id array = privateObject.object; + id scan = [NSScanner scannerWithString:propertyName]; + NSInteger propertyIndex; + // Is asked property an int ? + BOOL convertedToInt = ([scan scanInteger:&propertyIndex]); + if (convertedToInt && [scan isAtEnd]) + { + if (propertyIndex < 0 || propertyIndex >= [array count]) return NULL; + + id o = [array objectAtIndex:propertyIndex]; + JSValueRef value = NULL; + [JSCocoaFFIArgument boxObject:o toJSValueRef:&value inContext:ctx]; + return value; + } + + // If we have 'length', switch it to 'count' + if ([propertyName isEqualToString:@"length"]) propertyName = @"count"; + + // NSArray bridge + id callee = [privateObject object]; + SEL sel = NSSelectorFromString(propertyName); + if ([propertyName rangeOfString:@":"].location == NSNotFound && ![callee respondsToSelector:sel] + && ![propertyName isEqualToString:@"valueOf"] + && ![propertyName isEqualToString:@"toString"] + ) + { + id script = [NSString stringWithFormat:@"return Array.prototype.%@", propertyName]; + JSStringRef scriptJS = JSStringCreateWithUTF8CString([script UTF8String]); + JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); + JSValueRef result = JSObjectCallAsFunction(ctx, fn, NULL, 0, NULL, NULL); + JSStringRelease(scriptJS); + BOOL isJavascriptArrayMethod = result ? !JSValueIsUndefined(ctx, result) : NO; + + // Return the packaged Javascript function + if (isJavascriptArrayMethod) + { +// NSLog(@"*** array method : %@", propertyName); +// JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; + JSObjectRef o = [jsc newPrivateFunction]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(o); + private.type = @"jsFunction"; + [private setJSValueRef:result ctx:ctx]; + return o; + } + } + } + + + // Special case for NSMutableDictionary get +// if ([privateObject.object isKindOfClass:[NSDictionary class]]) + // Use respondsToSelector for custom indexed access + if ([privateObject.object respondsToSelector:@selector(objectForKey:)]) + { + id dictionary = privateObject.object; + id o = [dictionary objectForKey:propertyName]; + if (o) + { + JSValueRef value = NULL; + [JSCocoaFFIArgument boxObject:o toJSValueRef:&value inContext:ctx]; + return value; + } + } + + // Special case for JSCocoaMemoryBuffer get + if ([privateObject.object isKindOfClass:[JSCocoaMemoryBuffer class]]) + { + id buffer = privateObject.object; + + id scan = [NSScanner scannerWithString:propertyName]; + NSInteger propertyIndex; + // Is asked property an int ? + BOOL convertedToInt = ([scan scanInteger:&propertyIndex]); + if (convertedToInt && [scan isAtEnd]) + { + if (propertyIndex < 0 || propertyIndex >= [buffer typeCount]) return NULL; + return [buffer valueAtIndex:propertyIndex inContext:ctx]; + } + } + + // Check object's internal property in its jsHash + id callee = [privateObject object]; + if ([callee respondsToSelector:@selector(JSValueForJSName:)]) + { + JSValueRefAndContextRef name = { JSValueMakeString(ctx, propertyNameJS), ctx } ; + JSValueRef hashProperty = [callee JSValueForJSName:name].value; + if (hashProperty && !JSValueIsNull(ctx, hashProperty)) + { + BOOL returnHashValue = YES; + // Make sure to not return hash value if it's native code (valueOf, toString) + if ([propertyName isEqualToString:@"valueOf"] || [propertyName isEqualToString:@"toString"]) + { + id script = [NSString stringWithFormat:@"return arguments[0].toString().indexOf('[native code]') != -1", propertyName]; + JSStringRef scriptJS = JSStringCreateWithUTF8CString([script UTF8String]); + JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); + JSValueRef result = JSObjectCallAsFunction(ctx, fn, NULL, 1, (JSValueRef*)&hashProperty, NULL); + JSStringRelease(scriptJS); + BOOL isNativeCode = result ? JSValueToBoolean(ctx, result) : NO; + returnHashValue = !isNativeCode; +// NSLog(@"isNative(%@)=%d rawJSResult=%p hashProperty=%p returnHashValue=%d", propertyName, isNativeCode, result, hashProperty, returnHashValue); + } + if (returnHashValue) return hashProperty; + } + } +/* + // ## Use javascript override functions, only bridge side. Discarded for now as it doesn't give a way to call the original method + // ## Plus : useful ? as it can be done by setting custom js functions on the boxed objects + // Check if this is a Javascript override + id script = [NSString stringWithFormat:@"__globalJSFunctionRepository__.%@.%@", [callee class], propertyName]; + JSStringRef jsScript = JSStringCreateWithUTF8CString([script UTF8String]); + JSValueRef result = JSEvaluateScript(ctx, jsScript, NULL, NULL, 1, NULL); + JSStringRelease(jsScript); + if (result && JSValueGetType(ctx, result) == kJSTypeObject) + { + NSLog(@"GOT IT %@", propertyName); + JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(o); + private.type = @"jsFunction"; + [private setJSValueRef:result ctx:ctx]; + return o; + } +*/ + // + // Attempt Zero arg autocall + // Object.alloc().init() -> Object.alloc.init + // + if ([jsc useAutoCall]) + { + callee = [privateObject object]; + SEL sel = NSSelectorFromString(propertyName); + + BOOL isInstanceCall = [propertyName isEqualToString:@"instance"]; + // Go for zero arg call + if ([propertyName rangeOfString:@":"].location == NSNotFound && ([callee respondsToSelector:sel] || isInstanceCall)) + { + // + // Delegate canCallMethod, callMethod + // + if (delegate) + { + // Check if calling is allowed + if ([delegate respondsToSelector:@selector(JSCocoa:canCallMethod:ofObject:argumentCount:arguments:inContext:exception:)]) + { + BOOL canCall = [delegate JSCocoa:jsc canCallMethod:propertyName ofObject:callee argumentCount:0 arguments:NULL inContext:ctx exception:exception]; + if (!canCall) + { + if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow calling [%@ %@]", callee, propertyName]); + return NULL; + } + } + // Check if delegate handles calling + if ([delegate respondsToSelector:@selector(JSCocoa:callMethod:ofObject:privateObject:argumentCount:arguments:inContext:exception:)]) + { + JSValueRef delegateCall = [delegate JSCocoa:jsc callMethod:propertyName ofObject:callee privateObject:privateObject argumentCount:0 arguments:NULL inContext:ctx exception:exception]; + if (delegateCall) + return delegateCall; + } + } + + // instance + if (isInstanceCall) + { + // Manually call and box our object + id class = [callee class]; + id instance = [[class alloc] init]; + JSValueRef returnValue; + [JSCocoaFFIArgument boxObject:instance toJSValueRef:&returnValue inContext:ctx]; + // Release it, making the javascript box the sole retainer + // Nulling all references to this object will release the instance during Javascript GC + JSCocoaPrivateObject* private = JSObjectGetPrivate(JSValueToObject(ctx, returnValue, NULL)); + [private.object release]; + + return returnValue; + } + + // Special case for alloc autocall — do not retain alloced result as it might crash (eg [[NSLocale alloc] retain] fails in ObjC) + if ([propertyName isEqualToString:@"alloc"]) + { + id allocatedObject = [callee alloc]; +// JSObjectRef jsObject = [JSCocoaController jsCocoaPrivateObjectInContext:ctx]; + JSObjectRef jsObject = [jsc newPrivateObject]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(jsObject); + private.type = @"@"; + [private setObjectNoRetain:allocatedObject]; + return jsObject; + } + + // Get method pointer + Method method = class_getInstanceMethod([callee class], sel); + if (!method) method = class_getClassMethod([callee class], sel); + + // If we didn't find a method, try Distant Object + if (!method) + { + JSValueRef res = [jsc JSCocoa:jsc callMethod:propertyName ofObject:callee privateObject:privateObject argumentCount:0 arguments:NULL inContext:ctx exception:exception]; + if (res) return res; + + throwException(ctx, exception, [NSString stringWithFormat:@"Could not get property[%@ %@]", callee, propertyName]); + return NULL; + } + + // Extract arguments + const char* typeEncoding = method_getTypeEncoding(method); + id argumentEncodings = [JSCocoaController parseObjCMethodEncoding:typeEncoding]; + // Call address + void* callAddress = getObjCCallAddress(argumentEncodings); + + // + // ffi data + // + ffi_cif cif; + ffi_type* args[2]; + void* values[2]; + char* selector; + + selector = (char*)NSSelectorFromString(propertyName); + args[0] = &ffi_type_pointer; + args[1] = &ffi_type_pointer; + values[0] = (void*)&callee; + values[1] = (void*)&selector; + + // Get return value holder + id returnValue = [argumentEncodings objectAtIndex:0]; + + // Allocate return value storage if it's a pointer + if ([returnValue typeEncoding] == '^') + [returnValue allocateStorage]; + + // Setup ffi + ffi_status prep_status = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, [returnValue ffi_type], args); + // + // Call ! + // + if (prep_status == FFI_OK) + { + void* storage = [returnValue storage]; + if ([returnValue ffi_type] == &ffi_type_void) storage = NULL; + ffi_call(&cif, callAddress, storage, values); + } + + // Return now if our function returns void + // NO - box it +// if ([returnValue ffi_type] == &ffi_type_void) return NULL; + // Else, convert return value + JSValueRef jsReturnValue = NULL; + BOOL converted = [returnValue toJSValueRef:&jsReturnValue inContext:ctx]; + if (!converted) return throwException(ctx, exception, [NSString stringWithFormat:@"Return value not converted in %@", propertyName]), NULL; + + return jsReturnValue; + } + } + + // Check if we're holding an out value + if ([privateObject.object isKindOfClass:[JSCocoaOutArgument class]]) + { + JSValueRef outValue = [(JSCocoaOutArgument*)privateObject.object outJSValueRefInContext:ctx]; + if (outValue && JSValueGetType(ctx, outValue) == kJSTypeObject) + { + JSObjectRef outObject = JSValueToObject(ctx, outValue, NULL); + JSValueRef possibleReturnValue = JSObjectGetProperty(ctx, outObject, propertyNameJS, NULL); + return possibleReturnValue; + } + } + + // Info object for instances and classes + if ([propertyName isEqualToString:@RuntimeInformationPropertyName]) + { + JSObjectRef o = JSObjectMake(ctx, jsCocoaInfoClass, NULL); + + JSStringRef classNameProperty = JSStringCreateWithUTF8CString("className"); + JSStringRef className = JSStringCreateWithUTF8CString([[[[privateObject object] class] description] UTF8String]); + JSObjectSetProperty(ctx, o, classNameProperty, JSValueMakeString(ctx, className), + kJSPropertyAttributeReadOnly|kJSPropertyAttributeDontEnum|kJSPropertyAttributeDontDelete, NULL); + JSStringRelease(classNameProperty); + JSStringRelease(className); + return o; + } + + + // + // We're asked a property name and at this point we've checked the class's jsarray, autocall. + // If the property we're asked does not start a split call we'll return NULL. + // + // Check if the property is actually a method. + // If NO, replace underscores with colons + // add a ':' suffix + // + // If callee still fails to responds to that, check if propertyName starts a split call. + // If NO, return null + // + id methodName = [NSMutableString stringWithString:propertyName]; + // If responds to selector, OK + if (![callee respondsToSelector:NSSelectorFromString(methodName)] + // non ObjC methods + && ![methodName isEqualToString:@"valueOf"] + && ![methodName isEqualToString:@"Super"] + && ![methodName isEqualToString:@"Original"] +/* && ![methodName isEqualToString:@"instance"]*/) + { + // If setting on boxed objects is allowed, check existence of a property set on the js object - this is a reentrant call + if ([jsc canSetOnBoxedObjects]) + { + // We need to bypass our get handler to get the js value + static int canSetCheck = 0; + // Return NULL so the get handler will retrieve the js property stored in the js object + if (canSetCheck > 0) + return NULL; + + canSetCheck++; + // Call default handler + JSValueRef jsValueSetOnBoxedObject = JSObjectGetProperty(ctx, object, propertyNameJS, nil); + canSetCheck--; + + // If we have something other than undefined, return it + if (JSValueGetType(ctx, jsValueSetOnBoxedObject) != kJSTypeUndefined) + return jsValueSetOnBoxedObject; + } + + if ([methodName rangeOfString:@"_"].location != NSNotFound) + [methodName replaceOccurrencesOfString:@"_" withString:@":" options:0 range:NSMakeRange(0, [methodName length])]; + + if ([jsc callSelectorsMissingTrailingSemicolon] && ![methodName hasSuffix:@":"]) [methodName appendString:@":"]; + + if (![callee respondsToSelector:NSSelectorFromString(methodName)]) + { + // Instance check + if ([methodName hasPrefix:@"instance"]) + { + id initMethodName = [NSString stringWithFormat:@"init%@", [methodName substringFromIndex:8]]; + if ([callee instancesRespondToSelector:NSSelectorFromString(initMethodName)]) + { +// JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; + JSObjectRef o = [jsc newPrivateFunction]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(o); + private.type = @"method"; + private.methodName = methodName; + return o; + } + } + + // + // This may be a JS function + // + Class class = [callee class]; + JSValueRef result = NULL; + while (class) + { + id script = [NSString stringWithFormat:@"__globalJSFunctionRepository__.%@.%@", class, propertyName]; + JSStringRef jsScript = JSStringCreateWithUTF8CString([script UTF8String]); + result = JSEvaluateScript(ctx, jsScript, NULL, NULL, 1, NULL); + JSStringRelease(jsScript); + // Found ? Break + if (result && JSValueGetType(ctx, result) == kJSTypeObject) break; + + // Go up parent class + class = [class superclass]; + } + // This is a pure JS function call — box it + if (result && JSValueGetType(ctx, result) == kJSTypeObject) + { +// JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; + JSObjectRef o = [jsc newPrivateFunction]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(o); + private.type = @"jsFunction"; + [private setJSValueRef:result ctx:ctx]; + return o; + } + + methodName = propertyName; + + // Get the meta class if callee is a class + class = [callee class]; + if (callee == class) + class = objc_getMetaClass(object_getClassName(class)); + // Try split start + BOOL isMaybeSplit = NO; + if ([jsc useSplitCall]) + isMaybeSplit = [JSCocoaController isMaybeSplitCall:methodName forClass:class]; + // If not split and not NSString, return (if NSString, try to convert to JS string in callAsFunction and use native JS methods) + if (!isMaybeSplit && ![callee isKindOfClass:[NSString class]]) + { + return NULL; + } + } + } + + // Get ready for method call +// JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; + JSObjectRef o = [jsc newPrivateFunction]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(o); + private.type = @"method"; + private.methodName = methodName; + + return o; + } + + // Struct + rawPointer valueOf + if (/*[privateObject.type isEqualToString:@"struct"] &&*/ ([propertyName isEqualToString:@"valueOf"] || [propertyName isEqualToString:@"toString"])) + { +// JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; + JSObjectRef o = [jsc newPrivateFunction]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(o); + private.type = @"method"; + private.methodName = propertyName; + return o; + } + + + // Pointer ops + // * If we have an external Javascript context, query it + // * Handle pointer reference / dereference with JSCocoaFFIArgument + if ([privateObject.type isEqualToString:@"rawPointer"]) + { + BOOL responds = NO; + id methodName = propertyName; + responds = [privateObject respondsToSelector:NSSelectorFromString(propertyName)]; + if (!responds) { + methodName = [NSString stringWithFormat:@"%@:", methodName]; + responds = [privateObject respondsToSelector:NSSelectorFromString(methodName)]; + } + if (responds) + { + // When calling a method with arguments, this will be used to get the instance on which to call + id callee = privateObject; + // Retaining the object leaks + [privateObject setObjectNoRetain:privateObject]; + + privateObject = [[JSCocoaPrivateObject new] autorelease]; + privateObject.object = callee; + privateObject.type = @"@"; + goto call; + } + } + + // External WebView value + if ([privateObject.type isEqualToString:@"externalJSValueRef"] || [[privateObject rawPointerEncoding] isEqualToString:@"^{OpaqueJSContext=}"]) + { + JSValueRef externalValue = [privateObject jsValueRef]; + JSContextRef externalCtx = externalValue ? [privateObject ctx] : [privateObject rawPointer]; + JSObjectRef externalObject = externalValue ? JSValueToObject(externalCtx, externalValue, NULL) : JSContextGetGlobalObject(externalCtx); + + if (!JSObjectHasProperty(externalCtx, externalObject, propertyNameJS)) return NULL; + JSValueRef r = JSObjectGetProperty(externalCtx, externalObject, propertyNameJS, exception); + // If WebView had an exception, re-throw it in our context + if (exception && *exception) + { + id s = [JSCocoaController formatJSException:*exception inContext:externalCtx]; + throwException(ctx, exception, [NSString stringWithFormat:@"(WebView) %@", s]); + return JSValueMakeNull(ctx); + } + JSValueRef r2 = boxedValueFromExternalContext(externalCtx, r, ctx); + return r2; + } + + + // Structs will get here when being asked javascript attributes (eg 'x' in point.x) +// NSLog(@"Asking for property %@ %@(%@)", propertyName, privateObject, privateObject.type); + + return NULL; +} + + +// +// setProperty +// call setter : propertyName -> setPropertyName +// +static bool jsCocoaObject_setProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyNameJS, JSValueRef jsValue, JSValueRef* exception) +{ + JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(object); + NSString* propertyName = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, propertyNameJS); + [NSMakeCollectable(propertyName) autorelease]; + +// NSLog(@"****SET %@ in ctx %p on object %p (type=%@) method=%@", propertyName, ctx, object, privateObject.type, privateObject.methodName); + + // Get delegate + JSCocoaController* jsc = [JSCocoaController controllerFromContext:ctx]; + id delegate = jsc.delegate; + + if ([privateObject.type isEqualToString:@"@"]) + { + // + // Delegate canSetProperty, setProperty + // + if (delegate) + { + // Check if setting is allowed + if ([delegate respondsToSelector:@selector(JSCocoa:canSetProperty:ofObject:toValue:inContext:exception:)]) + { + BOOL canSet = [delegate JSCocoa:jsc canSetProperty:propertyName ofObject:privateObject.object toValue:jsValue inContext:ctx exception:exception]; + if (!canSet) + { + if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow setting %@.%@", privateObject.object, propertyName]); + return NULL; + } + } + // Check if delegate handles getting + if ([delegate respondsToSelector:@selector(JSCocoa:setProperty:ofObject:toValue:inContext:exception:)]) + { + BOOL delegateSet = [delegate JSCocoa:jsc setProperty:propertyName ofObject:privateObject.object toValue:jsValue inContext:ctx exception:exception]; + if (delegateSet) return true; + } + } + + // Special case for NSMutableArray set +// if ([privateObject.object isKindOfClass:[NSArray class]]) + if ([privateObject.object respondsToSelector:@selector(replaceObjectAtIndex:withObject:)]) + { + id array = privateObject.object; +// if (![array respondsToSelector:@selector(replaceObjectAtIndex:withObject:)]) return throwException(ctx, exception, @"Calling set on a non mutable array"), false; + id scan = [NSScanner scannerWithString:propertyName]; + NSInteger propertyIndex; + // Is asked property an int ? + BOOL convertedToInt = ([scan scanInteger:&propertyIndex]); + if (convertedToInt && [scan isAtEnd]) + { + if (propertyIndex < 0 || propertyIndex >= [array count]) return false; + + id property = NULL; + if ([JSCocoaFFIArgument unboxJSValueRef:jsValue toObject:&property inContext:ctx]) + { + [array replaceObjectAtIndex:propertyIndex withObject:property]; + return true; + } + else return false; + } + } + + + // Special case for NSMutableDictionary set +// if ([privateObject.object isKindOfClass:[NSDictionary class]]) + if ([privateObject.object respondsToSelector:@selector(setObject:forKey:)]) + { + id dictionary = privateObject.object; +// if (![dictionary respondsToSelector:@selector(setObject:forKey:)]) return throwException(ctx, exception, @"Calling set on a non mutable dictionary"), false; + + id property = NULL; + if ([JSCocoaFFIArgument unboxJSValueRef:jsValue toObject:&property inContext:ctx]) + { + [dictionary setObject:property forKey:propertyName]; + return true; + } + else return false; + } + + + // Special case for JSCocoaMemoryBuffer get + if ([privateObject.object isKindOfClass:[JSCocoaMemoryBuffer class]]) + { + id buffer = privateObject.object; + + id scan = [NSScanner scannerWithString:propertyName]; + NSInteger propertyIndex; + // Is asked property an int ? + BOOL convertedToInt = ([scan scanInteger:&propertyIndex]); + if (convertedToInt && [scan isAtEnd]) + { + if (propertyIndex < 0 || propertyIndex >= [buffer typeCount]) return NULL; + return [buffer setValue:jsValue atIndex:propertyIndex inContext:ctx]; + } + } + + + + // Try shorthand overload : obc[selector] = function + id callee = [privateObject object]; + if ([propertyName rangeOfString:@":"].location != NSNotFound) + { + JSValueRefAndContextRef v = { jsValue, ctx }; + [JSCocoaController overloadInstanceMethod:propertyName class:[callee class] jsFunction:v]; + return true; + } + + + // Can't use capitalizedString on the whole string as it will transform + // myValue + // to Myvalue (thereby destroying camel letters) + // we want MyValue + + // Capitalize only first letter + NSString* setterName = [NSString stringWithFormat:@"set%@%@:", + [[propertyName substringWithRange:NSMakeRange(0,1)] capitalizedString], + [propertyName substringWithRange:NSMakeRange(1, [propertyName length]-1)]]; + + // + // Attempt Zero arg autocall for setter + // Object.alloc().init() -> Object.alloc.init + // + SEL sel = NSSelectorFromString(setterName); + if ([callee respondsToSelector:sel]) + { + // + // Delegate canCallMethod, callMethod + // + if (delegate) + { + // Check if calling is allowed + if ([delegate respondsToSelector:@selector(JSCocoa:canCallMethod:ofObject:argumentCount:arguments:inContext:exception:)]) + { + BOOL canCall = [delegate JSCocoa:jsc canCallMethod:setterName ofObject:callee argumentCount:0 arguments:NULL inContext:ctx exception:exception]; + if (!canCall) + { + if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow calling [%@ %@]", callee, setterName]); + return NULL; + } + } + // Check if delegate handles calling + if ([delegate respondsToSelector:@selector(JSCocoa:callMethod:ofObject:privateObject:argumentCount:arguments:inContext:exception:)]) + { + JSValueRef delegateCall = [delegate JSCocoa:jsc callMethod:setterName ofObject:callee privateObject:privateObject argumentCount:0 arguments:NULL inContext:ctx exception:exception]; + if (delegateCall) return !!delegateCall; + } + } + + // Get method pointer + Method method = class_getInstanceMethod([callee class], sel); + if (!method) method = class_getClassMethod([callee class], sel); + + // If we didn't find a method, try Distant Object + if (!method) + { + // Last chance before exception : try calling DO + BOOL b = [jsc JSCocoa:jsc setProperty:propertyName ofObject:callee toValue:jsValue inContext:ctx exception:exception]; + if (b) return YES; + + throwException(ctx, exception, [NSString stringWithFormat:@"Could not set property[%@ %@]", callee, propertyName]); + return NULL; + } + + // Extract arguments + const char* typeEncoding = method_getTypeEncoding(method); + id argumentEncodings = [JSCocoaController parseObjCMethodEncoding:typeEncoding]; + if ([[argumentEncodings objectAtIndex:0] typeEncoding] != 'v') return throwException(ctx, exception, [NSString stringWithFormat:@"(in setter) %@ must return void", setterName]), false; + + // Call address + void* callAddress = getObjCCallAddress(argumentEncodings); + + // + // ffi data + // + ffi_cif cif; + ffi_type* args[3]; + void* values[3]; + char* selector; + + selector = (char*)NSSelectorFromString(setterName); + args[0] = &ffi_type_pointer; + args[1] = &ffi_type_pointer; + values[0] = (void*)&callee; + values[1] = (void*)&selector; + + // Get arg (skip return value, instance, selector) + JSCocoaFFIArgument* arg = [argumentEncodings objectAtIndex:3]; + BOOL converted = [arg fromJSValueRef:jsValue inContext:ctx]; + if (!converted) return throwException(ctx, exception, [NSString stringWithFormat:@"(in setter) Argument %c not converted", [arg typeEncoding]]), false; + args[2] = [arg ffi_type]; + values[2] = [arg storage]; + + // Setup ffi + ffi_status prep_status = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 3, &ffi_type_void, args); + // + // Call ! + // + if (prep_status == FFI_OK) + { + ffi_call(&cif, callAddress, NULL, values); + } + return true; + } + + if ([callee respondsToSelector:@selector(setJSValue:forJSName:)]) + { + // Set as instance variable +// BOOL set = [callee setJSValue:jsValue forJSName:propertyNameJS]; + JSValueRefAndContextRef value = { JSValueMakeNull(ctx), ctx }; + value.value = jsValue; + + JSValueRefAndContextRef name = { JSValueMakeNull(ctx), ctx } ; + name.value = JSValueMakeString(ctx, propertyNameJS); + BOOL set = [callee setJSValue:value forJSName:name]; + if (set) return true; + } + } + + // External WebView value + if ([privateObject.type isEqualToString:@"externalJSValueRef"] || [[privateObject rawPointerEncoding] isEqualToString:@"^{OpaqueJSContext=}"]) + { + JSValueRef externalValue = [privateObject jsValueRef]; + JSContextRef externalCtx = externalValue ? [privateObject ctx] : [privateObject rawPointer]; + JSObjectRef externalObject = externalValue ? JSValueToObject(externalCtx, externalValue, NULL) : JSContextGetGlobalObject(externalCtx); + if (!externalObject) return false; + + JSValueRef convertedValue = valueToExternalContext(ctx, jsValue, externalCtx); + JSObjectSetProperty(externalCtx, externalObject, propertyNameJS, convertedValue, kJSPropertyAttributeNone, exception); + + // If WebView had an exception, re-throw it in our context + if (exception && *exception) + { + id s = [JSCocoaController formatJSException:*exception inContext:externalCtx]; + throwException(ctx, exception, [NSString stringWithFormat:@"(WebView) %@", s]); + return false; + } + + return true; + } + + // + // From here we return false to have Javascript set values on Javascript objects : valueOf, thisObject, structures + // + + // Special case for autocall : allow current js object to receive a custom valueOf method that will handle autocall + // And a thisObject property holding class for instance autocall + if ([propertyName isEqualToString:@"valueOf"]) return false; + // An out argument allocates pointer storage when calling stuff like gl version. + // JSCocoa needs to set a custom javascript property to recognize out arguments. + if ([propertyName isEqualToString:@"isOutArgument"]) return false; + // Allow general setting on structs + if ([privateObject.type isEqualToString:@"struct"]) return false; + + // Don't throw an exception if setting is allowed + if ([jsc canSetOnBoxedObjects]) return false; + + // Setter fails AND WARNS if propertyName can't be set + // This happens of non-JSCocoa ObjC objects, eg NSWorkspace.sharedWorspace.someVariable = value + return throwException(ctx, exception, [NSString stringWithFormat:@"(in setter) object %@ does not support setting — Derive from that class to make it able to host any Javascript object ", privateObject.object]), false; +} + + +// +// deleteProperty +// delete property in hash +// +static bool jsCocoaObject_deleteProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyNameJS, JSValueRef* exception) +{ + NSString* propertyName = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, propertyNameJS); + [NSMakeCollectable(propertyName) autorelease]; + + JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(object); + + if (![privateObject.type isEqualToString:@"@"]) return false; + + id callee = [privateObject object]; + if (![callee respondsToSelector:@selector(setJSValue:forJSName:)]) return false; + JSValueRefAndContextRef name = { JSValueMakeNull(ctx), ctx } ; + name.value = JSValueMakeString(ctx, propertyNameJS); + return [callee deleteJSValueForJSName:name]; +} + + +// +// getPropertyNames +// enumerate dictionary keys +// +static void jsCocoaObject_getPropertyNames(JSContextRef ctx, JSObjectRef object, JSPropertyNameAccumulatorRef propertyNames) +{ + JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(object); + + // If we have a dictionary, add keys from allKeys + if ([privateObject.type isEqualToString:@"@"]) + { + id o = privateObject.object; + // Vend property only for classes + if (o == [o class]) + { + JSStringRef jsString = JSStringCreateWithUTF8CString(RuntimeInformationPropertyName); + JSPropertyNameAccumulatorAddName(propertyNames, jsString); + JSStringRelease(jsString); + } + if ([o isKindOfClass:[NSDictionary class]]) + { + id dictionary = privateObject.object; + id keys = [dictionary allKeys]; + + for (id key in keys) + { + JSStringRef jsString = JSStringCreateWithUTF8CString([key UTF8String]); + JSPropertyNameAccumulatorAddName(propertyNames, jsString); + JSStringRelease(jsString); + } + } + } +} + + + +// +// callAsFunction +// done in two methods. +// jsCocoaObject_callAsFunction is called first and handles +// * C and ObjC calls : calls jsCocoaObject_callAsFunction_ffi +// * Super call : in a derived ObjC class method, call this.Super(arguments) to call the parent method with jsCocoaObject_callAsFunction_ffi +// * js function calls : on an ObjC class, use of pure js functions as methods +// * toString, valueOf +// +// jsCocoaObject_callAsFunction_ffi calls a C function or an ObjC method with provided arguments. +// + +// This uses libffi to call C and ObjC. +static JSValueRef jsCocoaObject_callAsFunction_ffi(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, JSValueRef arguments[], JSValueRef* exception, NSString* superSelector, Class superSelectorClass, BOOL isVariadic, JSValueRef** argumentsToFree) +{ + JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(function); + JSCocoaPrivateObject* thisPrivateObject = JSObjectGetPrivate(thisObject); + + // Return an exception if calling on NULL + if (thisPrivateObject.object == NULL && !privateObject.xml) + return throwException(ctx, exception, @"jsCocoaObject_callAsFunction : call with null object"), NULL; + + // Function address + void* callAddress = NULL; + + // Number of arguments of called method or function + NSUInteger callAddressArgumentCount = 0; + + // Arguments encoding + // Holds return value encoding as first element + NSMutableArray* argumentEncodings = nil; + + // Calling ObjC ? If NO, we're calling C + BOOL callingObjC = NO; + // Structure return (objc_msgSend_stret) + BOOL usingStret = NO; + // Calling instance... , replaced with init... and released, making the js object sole owner + BOOL callingInstance = NO; + + + // Get delegate + JSCocoaController* jsc = [JSCocoaController controllerFromContext:ctx]; + id delegate = jsc.delegate; + + // + // ObjC setup + // + id callee = NULL, methodName = NULL, functionName = NULL; + + // Calls can be made on boxed ObjC objects AND JSCocoaPrivateObjects + if ([privateObject.type isEqualToString:@"method"] && ([thisPrivateObject.type isEqualToString:@"@"] || [thisPrivateObject.object class] == [JSCocoaPrivateObject class])) + { + callingObjC = YES; + callee = [thisPrivateObject object]; + methodName = superSelector ? superSelector : [NSMutableString stringWithString:privateObject.methodName]; +// NSLog(@"calling %@.%@", callee, methodName); + + // + // Delegate canCallMethod, callMethod + // Called first so it gets a chance to do handle custom messages + // + if (delegate) + { + // Check if calling is allowed + if ([delegate respondsToSelector:@selector(JSCocoa:canCallMethod:ofObject:argumentCount:arguments:inContext:exception:)]) + { + BOOL canCall = [delegate JSCocoa:jsc canCallMethod:methodName ofObject:callee argumentCount:argumentCount arguments:arguments inContext:ctx exception:exception]; + if (!canCall) + { + if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow calling [%@ %@]", callee, methodName]); + return NULL; + } + } + // Check if delegate handles calling + if ([delegate respondsToSelector:@selector(JSCocoa:callMethod:ofObject:privateObject:argumentCount:arguments:inContext:exception:)]) + { + JSValueRef delegateCall = [delegate JSCocoa:jsc callMethod:methodName ofObject:callee privateObject:thisPrivateObject argumentCount:argumentCount arguments:arguments inContext:ctx exception:exception]; + if (delegateCall) return delegateCall; + } + } + // Special case for alloc autocall — do not retain alloced result as it might crash (eg [[NSLocale alloc] retain] fails in ObjC) + if (![jsc useAutoCall] && argumentCount == 0 && [methodName isEqualToString:@"alloc"]) + { + id allocatedObject = [callee alloc]; +// JSObjectRef jsObject = [JSCocoaController jsCocoaPrivateObjectInContext:ctx]; + JSObjectRef jsObject = [jsc newPrivateObject]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(jsObject); + private.type = @"@"; + [private setObjectNoRetain:allocatedObject]; + return jsObject; + } + + + // Instance call +/* + if ([callee class] == callee && [methodName isEqualToString:@"instance"]) + { + if (argumentCount > 1) return throwException(ctx, exception, @"Invalid argument count in instance call : must be 0 or 1"), NULL; + return [callee instanceWithContext:ctx argumentCount:argumentCount arguments:arguments exception:exception]; + } +*/ + // Check selector + if (![callee respondsToSelector:NSSelectorFromString(methodName)]) + { + // + // Split call + // set( { value : '5', forKey : 'hello' } ) + // -> setValue:forKey: + // + if ([jsc useSplitCall]) + { + id splitMethodName = privateObject.methodName; + id class = [callee class]; + if (callee == class) + class = objc_getMetaClass(object_getClassName(class)); + BOOL isSplitCall = [JSCocoaController trySplitCall:&splitMethodName class:class argumentCount:&argumentCount arguments:&arguments ctx:ctx]; + if (isSplitCall) + { + methodName = splitMethodName; + // trySplitCall returned new arguments that we'll need to free later on + *argumentsToFree = arguments; + } + } + } + + // Get method pointer + Method method = class_getInstanceMethod([callee class], NSSelectorFromString(methodName)); + if (!method) method = class_getClassMethod([callee class], NSSelectorFromString(methodName)); + + // If we didn't find a method, try an instance call, then try treating object as Javascript string, then try Distant Object + if (!method) + { + // Instance Check + if ([methodName hasPrefix:@"instance"]) + { + id initMethodName = [NSString stringWithFormat:@"init%@", [methodName substringFromIndex:8]]; + id class = [callee class]; + method = class_getInstanceMethod(class, NSSelectorFromString(initMethodName)); + methodName = initMethodName; + callee = [class alloc]; + callingInstance = YES; + } + + if (!method) + { + // (First) Last chance before exception : try treating callee as a Javascript string + if ([callee isKindOfClass:[NSString class]]) + { + id script = [NSString stringWithFormat:@"String.prototype.%@", methodName]; + JSStringRef jsScript = JSStringCreateWithUTF8CString([script UTF8String]); + JSValueRef result = JSEvaluateScript(ctx, jsScript, NULL, NULL, 1, NULL); + JSStringRelease(jsScript); + if (result && JSValueGetType(ctx, result) == kJSTypeObject) + { + JSStringRef string = JSStringCreateWithCFString((CFStringRef)callee); + JSValueRef stringValue = JSValueMakeString(ctx, string); + JSStringRelease(string); + + JSObjectRef functionObject = JSValueToObject(ctx, result, NULL); + JSObjectRef jsThisObject = JSValueToObject(ctx, stringValue, NULL); + JSValueRef r = JSObjectCallAsFunction(ctx, functionObject, jsThisObject, argumentCount, arguments, NULL); + return r; + } + } + + // Last chance before exception : try calling DO + JSValueRef res = [jsc JSCocoa:jsc callMethod:methodName ofObject:callee privateObject:thisPrivateObject argumentCount:argumentCount arguments:arguments inContext:ctx exception:exception]; + if (res) return res; + + return throwException(ctx, exception, [NSString stringWithFormat:@"jsCocoaObject_callAsFunction : method %@ of object %@ not found — remnant of a split call ?", methodName, [callee class]]), NULL; + } + } + + // Extract arguments + const char* typeEncoding = method_getTypeEncoding(method); +// NSLog(@"method %@ encoding=%s", methodName, typeEncoding); + argumentEncodings = [JSCocoaController parseObjCMethodEncoding:typeEncoding]; + if (!argumentEncodings) { + return throwException(ctx, exception, [NSString stringWithFormat:@"jsCocoaObject_callAsFunction could not parse type encodings %s of [%@ %@]", [JSCocoa typeEncodingOfMethod:methodName class:[[callee class] description]], methodName, [callee class]]), NULL; + } + // Function arguments is all arguments minus return value and [instance, selector] params to objc_send + callAddressArgumentCount = [argumentEncodings count]-3; + + // Get call address + callAddress = getObjCCallAddress(argumentEncodings); + } + + // + // C setup + // + if (!callingObjC) + { + if (!privateObject.xml) return throwException(ctx, exception, @"jsCocoaObject_callAsFunction : no xml in object = nothing to call (Autocall problem ? To call argless objCobject.method(), remove the parens if autocall is ON)") , NULL; +// NSLog(@"C encoding=%@", privateObject.xml); + argumentEncodings = [JSCocoaController parseCFunctionEncoding:privateObject.xml functionName:&functionName]; + // Grab symbol + callAddress = dlsym(RTLD_DEFAULT, [functionName UTF8String]); + if (!callAddress) return throwException(ctx, exception, [NSString stringWithFormat:@"Function %@ not found", functionName]), NULL; + // Function arguments is all arguments minus return value + callAddressArgumentCount = [argumentEncodings count]-1; + + // + // Delegate canCallFunction + // + if (delegate) + { + // Check if calling is allowed + if ([delegate respondsToSelector:@selector(JSCocoa:canCallFunction:argumentCount:arguments:inContext:exception:)]) + { + BOOL canCall = [delegate JSCocoa:jsc canCallFunction:functionName argumentCount:argumentCount arguments:arguments inContext:ctx exception:exception]; + if (!canCall) + { + if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow calling function %@", functionName]); + return NULL; + } + } + } + } + + // + // Variadic call ? + // If argument count doesn't match descripted argument count, + // we may have a variadic call + // + // Possibly account for a missing terminating NULL in ObjC variadic method + // -> allows calling + // [NSArray arrayWithObjects:'hello', 'world'] + // instead of + // [NSArray arrayWithObjects:'hello', 'world', null] + // + BOOL sugarCheckVariadic = NO; + // Check if selector or method names matches a known variadic method. This may be a false positive ... + if (isVariadic) + { + // ... so we check further. + if (methodName) isVariadic = [[JSCocoaController controllerFromContext:ctx] isMethodVariadic:methodName class:[callee class]]; + else isVariadic = [[JSCocoaController controllerFromContext:ctx] isFunctionVariadic:functionName]; + + // Bail if not variadic + if (!isVariadic) + { + return throwException(ctx, exception, [NSString stringWithFormat:@"Bad argument count in %@ : expected %d, got %d", functionName ? functionName : methodName, callAddressArgumentCount, argumentCount]), NULL; + } + // Sugar check : if last object is not NULL, account for it + if (isVariadic && callingObjC && argumentCount && !JSValueIsNull(ctx, arguments[argumentCount-1])) + { + // Will be tested during argument conversion + sugarCheckVariadic = YES; + argumentCount++; + } + } + else + { + if (callAddressArgumentCount != argumentCount) + { + return throwException(ctx, exception, [NSString stringWithFormat:@"Bad argument count in %@ : expected %d, got %d", functionName ? functionName : methodName, callAddressArgumentCount, argumentCount]), NULL; + } + } + + // + // ffi data + // + ffi_cif cif; + ffi_type** args = NULL; + void** values = NULL; + char* selector; + // super call + struct objc_super _super; + void* superPointer; + + // Total number of arguments to ffi_call + NSUInteger effectiveArgumentCount = argumentCount + (callingObjC ? 2 : 0); + if (effectiveArgumentCount > 0) + { + args = malloc(sizeof(ffi_type*)*effectiveArgumentCount); + values = malloc(sizeof(void*)*effectiveArgumentCount); + + // If calling ObjC, setup instance and selector + int i, idx = 0; + if (callingObjC) + { + selector = (char*)NSSelectorFromString(methodName); + args[0] = &ffi_type_pointer; + args[1] = &ffi_type_pointer; + values[0] = (void*)&callee; + values[1] = (void*)&selector; + idx = 2; + + // Super handling + if (superSelector) + { + if (superSelectorClass == nil) return throwException(ctx, exception, [NSString stringWithFormat:@"Null superclass in %@", callee]), NULL; + callAddress = objc_msgSendSuper; + if (usingStret) callAddress = objc_msgSendSuper_stret; + _super.receiver = callee; +#if __LP64__ + _super.super_class = superSelectorClass; +//#elif TARGET_IPHONE_SIMULATOR || !TARGET_OS_IPHONE +// _super.class = superSelectorClass; +#else + _super.super_class = superSelectorClass; +#endif + superPointer = &_super; + values[0] = &superPointer; +// NSLog(@"superClass=%@ (old=%@) (%@) function=%p", superSelectorClass, [callee superclass], [callee class], function); + } + } + + // Setup arguments, unboxing or converting data + for (i=0; i= callAddressArgumentCount) + { + arg = [[JSCocoaFFIArgument alloc] init]; + [arg setTypeEncoding:'@']; + [arg autorelease]; + } + else + arg = [argumentEncodings objectAtIndex:idx+1]; + + // Convert argument + JSValueRef jsValue = sugarCheckVariadic && i == argumentCount-1 ? JSValueMakeNull(ctx) : arguments[i]; + BOOL shouldConvert = YES; + // Check type o modifiers + if ([arg typeEncoding] == '^') + { + // If holding a JSCocoaOutArgument, allocate custom storage + if (JSValueGetType(ctx, jsValue) == kJSTypeObject) + { + JSStringRef jsName = JSStringCreateWithUTF8CString("isOutArgument"); + BOOL isOutArgument = JSValueToBoolean(ctx, JSObjectGetProperty(ctx, JSValueToObject(ctx, jsValue, NULL), jsName, NULL)); + JSStringRelease(jsName); + if (isOutArgument) + { + id unboxed = nil; + [JSCocoaFFIArgument unboxJSValueRef:jsValue toObject:&unboxed inContext:ctx]; + if (unboxed && [unboxed isKindOfClass:[JSCocoaOutArgument class]]) + { + if (![(JSCocoaOutArgument*)unboxed mateWithJSCocoaFFIArgument:arg]) return throwException(ctx, exception, [NSString stringWithFormat:@"Pointer argument %@ not handled", [arg pointerTypeEncoding]]), NULL; + shouldConvert = NO; + [arg setIsOutArgument:YES]; + } + if (unboxed && [unboxed isKindOfClass:[JSCocoaMemoryBuffer class]]) + { + JSCocoaMemoryBuffer* buffer = unboxed; + [arg setTypeEncoding:[arg typeEncoding] withCustomStorage:[buffer pointerForIndex:0]]; + shouldConvert = NO; + [arg setIsOutArgument:YES]; + } + } + } + + if (shouldConvert) + { + // Allocate default storage + [arg allocateStorage]; + } + + } + + args[idx] = [arg ffi_type]; + if (shouldConvert) + { + BOOL converted = [arg fromJSValueRef:jsValue inContext:ctx]; + if (!converted) + return throwException(ctx, exception, [NSString stringWithFormat:@"Argument %c not converted", [arg typeEncoding]]), NULL; + } + values[idx] = [arg storage]; + } + } + + // Get return value holder + id returnValue = [argumentEncodings objectAtIndex:0]; + + // Allocate return value storage if it's a pointer + if ([returnValue typeEncoding] == '^') + [returnValue allocateStorage]; + + // Setup ffi + ffi_status prep_status = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, (unsigned int)effectiveArgumentCount, [returnValue ffi_type], args); + + // + // Call ! + // + if (prep_status == FFI_OK) + { + void* storage = [returnValue storage]; + if ([returnValue ffi_type] == &ffi_type_void) storage = NULL; + + // Catch exceptions when calling ObjC + if (callingObjC) + { + @try + { + ffi_call(&cif, callAddress, storage, values); + } + @catch (NSException* e) + { + if (effectiveArgumentCount > 0) + { + free(args); + free(values); + } + [JSCocoaFFIArgument boxObject:e toJSValueRef:exception inContext:ctx]; + return NULL; + } + } + else + ffi_call(&cif, callAddress, storage, values); + } + + if (effectiveArgumentCount > 0) + { + free(args); + free(values); + } + if (prep_status != FFI_OK) return throwException(ctx, exception, @"ffi_prep_cif failed"), NULL; + + // Return now if our function returns void + // Return null as a JSValueRef to avoid crashing + if ([returnValue ffi_type] == &ffi_type_void) return JSValueMakeNull(ctx); + + // Else, convert return value + JSValueRef jsReturnValue = NULL; + BOOL converted = [returnValue toJSValueRef:&jsReturnValue inContext:ctx]; + if (!converted) return throwException(ctx, exception, [NSString stringWithFormat:@"Return value not converted in %@", methodName?methodName:functionName]), NULL; + + // Instance call : release object to make js object sole owner + if (callingInstance) + { + JSCocoaPrivateObject* private = JSObjectGetPrivate(JSValueToObject(ctx, jsReturnValue, NULL)); + [private.object release]; + } + + return jsReturnValue; +} + +// +// This method handles +// * C and ObjC calls +// * Super call : retrieves the method name to call, thereby giving new arguments to jsCocoaObject_callAsFunction_ffi +// * js function calls : on an ObjC class, use of pure js functions as methods +// * toString, valueOf +// +static JSValueRef jsCocoaObject_callAsFunction(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) +{ + JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(function); + JSValueRef* superArguments = NULL; + id superSelector = NULL; + id superSelectorClass = NULL; + + // Pure JS functions for derived ObjC classes + if ([privateObject jsValueRef]) + { + if ([privateObject.type isEqualToString:@"jsFunction"]) + { + JSObjectRef jsFunction = JSValueToObject(ctx, [privateObject jsValueRef], NULL); + JSValueRef ret = JSObjectCallAsFunction(ctx, jsFunction, thisObject, argumentCount, arguments, exception); + return ret; + } + else + if ([privateObject.type isEqualToString:@"externalJSValueRef"]) + { + JSContextRef externalCtx = [privateObject ctx]; + JSObjectRef jsFunction = JSValueToObject(externalCtx, [privateObject jsValueRef], NULL); + if (!jsFunction) + { + throwException(ctx, exception, [NSString stringWithFormat:@"WebView call : value not a function"]); + return JSValueMakeNull(ctx); + } + + // Retrieve 'this' : either the global external object (window), or a result from previous calll + JSObjectRef externalThisObject; + JSCocoaPrivateObject* privateThis = JSObjectGetPrivate(thisObject); + if ([privateThis jsValueRef]) externalThisObject = JSValueToObject(externalCtx, [privateThis jsValueRef], NULL); + else externalThisObject = JSContextGetGlobalObject(externalCtx); + + if (!externalThisObject) + { + throwException(ctx, exception, [NSString stringWithFormat:@"WebView call : externalThisObject not found"]); + return JSValueMakeNull(ctx); + } + + // Convert arguments to WebView context + JSValueRef* convertedArguments = NULL; + if (argumentCount) convertedArguments = malloc(sizeof(JSValueRef)*argumentCount); + for (int i=0; i", + [self class], + self, + ((id)self == (id)[self class]) ? @"Class" : @"", + [boxedObject class], + boxedObject, + retainCount]; +} + +@end + diff --git a/JSCocoa/JSCocoaFFIArgument.h b/JSCocoa/JSCocoaFFIArgument.h new file mode 100644 index 000000000..911d6d563 --- /dev/null +++ b/JSCocoa/JSCocoaFFIArgument.h @@ -0,0 +1,97 @@ +// +// JSCocoaFFIArgument.h +// JSCocoa +// +// Created by Patrick Geiller on 14/07/08. +// Copyright 2008 __MyCompanyName__. All rights reserved. +// + +#if !TARGET_IPHONE_SIMULATOR && !TARGET_OS_IPHONE +#import +#import +#define MACOSX +#include +#endif +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE +#import "iPhone/libffi/ffi.h" +#endif + +@interface JSCocoaFFIArgument : NSObject { + char typeEncoding; + NSString* structureTypeEncoding; + NSString* pointerTypeEncoding; + + void* ptr; + + ffi_type structureType; + + id customData; + BOOL isReturnValue; + BOOL ownsStorage; + BOOL isOutArgument; +} + +- (NSString*)typeDescription; + +- (BOOL)setTypeEncoding:(char)encoding; +- (BOOL)setTypeEncoding:(char)encoding withCustomStorage:(void*)storagePtr; +- (void)setStructureTypeEncoding:(NSString*)encoding; +- (void)setStructureTypeEncoding:(NSString*)encoding withCustomStorage:(void*)storagePtr; +- (void)setPointerTypeEncoding:(NSString*)encoding; + ++ (int)sizeOfTypeEncoding:(char)encoding; ++ (int)alignmentOfTypeEncoding:(char)encoding; + ++ (ffi_type*)ffi_typeForTypeEncoding:(char)encoding; + ++ (int)sizeOfStructure:(NSString*)encoding; + + ++ (NSArray*)typeEncodingsFromStructureTypeEncoding:(NSString*)structureTypeEncoding; ++ (NSArray*)typeEncodingsFromStructureTypeEncoding:(NSString*)structureTypeEncoding parsedCount:(NSInteger*)count; + + ++ (NSString*)structureNameFromStructureTypeEncoding:(NSString*)structureTypeEncoding; ++ (NSString*)structureFullTypeEncodingFromStructureTypeEncoding:(NSString*)structureTypeEncoding; ++ (NSString*)structureFullTypeEncodingFromStructureName:(NSString*)structureName; ++ (NSString*)structureTypeEncodingDescription:(NSString*)structureTypeEncoding; + ++ (BOOL)fromJSValueRef:(JSValueRef)value inContext:(JSContextRef)ctx typeEncoding:(char)typeEncoding fullTypeEncoding:(NSString*)fullTypeEncoding fromStorage:(void*)ptr; + ++ (BOOL)toJSValueRef:(JSValueRef*)value inContext:(JSContextRef)ctx typeEncoding:(char)typeEncoding fullTypeEncoding:(NSString*)fullTypeEncoding fromStorage:(void*)ptr; + ++ (NSInteger)structureToJSValueRef:(JSValueRef*)value inContext:(JSContextRef)ctx fromCString:(char*)c fromStorage:(void**)storage; ++ (NSInteger)structureToJSValueRef:(JSValueRef*)value inContext:(JSContextRef)ctx fromCString:(char*)c fromStorage:(void**)ptr initialValues:(JSValueRef*)initialValues initialValueCount:(NSInteger)initialValueCount convertedValueCount:(NSInteger*)convertedValueCount; ++ (NSInteger)structureFromJSObjectRef:(JSObjectRef)value inContext:(JSContextRef)ctx inParentJSValueRef:(JSValueRef)parentValue fromCString:(char*)c fromStorage:(void**)ptr; + ++ (void)alignPtr:(void**)ptr accordingToEncoding:(char)encoding; ++ (void)advancePtr:(void**)ptr accordingToEncoding:(char)encoding; + + +- (void*)allocateStorage; +- (void*)allocatePointerStorage; +- (void**)storage; +- (void**)rawStoragePointer; +- (char)typeEncoding; +- (NSString*)structureTypeEncoding; +- (id)pointerTypeEncoding; + + +- (void)setIsReturnValue:(BOOL)v; +- (BOOL)isReturnValue; +- (void)setIsOutArgument:(BOOL)v; +- (BOOL)isOutArgument; + +- (BOOL)fromJSValueRef:(JSValueRef)value inContext:(JSContextRef)ctx; +- (BOOL)toJSValueRef:(JSValueRef*)value inContext:(JSContextRef)ctx; + + ++ (BOOL)boxObject:(id)o toJSValueRef:(JSValueRef*)value inContext:(JSContextRef)ctx; ++ (BOOL)unboxJSValueRef:(JSValueRef)value toObject:(id*)o inContext:(JSContextRef)ctx; ++ (BOOL)unboxJSArray:(JSObjectRef)value toObject:(id*)o inContext:(JSContextRef)ctx; ++ (BOOL)unboxJSHash:(JSObjectRef)value toObject:(id*)o inContext:(JSContextRef)ctx; + + +- (ffi_type*)ffi_type; + +@end diff --git a/JSCocoa/JSCocoaFFIArgument.m b/JSCocoa/JSCocoaFFIArgument.m new file mode 100644 index 000000000..0b60508ce --- /dev/null +++ b/JSCocoa/JSCocoaFFIArgument.m @@ -0,0 +1,1357 @@ +// +// JSCocoaFFIArgument.m +// JSCocoa +// +// Created by Patrick Geiller on 14/07/08. +// Copyright 2008 __MyCompanyName__. All rights reserved. +// + +#import "JSCocoaFFIArgument.h" +#import "JSCocoaController.h" +#import "JSCocoaPrivateObject.h" +#import + + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE +#import "GDataDefines.h" +#import "GDataXMLNode.h" +#endif + +@implementation JSCocoaFFIArgument + + +- (id)init +{ + self = [super init]; + + ptr = NULL; + typeEncoding = 0; + isReturnValue = NO; + ownsStorage = YES; + isOutArgument = NO; + + structureTypeEncoding = nil; + structureType.elements = NULL; + + pointerTypeEncoding = nil; + + // Used to store string data while converting JSStrings to char* + customData = nil; + + return self; +} + +- (void)cleanUp +{ + if (structureTypeEncoding) [structureTypeEncoding release]; + if (pointerTypeEncoding) [pointerTypeEncoding release]; + if (ptr && ownsStorage) free(ptr); + if (customData) [customData release]; + + if (structureType.elements) free(structureType.elements); + ptr = NULL; +} + +- (void)dealloc +{ + [self cleanUp]; + [super dealloc]; +} +- (void)finalize +{ + [self cleanUp]; + [super finalize]; +} + +- (NSString*)description +{ + return [NSString stringWithFormat:@"JSCocoaFFIArgument %p typeEncoding=%c %@ isReturnValue=%d storage=%p", self, + typeEncoding, + (structureTypeEncoding ? structureTypeEncoding : @""), + isReturnValue, ptr]; +} + ++ (NSString*)typeDescriptionForTypeEncoding:(char)typeEncoding fullTypeEncoding:(NSString*)fullTypeEncoding +{ + switch (typeEncoding) + { + case _C_VOID: return @"void"; + case _C_ID: return @"ObjC object"; + case _C_CLASS: return @"ObjC class"; + case _C_CHR: return @"char"; + case _C_UCHR: return @"unsigned char"; + case _C_SHT: return @"short"; + case _C_USHT: return @"unsigned short"; + case _C_INT: return @"int"; + case _C_UINT: return @"unsigned int"; + case _C_LNG: return @"long"; + case _C_ULNG: return @"unsigned long"; + case _C_LNG_LNG: return @"long long"; + case _C_ULNG_LNG:return @"unsigned long long"; + case _C_FLT: return @"float"; + case _C_DBL: return @"double"; + case '{': + { + // Special case for getting raw JSValues to ObjC + BOOL isJSStruct = [fullTypeEncoding hasPrefix:@"{JSValueRefAndContextRef"]; + if (isJSStruct) + { + return @"(JSCocoa structure used to pass JSValueRef without conversion)"; + } + +// if (!JSValueIsObject(ctx, value)) return NO; +// JSObjectRef object = JSValueToObject(ctx, value, NULL); +// void* p = ptr; +// id r = [JSCocoaFFIArgument structureFullTypeEncodingFromStructureTypeEncoding:fullTypeEncoding]; +// if (!r) return [NSString stringWithFormat:@"(unknown structure %@)", fullTypeEncoding]; + return [JSCocoaFFIArgument structureTypeEncodingDescription:fullTypeEncoding]; + } + case _C_SEL: return @"selector"; + case _C_CHARPTR: return @"char*"; + case _C_BOOL: return @"BOOL"; + case _C_PTR: return @"pointer"; + case _C_UNDEF: return @"(function pointer or block?)"; + } + return @"(unknown)"; +} + +- (NSString*)typeDescription +{ + return [[self class] typeDescriptionForTypeEncoding:typeEncoding fullTypeEncoding:structureTypeEncoding]; +} + +#pragma mark Getters / Setters + +// +// Needed because libffi needs at least sizeof(long) as return value storage +// +- (void)setIsReturnValue:(BOOL)v +{ + isReturnValue = v; +} +- (BOOL)isReturnValue +{ + return isReturnValue; +} + +- (void)setIsOutArgument:(BOOL)v +{ + isOutArgument = v; +} +- (BOOL)isOutArgument +{ + return isOutArgument; +} + +- (char)typeEncoding +{ + return typeEncoding; +} + +- (BOOL)setTypeEncoding:(char)encoding +{ + if ([JSCocoaFFIArgument sizeOfTypeEncoding:encoding] == -1) { + NSLog(@"Bad type encoding %c", encoding); + return NO; + }; + + typeEncoding = encoding; + [self allocateStorage]; + + return YES; +} + +- (BOOL)setTypeEncoding:(char)encoding withCustomStorage:(void*)storagePtr +{ + if ([JSCocoaFFIArgument sizeOfTypeEncoding:encoding] == -1) { + NSLog(@"Bad type encoding %c", encoding); + return NO; + }; + + typeEncoding = encoding; + ownsStorage = NO; + ptr = storagePtr; + + return YES; +} + +- (NSString*)structureTypeEncoding +{ + return structureTypeEncoding; +} + +- (void)setStructureTypeEncoding:(NSString*)encoding +{ + [self setStructureTypeEncoding:encoding withCustomStorage:NULL]; +} + +- (void)setStructureTypeEncoding:(NSString*)encoding withCustomStorage:(void*)storagePtr +{ + typeEncoding = '{'; + structureTypeEncoding = [[NSString alloc] initWithString:encoding]; + + if (storagePtr) + { + ownsStorage = NO; + ptr = storagePtr; + } + else [self allocateStorage]; + + id types = [JSCocoaFFIArgument typeEncodingsFromStructureTypeEncoding:encoding]; + NSUInteger elementCount = [types count]; + + // + // Build FFI type + // + structureType.size = 0; + structureType.alignment = 0; + structureType.type = FFI_TYPE_STRUCT; + structureType.elements = malloc(sizeof(ffi_type*)*(elementCount+1)); // +1 is trailing NULL + + int i = 0; + for (id type in types) + { + char charEncoding = *(char*)[type UTF8String]; + structureType.elements[i++] = [JSCocoaFFIArgument ffi_typeForTypeEncoding:charEncoding]; + } + structureType.elements[elementCount] = NULL; +} + +// +// type o handling +// (pointers passed as arguments to a function, function writes values to these arguments) +// +- (void)setPointerTypeEncoding:(NSString*)encoding +{ + typeEncoding = '^'; + pointerTypeEncoding = [[NSString alloc] initWithString:encoding]; +} + +- (id)pointerTypeEncoding +{ + return pointerTypeEncoding; +} + + +- (ffi_type*)ffi_type +{ + if (!typeEncoding) return NULL; + if (pointerTypeEncoding) return &ffi_type_pointer; + + if (typeEncoding == '{') return &structureType; + + return [JSCocoaFFIArgument ffi_typeForTypeEncoding:typeEncoding]; +} + + +#pragma mark Storage + +- (void*)allocateStorage +{ + if (!typeEncoding) return NSLog(@"No type encoding set in %@", self), NULL; + + // NO ! will destroy structureTypeEncoding +// [self cleanUp]; + // Special case for structs + if (typeEncoding == '{') + { +// NSLog(@"allocateStorage: Allocating struct"); + // Some front padding for alignment and tail padding for structure + // ( http://developer.apple.com/documentation/DeveloperTools/Conceptual/LowLevelABI/Articles/IA32.html ) + // Structures are tail-padded to 32-bit multiples. + + // +16 for alignment + // +4 for tail padding +// ptr = malloc([JSCocoaFFIArgument sizeOfStructure:structureTypeEncoding] + 16 + 4); + ptr = malloc([JSCocoaFFIArgument sizeOfStructure:structureTypeEncoding] + 4); + return ptr; + } + + int size = [JSCocoaFFIArgument sizeOfTypeEncoding:typeEncoding]; + + // Bail if we can't handle our type + if (size == -1) return NSLog(@"Can't handle type %c", typeEncoding), NULL; + if (size >= 0) + { + int minimalReturnSize = sizeof(long); + if (isReturnValue && size < minimalReturnSize) size = minimalReturnSize; + ptr = malloc(size); + } +// NSLog(@"Allocated size=%d (%p) for object %@", size, ptr, self); + + return ptr; +} + +// type o : out arguments (eg fn(int* pointerToIntResult)) +- (void*)allocatePointerStorage +{ + typeEncoding = [pointerTypeEncoding UTF8String][1]; + if (typeEncoding == '{') + { + structureTypeEncoding = [pointerTypeEncoding substringFromIndex:1]; + [structureTypeEncoding retain]; + } + [self allocateStorage]; + return ptr; +} + +- (void**)storage +{ + if (typeEncoding == '{') + { +/* + int alignOnSize = 16; + + int address = (int)ptr; + if ((address % alignOnSize) != 0) + address = (address+alignOnSize) & ~(alignOnSize-1); +*/ + if (pointerTypeEncoding) return &ptr; +// return (void**)address; + } + + // Type o : return writable address +// if (pointerTypeEncoding) + if (isOutArgument) + { + return &ptr; + } + + return ptr; +} + +- (void**)rawStoragePointer +{ + return ptr; +} + +// This destroys the original pointer value by modifying it in place : maybe change to returning the new address ? ++ (void)alignPtr:(void**)ptr accordingToEncoding:(char)encoding +{ + int alignOnSize = [JSCocoaFFIArgument alignmentOfTypeEncoding:encoding]; + + long address = (long)*ptr; + if ((address % alignOnSize) != 0) + address = (address+alignOnSize) & ~(alignOnSize-1); +// NSLog(@"alignOf(%c)=%d", encoding, alignOnSize); + + *ptr = (void*)address; +} + +// This destroys the original pointer value by modifying it in place : maybe change to returning the new address ? ++ (void)advancePtr:(void**)ptr accordingToEncoding:(char)encoding +{ + long address = (long)*ptr; + address += [JSCocoaFFIArgument sizeOfTypeEncoding:encoding]; + *ptr = (void*)address; +} + + +#pragma mark Conversion + +// +// Convert from js value +// +- (BOOL)fromJSValueRef:(JSValueRef)value inContext:(JSContextRef)ctx +{ + BOOL r = [JSCocoaFFIArgument fromJSValueRef:value inContext:ctx typeEncoding:typeEncoding fullTypeEncoding:structureTypeEncoding?structureTypeEncoding:pointerTypeEncoding fromStorage:ptr]; + if (!r) + { + NSLog(@"fromJSValueRef FAILED, jsType=%d encoding=%c structureEncoding=%@", JSValueGetType(ctx, value), typeEncoding, structureTypeEncoding); + } + return r; +} + ++ (BOOL)fromJSValueRef:(JSValueRef)value inContext:(JSContextRef)ctx typeEncoding:(char)typeEncoding fullTypeEncoding:(NSString*)fullTypeEncoding fromStorage:(void*)ptr +{ + if (!typeEncoding) return NO; + +// JSType type = JSValueGetType(ctx, value); +// NSLog(@"JSType=%d encoding=%c self=%p", type, typeEncoding, self); + + switch (typeEncoding) + { + case _C_ID: + case _C_CLASS: + { + return [self unboxJSValueRef:value toObject:ptr inContext:ctx]; + } + + case _C_CHR: + case _C_UCHR: + case _C_SHT: + case _C_USHT: + case _C_INT: + case _C_UINT: + case _C_LNG: + case _C_ULNG: + case _C_LNG_LNG: + case _C_ULNG_LNG: + case _C_FLT: + case _C_DBL: + { + double number = JSValueToNumber(ctx, value, NULL); +// unsigned int u = number; +// NSLog(@"type=%d typeEncoding=%c n=%f uint=%d, %d", JSValueGetType(ctx, value), typeEncoding, number, (unsigned int)number, u); + + switch (typeEncoding) + { + case _C_CHR: *(char*)ptr = (char)number; break; + case _C_UCHR: *(unsigned char*)ptr = (unsigned char)number; break; + case _C_SHT: *(short*)ptr = (short)number; break; + case _C_USHT: *(unsigned short*)ptr = (unsigned short)number; break; + case _C_INT: + case _C_UINT: + { +#ifdef __BIG_ENDIAN__ + // Two step conversion : to unsigned int then to int. One step conversion fails on PPC. + unsigned int uint = (unsigned int)number; + *(signed int*)ptr = (signed int)uint; + break; +#endif +#ifdef __LITTLE_ENDIAN__ + *(int*)ptr = (int)number; + break; +#endif + } +/* + case _C_UINT: + { + // Two step conversion : to unsigned int then to int. One step conversion fails on PPC. + int uint = (int)number; + unsigned int u = (unsigned)uint; + NSLog(@"%d %u", uint, u); + *(signed int*)ptr = (signed int)uint; + break; + } +*/ +// case _C_UINT: *(unsigned int*)ptr = (unsigned int)number; break; + case _C_LNG: *(long*)ptr = (long)number; break; + case _C_ULNG: *(unsigned long*)ptr = (unsigned long)number; break; + case _C_LNG_LNG: *(long long*)ptr = (long long)number; break; + case _C_ULNG_LNG: *(unsigned long long*)ptr = (unsigned long long)number; break; + case _C_FLT: *(float*)ptr = (float)number; break; + case _C_DBL: *(double*)ptr = (double)number; break; + } + return YES; + } + case '{': + { + // Special case for getting raw JSValues to ObjC +// BOOL isJSStruct = NSOrderedSame == [fullTypeEncoding compare:@"{JSValueRefAndContextRef" options:0 range:NSMakeRange(0, sizeof("{JSValueRefAndContextRef")-1)]; + BOOL isJSStruct = [fullTypeEncoding hasPrefix:@"{JSValueRefAndContextRef"]; + + if (isJSStruct) + { + // Beware ! This context is not the global context and will be valid only for that call. + // Other uses (closures) use the global context via JSCocoaController. + JSValueRefAndContextRef* jsStruct = (JSValueRefAndContextRef*)ptr; + jsStruct->value = value; + jsStruct->ctx = ctx; + return YES; + } + + if (!JSValueIsObject(ctx, value)) return NO; + JSObjectRef object = JSValueToObject(ctx, value, NULL); + void* p = ptr; + id type = [JSCocoaFFIArgument structureFullTypeEncodingFromStructureTypeEncoding:fullTypeEncoding]; + NSInteger numParsed = [JSCocoaFFIArgument structureFromJSObjectRef:object inContext:ctx inParentJSValueRef:NULL fromCString:(char*)[type UTF8String] fromStorage:&p]; + return numParsed; + } + case _C_SEL: + { + id str = NSStringFromJSValue(ctx, value); + *(SEL*)ptr = NSSelectorFromString(str); + return YES; + } + case _C_CHARPTR: + { + id str = NSStringFromJSValue(ctx, value); + *(char**)ptr = (char*)[str UTF8String]; + return YES; + } + case _C_BOOL: + { + bool b = JSValueToBoolean(ctx, value); + *(BOOL*)ptr = b; + return YES; + } + + case _C_PTR: + { + if ([fullTypeEncoding hasPrefix:@"^{OpaqueJSValue"]) + { + NSLog(@"JSValueRef argument was converted to nil — to pass raw Javascript values to ObjC, use JSValueRefAndContextRef"); + *(id*)ptr = nil; + return YES; + } + return [self unboxJSValueRef:value toObject:ptr inContext:ctx]; + } + + } + return NO; +} + + +// +// Convert to js value +// +- (BOOL)toJSValueRef:(JSValueRef*)value inContext:(JSContextRef)ctx +{ + void* p = ptr; +#ifdef __BIG_ENDIAN__ + long v; + // Return value was padded, need to do some shifting on PPC + if (isReturnValue) + { + int size = [JSCocoaFFIArgument sizeOfTypeEncoding:typeEncoding]; + int paddedSize = sizeof(long); + + if (size > 0 && size < paddedSize && paddedSize == 4) + { + v = *(long*)ptr; + v = CFSwapInt32(v); + p = &v; + } + } +#endif +// if (typeEncoding == '{') p = [self storage]; + id encoding = structureTypeEncoding ? structureTypeEncoding : pointerTypeEncoding; + BOOL r = [JSCocoaFFIArgument toJSValueRef:value inContext:ctx typeEncoding:typeEncoding fullTypeEncoding:encoding fromStorage:p]; + if (!r) NSLog(@"toJSValueRef FAILED"); + return r; +} + + ++ (BOOL)toJSValueRef:(JSValueRef*)value inContext:(JSContextRef)ctx typeEncoding:(char)typeEncoding fullTypeEncoding:(NSString*)fullTypeEncoding fromStorage:(void*)ptr +{ + if (!typeEncoding) return NO; + +// NSLog(@"toJSValueRef: %c ptr=%p", typeEncoding, ptr); + switch (typeEncoding) + { + case _C_ID: + case _C_CLASS: + { + id objcObject = *(id*)ptr; + return [self boxObject:(id)objcObject toJSValueRef:value inContext:ctx]; + + } + + case _C_VOID: + return YES; + + case _C_CHR: + case _C_UCHR: + case _C_SHT: + case _C_USHT: + case _C_INT: + case _C_UINT: + case _C_LNG: + case _C_ULNG: + case _C_LNG_LNG: + case _C_ULNG_LNG: + case _C_FLT: + case _C_DBL: + { + double number; + switch (typeEncoding) + { + case _C_CHR: number = *(char*)ptr; break; + case _C_UCHR: number = *(unsigned char*)ptr; break; + case _C_SHT: number = *(short*)ptr; break; + case _C_USHT: number = *(unsigned short*)ptr; break; + case _C_INT: number = *(int*)ptr; break; + case _C_UINT: number = *(unsigned int*)ptr; break; + case _C_LNG: number = *(long*)ptr; break; + case _C_ULNG: number = *(unsigned long*)ptr; break; + case _C_LNG_LNG: number = *(long long*)ptr; break; + case _C_ULNG_LNG: number = *(unsigned long long*)ptr; break; + case _C_FLT: number = *(float*)ptr; break; + case _C_DBL: number = *(double*)ptr; break; + } + *value = JSValueMakeNumber(ctx, number); + return YES; + } + + + case '{': + { + // Special case for getting raw JSValues from ObjC to JS + BOOL isJSStruct = [fullTypeEncoding hasPrefix:@"{JSValueRefAndContextRef"]; + if (isJSStruct) + { + JSValueRefAndContextRef* jsStruct = (JSValueRefAndContextRef*)ptr; + *value = jsStruct->value; + return YES; + } + + void* p = ptr; + id type = [JSCocoaFFIArgument structureFullTypeEncodingFromStructureTypeEncoding:fullTypeEncoding]; + // Bail if structure not found + if (!type) return 0; + +// JSObjectRef jsObject = [JSCocoaController jsCocoaPrivateObjectInContext:ctx]; + JSObjectRef jsObject = [[JSCocoa controllerFromContext:ctx] newPrivateObject]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(jsObject); + private.type = @"struct"; + NSInteger numParsed = [JSCocoaFFIArgument structureToJSValueRef:value inContext:ctx fromCString:(char*)[type UTF8String] fromStorage:&p]; + return numParsed; + } + + case _C_SEL: + { + SEL sel = *(SEL*)ptr; + id str = NSStringFromSelector(sel); +// JSStringRef jsName = JSStringCreateWithUTF8CString([str UTF8String]); + JSStringRef jsName = JSStringCreateWithCFString((CFStringRef)str); + *value = JSValueMakeString(ctx, jsName); + JSStringRelease(jsName); + return YES; + } + case _C_BOOL: + { + BOOL b = *(BOOL*)ptr; + *value = JSValueMakeBoolean(ctx, b); + return YES; + } + case _C_CHARPTR: + { + // Rturn Javascript null if char* is null + char* charPtr = *(char**)ptr; + if (!charPtr) + { + *value = JSValueMakeNull(ctx); + return YES; + } + // Convert to NSString and then to Javascript string + NSString* name = [NSString stringWithUTF8String:charPtr]; + JSStringRef jsName = JSStringCreateWithCFString((CFStringRef)name); + *value = JSValueMakeString(ctx, jsName); + JSStringRelease(jsName); + return YES; + } + + case _C_PTR: + { +// JSObjectRef o = [JSCocoaController jsCocoaPrivateObjectInContext:ctx]; + JSObjectRef o = [[JSCocoa controllerFromContext:ctx] newPrivateObject]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(o); + private.type = @"rawPointer"; + [private setRawPointer:*(void**)ptr encoding:fullTypeEncoding]; + *value = o; + return YES; + } + } + + return NO; +} + +/* + + *value MUST be NULL to be receive allocated JSValue + + The given pointer is advanced in place : its value will change after the call. + Pass a writeable pointer whose original value you don't care about. + +*/ ++ (NSInteger)structureToJSValueRef:(JSValueRef*)value inContext:(JSContextRef)ctx fromCString:(char*)c fromStorage:(void**)ptr +{ + return [self structureToJSValueRef:value inContext:ctx fromCString:c fromStorage:ptr initialValues:nil initialValueCount:0 convertedValueCount:nil]; +} + ++ (NSInteger)structureToJSValueRef:(JSValueRef*)value inContext:(JSContextRef)ctx fromCString:(char*)c fromStorage:(void**)ptr initialValues:(JSValueRef*)initialValues initialValueCount:(NSInteger)initialValueCount convertedValueCount:(NSInteger*)convertedValueCount +{ + // Build new structure object +// JSObjectRef jsObject = [JSCocoaController jsCocoaPrivateObjectInContext:ctx]; + JSObjectRef jsObject = [[JSCocoa controllerFromContext:ctx] newPrivateObject]; + JSCocoaPrivateObject* private = JSObjectGetPrivate(jsObject); + private.type = @"struct"; + private.structureName = [JSCocoaFFIArgument structureNameFromStructureTypeEncoding:[NSString stringWithUTF8String:c]]; + if (!*value) *value = jsObject; + + char* c0 = c; + // Skip '{' + c += 1; + // Skip '_' if it's there + if (*c == '_') c++; + // Skip structureName, '=' + c += [private.structureName length]+1; + + int openedBracesCount = 1; + int closedBracesCount = 0; + for (; *c && closedBracesCount != openedBracesCount; c++) + { + if (*c == '{') openedBracesCount++; + if (*c == '}') closedBracesCount++; + // Parse name then type + if (*c == '"') + { + char* c2 = c+1; + while (c2 && *c2 != '"') c2++; + id propertyName = [[[NSString alloc] initWithBytes:c+1 length:(c2-c-1) encoding:NSUTF8StringEncoding] autorelease]; + c = c2; + // Skip '"' + c++; + char encoding = *c; + + JSValueRef valueJS = NULL; + if (encoding == '{') + { + NSInteger numParsed = [self structureToJSValueRef:&valueJS inContext:ctx fromCString:c fromStorage:ptr initialValues:initialValues initialValueCount:initialValueCount convertedValueCount:convertedValueCount]; + c += numParsed; + } + else + { + // Given a pointer to raw C structure data, convert its members to JS values + if (ptr) + { + // Align + [JSCocoaFFIArgument alignPtr:ptr accordingToEncoding:encoding]; + // Get value + [JSCocoaFFIArgument toJSValueRef:&valueJS inContext:ctx typeEncoding:encoding fullTypeEncoding:nil fromStorage:*ptr]; + // Advance ptr + [JSCocoaFFIArgument advancePtr:ptr accordingToEncoding:encoding]; + } + else + // Given no pointer, get values from initialValues array. If not present, create undefined values + { + if (!convertedValueCount) return 0; + if (initialValues && initialValueCount && *convertedValueCount < initialValueCount) valueJS = initialValues[*convertedValueCount]; + else valueJS = JSValueMakeUndefined(ctx); + } + if (convertedValueCount) *convertedValueCount = *convertedValueCount+1; + } + JSStringRef propertyNameJS = JSStringCreateWithCFString((CFStringRef)propertyName); + JSObjectSetProperty(ctx, jsObject, propertyNameJS, valueJS, 0, NULL); + JSStringRelease(propertyNameJS); + } + } + return c-c0-1; +} + ++ (NSInteger)structureFromJSObjectRef:(JSObjectRef)object inContext:(JSContextRef)ctx inParentJSValueRef:(JSValueRef)parentValue fromCString:(char*)c fromStorage:(void**)ptr +{ + id structureName = [JSCocoaFFIArgument structureNameFromStructureTypeEncoding:[NSString stringWithUTF8String:c]]; + char* c0 = c; + // Skip '{' + c += 1; + // Skip '_' if it's there + if (*c == '_') c++; + // Skip structureName, '=' + c += [structureName length]+1; + +// NSLog(@"%@", structureName); + int openedBracesCount = 1; + int closedBracesCount = 0; + for (; *c && closedBracesCount != openedBracesCount; c++) + { + if (*c == '{') openedBracesCount++; + if (*c == '}') closedBracesCount++; + // Parse name then type + if (*c == '"') + { + char* c2 = c+1; + while (c2 && *c2 != '"') c2++; + id propertyName = [[[NSString alloc] initWithBytes:c+1 length:(c2-c-1) encoding:NSUTF8StringEncoding] autorelease]; + c = c2; + + // Skip '"' + c++; + char encoding = *c; + + JSStringRef propertyNameJS = JSStringCreateWithUTF8CString([propertyName UTF8String]); + JSValueRef valueJS = JSObjectGetProperty(ctx, object, propertyNameJS, NULL); + JSStringRelease(propertyNameJS); +// JSObjectRef objectProperty2 = JSValueToObject(ctx, valueJS, NULL); + +// NSLog(@"%c %@ %p %p", encoding, propertyName, valueJS, objectProperty2); + if (encoding == '{') + { + if (JSValueIsObject(ctx, valueJS)) + { + JSObjectRef objectProperty = JSValueToObject(ctx, valueJS, NULL); + NSInteger numParsed = [self structureFromJSObjectRef:objectProperty inContext:ctx inParentJSValueRef:NULL fromCString:c fromStorage:ptr]; + c += numParsed; + } + else return 0; + } + else + { + // Align + [JSCocoaFFIArgument alignPtr:ptr accordingToEncoding:encoding]; + // Get value + [JSCocoaFFIArgument fromJSValueRef:valueJS inContext:ctx typeEncoding:encoding fullTypeEncoding:nil fromStorage:*ptr]; + // Advance ptr + [JSCocoaFFIArgument advancePtr:ptr accordingToEncoding:encoding]; + } + + } + } + return c-c0-1; +} + + + +#pragma mark Encoding size, alignment, FFI + ++ (int)sizeOfTypeEncoding:(char)encoding +{ + switch (encoding) + { + case _C_ID: return sizeof(id); + case _C_CLASS: return sizeof(Class); + case _C_SEL: return sizeof(SEL); + case _C_CHR: return sizeof(char); + case _C_UCHR: return sizeof(unsigned char); + case _C_SHT: return sizeof(short); + case _C_USHT: return sizeof(unsigned short); + case _C_INT: return sizeof(int); + case _C_UINT: return sizeof(unsigned int); + case _C_LNG: return sizeof(long); + case _C_ULNG: return sizeof(unsigned long); + case _C_LNG_LNG: return sizeof(long long); + case _C_ULNG_LNG:return sizeof(unsigned long long); + case _C_FLT: return sizeof(float); + case _C_DBL: return sizeof(double); + case _C_BOOL: return sizeof(BOOL); + case _C_VOID: return sizeof(void); + case _C_PTR: return sizeof(void*); + case _C_CHARPTR: return sizeof(char*); + // Function pointers +// case _C_UNDEF: return sizeof(void*); + // Blocks are encoded with @? + } + return -1; +} + +/* + __alignOf__ returns 8 for double, but its struct align is 4 + + use dummy structures to get struct alignment, each having a byte as first element +*/ +typedef struct { char a; id b; } struct_C_ID; +typedef struct { char a; char b; } struct_C_CHR; +typedef struct { char a; short b; } struct_C_SHT; +typedef struct { char a; int b; } struct_C_INT; +typedef struct { char a; long b; } struct_C_LNG; +typedef struct { char a; long long b; } struct_C_LNG_LNG; +typedef struct { char a; float b; } struct_C_FLT; +typedef struct { char a; double b; } struct_C_DBL; +typedef struct { char a; BOOL b; } struct_C_BOOL; + ++ (int)alignmentOfTypeEncoding:(char)encoding +{ + switch (encoding) + { + case _C_ID: return offsetof(struct_C_ID, b); + case _C_CLASS: return offsetof(struct_C_ID, b); + case _C_SEL: return offsetof(struct_C_ID, b); + case _C_CHR: return offsetof(struct_C_CHR, b); + case _C_UCHR: return offsetof(struct_C_CHR, b); + case _C_SHT: return offsetof(struct_C_SHT, b); + case _C_USHT: return offsetof(struct_C_SHT, b); + case _C_INT: return offsetof(struct_C_INT, b); + case _C_UINT: return offsetof(struct_C_INT, b); + case _C_LNG: return offsetof(struct_C_LNG, b); + case _C_ULNG: return offsetof(struct_C_LNG, b); + case _C_LNG_LNG: return offsetof(struct_C_LNG_LNG, b); + case _C_ULNG_LNG:return offsetof(struct_C_LNG_LNG, b); + case _C_FLT: return offsetof(struct_C_FLT, b); + case _C_DBL: return offsetof(struct_C_DBL, b); + case _C_BOOL: return offsetof(struct_C_BOOL, b); + case _C_PTR: return offsetof(struct_C_ID, b); + case _C_CHARPTR: return offsetof(struct_C_ID, b); + } + return -1; +} + + ++ (ffi_type*)ffi_typeForTypeEncoding:(char)encoding +{ + switch (encoding) + { + case _C_ID: + case _C_CLASS: + case _C_SEL: + case _C_PTR: + case _C_CHARPTR: return &ffi_type_pointer; + + case _C_CHR: return &ffi_type_sint8; + case _C_UCHR: return &ffi_type_uint8; + case _C_SHT: return &ffi_type_sint16; + case _C_USHT: return &ffi_type_uint16; + case _C_INT: + case _C_LNG: return &ffi_type_sint32; + case _C_UINT: + case _C_ULNG: return &ffi_type_uint32; + case _C_LNG_LNG: return &ffi_type_sint64; + case _C_ULNG_LNG: return &ffi_type_uint64; + case _C_FLT: return &ffi_type_float; + case _C_DBL: return &ffi_type_double; + case _C_BOOL: return &ffi_type_sint8; + case _C_VOID: return &ffi_type_void; + } + return NULL; +} + +// +// Type encodings +// http://developer.apple.com/mac/library/documentation/cocoa/conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html +// +// Will change a bit between 32 and 64 bits (NSUInteger I->Q, CGFloat f->d) +// +static NSMutableDictionary* typeEncodings = nil; ++ (NSString*)typeEncodingForType:(NSString*)encoding +{ + if (!typeEncodings) + { + typeEncodings = [NSMutableDictionary new]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(char)] forKey:@"char"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(int)] forKey:@"int"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(short)] forKey:@"short"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(long)] forKey:@"long"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(long long)] forKey:@"long long"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(unsigned char)] forKey:@"unsigned char"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(unsigned int)] forKey:@"unsigned int"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(unsigned short)] forKey:@"unsigned short"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(unsigned long)] forKey:@"unsigned long"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(unsigned long long)] forKey:@"unsigned long long"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(float)] forKey:@"float"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(double)] forKey:@"double"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(bool)] forKey:@"bool"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(void)] forKey:@"void"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(char*)] forKey:@"char*"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(id)] forKey:@"id"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(Class)] forKey:@"Class"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(SEL)] forKey:@"selector"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(BOOL)] forKey:@"BOOL"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(void*)] forKey:@"void*"]; + + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(NSInteger)] forKey:@"NSInteger"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(NSUInteger)] forKey:@"NSUInteger"]; + [typeEncodings setValue:[NSString stringWithUTF8String:@encode(CGFloat)] forKey:@"CGFloat"]; + } + return [typeEncodings valueForKey:encoding]; +} + + +#pragma mark Structure encoding, size + +/* + From + {_NSRect={_NSPoint=ff}{_NSSize=ff}} + + Return + {_NSRect="origin"{_NSPoint="x"f"y"f}"size"{_NSSize="width"f"height"f}} +*/ ++ (NSString*)structureNameFromStructureTypeEncoding:(NSString*)encoding +{ + // Extract structure name + // skip '{' + char* c = (char*)[encoding UTF8String]+1; + // skip '_' if it's there + if (*c == '_') c++; + char* c2 = c; + while (*c2 && *c2 != '=') c2++; + return [[[NSString alloc] initWithBytes:c length:(c2-c) encoding:NSUTF8StringEncoding] autorelease]; +} + ++ (NSMutableArray*)encodingsFromStructureTypeEncoding:(NSString*)encoding +{ + return nil; +} + ++ (NSString*)structureFullTypeEncodingFromStructureTypeEncoding:(NSString*)encoding +{ + id structureName = [JSCocoaFFIArgument structureNameFromStructureTypeEncoding:encoding]; + return [self structureFullTypeEncodingFromStructureName:structureName]; +} + ++ (NSString*)structureFullTypeEncodingFromStructureName:(NSString*)structureName +{ + // Fetch structure type encoding from BridgeSupport +// id xml = [[BridgeSupportController sharedController] query:structureName withType:@"struct"]; + id xml = [[BridgeSupportController sharedController] queryName:structureName type:@"struct"]; + + if (xml == nil) + { + NSLog(@"No structure encoding found for %@", structureName); + return nil; + } + id xmlDocument = [[NSXMLDocument alloc] initWithXMLString:xml options:0 error:nil]; + if (!xmlDocument) return NO; + id rootElement = [xmlDocument rootElement]; +#if __LP64__ + id type = [[rootElement attributeForName:@"type64"] stringValue]; +#else + id type = [[rootElement attributeForName:@"type"] stringValue]; +#endif + // Retain the string as releasing xmlDocument deallocs it + [[type retain] autorelease]; + + [xmlDocument release]; + return type; +} + + ++ (NSArray*)typeEncodingsFromStructureTypeEncoding:(NSString*)structureTypeEncoding +{ + return [self typeEncodingsFromStructureTypeEncoding:structureTypeEncoding parsedCount:nil]; +} + + ++ (NSArray*)typeEncodingsFromStructureTypeEncoding:(NSString*)structureTypeEncoding parsedCount:(NSInteger*)count +{ + id types = [[[NSMutableArray alloc] init] autorelease]; + char* c = (char*)[structureTypeEncoding UTF8String]; + char* c0 = c; + int openedBracesCount = 0; + int closedBracesCount = 0; + for (;*c; c++) + { + if (*c == '{') + { + openedBracesCount++; + while (*c && *c != '=') c++; + if (!*c) continue; + } + if (*c == '}') + { + closedBracesCount++; + + // If we parsed something (c>c0) and have an equal amount of opened and closed braces, we're done + if (c0 != c && openedBracesCount == closedBracesCount) + { + c++; + break; + } + continue; + } + if (*c == '=') continue; + + [types addObject:[NSString stringWithFormat:@"%c", *c]]; + + // Special case for pointers + if (*c == '^') + { + // Skip pointers to pointers (^^^) + while (*c && *c == '^') c++; + + // Skip type, special case for structure + if (*c == '{') + { + int openedBracesCount2 = 1; + int closedBracesCount2 = 0; + c++; + for (; *c && closedBracesCount2 != openedBracesCount2; c++) + { + if (*c == '{') openedBracesCount2++; + if (*c == '}') closedBracesCount2++; + } + c--; + } + else c++; + } + } + if (count) *count = c-c0; + if (closedBracesCount != openedBracesCount) return NSLog(@"Could not parse structure type encodings for %@", structureTypeEncoding), nil; + return types; +} + +// +// Given a structure encoding string, produce a human readable format +// ++ (NSInteger)structureTypeEncodingDescription:(NSString*)structureTypeEncoding inString:(NSMutableString**)str +{ + char* c = (char*)[structureTypeEncoding UTF8String]; + char* c0 = c; + // Skip '{' + c += 1; + // Skip '_' if it's there + if (*c == '_') c++; + // Skip structureName, '=' +// c += [private.structureName length]+1; + id structureName = [self structureNameFromStructureTypeEncoding:structureTypeEncoding]; + c += [structureName length]+1; + + int openedBracesCount = 1; + int closedBracesCount = 0; + int propertyCount = 0; + for (; *c && closedBracesCount != openedBracesCount; c++) + { + if (*c == '{') + { + [*str appendString:@"{"]; + openedBracesCount++; + } + if (*c == '}') + { + [*str appendString:@"}"]; + closedBracesCount++; + } + // Parse name then type + if (*c == '"') + { + propertyCount++; + if (propertyCount > 1) [*str appendString:@", "]; + char* c2 = c+1; + while (c2 && *c2 != '"') c2++; + id propertyName = [[[NSString alloc] initWithBytes:c+1 length:(c2-c-1) encoding:NSUTF8StringEncoding] autorelease]; + c = c2; + // Skip '"' + c++; + char encoding = *c; + [*str appendString:propertyName]; + [*str appendString:@": "]; + +// JSValueRef valueJS = NULL; + if (encoding == '{') + { + [*str appendString:@"{"]; + NSInteger parsed = [self structureTypeEncodingDescription:[NSString stringWithUTF8String:c] inString:str]; + c += parsed; +// NSLog(@"parsed %@ (%d)", substr, [substr length]); + } + else + { + [*str appendString:@"("]; + [*str appendString:[self typeDescriptionForTypeEncoding:encoding fullTypeEncoding:nil]]; + [*str appendString:@")"]; + } + } + } + return c-c0-1; +} ++ (NSString*)structureTypeEncodingDescription:(NSString*)structureTypeEncoding +{ + id fullStructureTypeEncoding = [self structureFullTypeEncodingFromStructureTypeEncoding:structureTypeEncoding]; + if (!fullStructureTypeEncoding) return [NSString stringWithFormat:@"(Could not describe struct %@)", structureTypeEncoding]; + + id str = [NSMutableString stringWithFormat:@"%@{", [self structureNameFromStructureTypeEncoding:fullStructureTypeEncoding]]; + [self structureTypeEncodingDescription:fullStructureTypeEncoding inString:&str]; + [str appendString:@"}"]; + return str; +} + + ++ (int)sizeOfStructure:(NSString*)encoding +{ + id types = [self typeEncodingsFromStructureTypeEncoding:encoding]; + int computedSize = 0; + void** ptr = (void**)&computedSize; + for (id type in types) + { + char charEncoding = *(char*)[type UTF8String]; + // Align + [JSCocoaFFIArgument alignPtr:ptr accordingToEncoding:charEncoding]; + // Advance ptr + [JSCocoaFFIArgument advancePtr:ptr accordingToEncoding:charEncoding]; + } + return computedSize; +} + + +#pragma mark Object boxing / unboxing + +// +// Box +// ++ (BOOL)boxObject:(id)objcObject toJSValueRef:(JSValueRef*)value inContext:(JSContextRef)ctx { + // Return null if our pointer is null + if (!objcObject) { + *value = JSValueMakeNull(ctx); + return YES; + } + // Use a global boxing function to always return the same Javascript object + // when requesting multiple boxings of the same ObjC object + *value = [[JSCocoa controllerFromContext:ctx] boxObject:objcObject]; + return YES; +} + +// +// Unbox +// ++ (BOOL)unboxJSValueRef:(JSValueRef)value toObject:(id*)o inContext:(JSContextRef)ctx +{ + // + // Boxing + // + // string -> NSString + // null -> nil (no box) + // number -> NSNumber + // [] -> NSMutableArray + // {} -> NSMutableDictionary + // + + // null + if (!value || JSValueIsNull(ctx, value) || JSValueIsUndefined(ctx, value)) + { + *(id*)o = nil; + return YES; + } + + + // string + if (JSValueIsString(ctx, value)) + { + JSStringRef resultStringJS = JSValueToStringCopy(ctx, value, NULL); + NSString* resultString = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, resultStringJS); +// NSLog(@"unboxed=%@", resultString); + JSStringRelease(resultStringJS); + [NSMakeCollectable(resultString) autorelease]; + *(id*)o = resultString; + return YES; + } + + + // number + if (JSValueIsNumber(ctx, value)) + { + double v = JSValueToNumber(ctx, value, NULL); + // Integer + if (fabs(round(v)-v) < 1e-6) + { + if (v < 0) + { + *(id*)o = [NSNumber numberWithInt:(int)v]; +// NSLog(@"int %d", (int)v); + } + else + { + *(id*)o = [NSNumber numberWithUnsignedInt:(unsigned int)v]; +// NSLog(@"UNSIGNED int %d", (unsigned int)v); + } + } + // Double + else + { + *(id*)o = [NSNumber numberWithDouble:v]; +// NSLog(@"double %f", v); + } + return YES; + } + + // bool + if (JSValueIsBoolean(ctx, value)) + { + bool v = JSValueToBoolean(ctx, value); + if (v) *(id*)o = [NSNumber numberWithBool:YES]; + else *(id*)o = nil; + return YES; + } + + // From here we must have a Javascript object (Array, Hash) or a boxed Cocoa object + if (!JSValueIsObject(ctx, value)) + return NO; + + JSObjectRef jsObject = JSValueToObject(ctx, value, NULL); + JSCocoaPrivateObject* private = JSObjectGetPrivate(jsObject); + // Pure js hashes and arrays are converted to NSArray and NSDictionary + if (!private) + { + // Use an anonymous function to test if object is Array or Object (hash) + // (can't use this.constructor==Array.prototype.constructor with JSEvaluateScript it doesn't take thisObject into account) + JSStringRef scriptJS = JSStringCreateWithUTF8CString("return arguments[0].constructor == Array.prototype.constructor"); + JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); + JSValueRef result = JSObjectCallAsFunction(ctx, fn, NULL, 1, (JSValueRef*)&jsObject, NULL); + JSStringRelease(scriptJS); + + BOOL isArray = JSValueToBoolean(ctx, result); + + if (isArray) return [self unboxJSArray:jsObject toObject:o inContext:ctx]; + else return [self unboxJSHash:jsObject toObject:o inContext:ctx]; + } + // ## Hmmm ? CGColorRef is returned as a pointer but CALayer.foregroundColor asks an objc object (@) +/* + if ([private.type isEqualToString:@"rawPointer"]) *(id*)o = [private rawPointer]; + else *(id*)o = [private object]; +*/ + + id obj = [private object]; + + if ([private.type isEqualToString:@"rawPointer"]) *(id*)o = [private rawPointer]; + else if (obj) *(id*)o = obj; + else if ([private.type isEqualToString:@"externalJSValueRef"]) + { + // Convert external jsValues by calling valueOf + JSValueRef v = valueOfCallback(ctx, NULL, JSValueToObject(ctx, value, NULL), 0, NULL, NULL); + return [self unboxJSValueRef:v toObject:o inContext:ctx]; + } + else + { +// NSLog(@"********* %@", private.type); + *(id*)o = nil; + } + + return YES; +} + +// +// Convert ['a', 'b', 1.23] to an NSArray +// ++ (BOOL)unboxJSArray:(JSObjectRef)object toObject:(id*)o inContext:(JSContextRef)ctx +{ + // Get property count + JSValueRef exception = NULL; + JSStringRef lengthJS = JSStringCreateWithUTF8CString("length"); + NSUInteger length = JSValueToNumber(ctx, JSObjectGetProperty(ctx, object, lengthJS, NULL), &exception); + JSStringRelease(lengthJS); + if (exception) return NO; + + // Converted array + id array = [NSMutableArray array]; + // Converted array property + id value; + int i; + // Loop over all properties of the array and call our trusty unboxer. + // He might reenter that function to convert arrays inside that array. + for (i=0; i +#import +#define MACOSX +#import +#endif +#import "JSCocoaFFIArgument.h" + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE +#import "iPhone/libffi/ffi.h" +#endif + + +@interface JSCocoaFFIClosure : NSObject { + + JSValueRef jsFunction; + // ##UNSURE This might cause a crash if we're registered in a non global context that will have been destroyed when we JSValueUnprotect the function + JSContextRef ctx; + + ffi_cif cif; +#if !TARGET_OS_IPHONE + ffi_closure* closure; +#endif + ffi_type** argTypes; + + NSMutableArray* encodings; + + JSObjectRef jsThisObject; + + BOOL isObjC; +} + +- (IMP)setJSFunction:(JSValueRef)fn inContext:(JSContextRef)ctx argumentEncodings:(NSMutableArray*)argumentEncodings objC:(BOOL)objC; +- (void*)functionPointer; +- (void)calledByClosureWithArgs:(void**)args returnValue:(void*)returnValue; + +@end diff --git a/JSCocoa/JSCocoaFFIClosure.m b/JSCocoa/JSCocoaFFIClosure.m new file mode 100644 index 000000000..378eab0f3 --- /dev/null +++ b/JSCocoa/JSCocoaFFIClosure.m @@ -0,0 +1,213 @@ +// +// JSCocoaFFIClosure.m +// JSCocoa +// +// Created by Patrick Geiller on 29/07/08. +// Copyright 2008 __MyCompanyName__. All rights reserved. +// + +#import "JSCocoaFFIClosure.h" +#import "JSCocoaController.h" +#include // for mmap() + +@implementation JSCocoaFFIClosure + + +// +// Common closure function, calling back closure object +// +void closure_function(ffi_cif* cif, void* resp, void** args, void* userdata) +{ + [(id)userdata calledByClosureWithArgs:args returnValue:resp]; +} + +- (id)init +{ + self = [super init]; + + argTypes = NULL; + encodings = NULL; + jsFunction = NULL; + + return self; +} + +// +// Cleanup : called by dealloc and finalize +// +- (void)cleanUp +{ + if (encodings) [encodings release]; + if (argTypes) free(argTypes); + + if (jsFunction) + { + JSValueUnprotect(ctx, jsFunction); + [JSCocoaController downJSValueProtectCount]; + } +#if !TARGET_OS_IPHONE + if (munmap(closure, sizeof(closure)) == -1) NSLog(@"ffi closure munmap failed"); +#endif +} +- (void)dealloc +{ +// NSLog(@"deallocing closure %p IMP=%p", self, closure); + [self cleanUp]; + [super dealloc]; +} +- (void)finalize +{ + [self cleanUp]; + [super finalize]; +} + +- (void*)functionPointer +{ +#if !TARGET_OS_IPHONE + return closure; +#else + return NULL; +#endif +} + + + +// +// Bind a js function to closure. We'll jsValueProtect that function from GC. +// +- (IMP)setJSFunction:(JSValueRef)fn inContext:(JSContextRef)context argumentEncodings:(NSMutableArray*)argumentEncodings objC:(BOOL)objC +{ +#if !TARGET_OS_IPHONE + if ([argumentEncodings count] == 0) return NULL; + + encodings = argumentEncodings; + [encodings retain]; + isObjC = objC; + + unsigned int i, argumentCount = (unsigned int)([argumentEncodings count]-1); + argTypes = malloc(sizeof(ffi_type*)*argumentCount); + for (i=0; i 0 && size < paddedSize && paddedSize == 4) + { + v = *(long*)returnValue; + v = CFSwapInt32(v); + *(long*)returnValue = v; + } +#endif + } + + if (effectiveArgumentCount) free(args); + if (exception) + { + @throw [NSException exceptionWithName:@"JSCocoa exception" + reason:[[JSCocoaController controllerFromContext:ctx] formatJSException:exception] + userInfo:nil]; + } +} + + + +@end diff --git a/JSCocoa/JSCocoaLib.h b/JSCocoa/JSCocoaLib.h new file mode 100644 index 000000000..e5c0b3082 --- /dev/null +++ b/JSCocoa/JSCocoaLib.h @@ -0,0 +1,98 @@ +// +// JSCocoaLib.h +// JSCocoa +// +// Created by Patrick Geiller on 21/12/08. +// Copyright 2008 __MyCompanyName__. All rights reserved. +// + +#if !TARGET_IPHONE_SIMULATOR && !TARGET_OS_IPHONE +#import +#endif +#import "JSCocoa.h" + +@class JSCocoaMemoryBuffer; + +@interface JSCocoaOutArgument : NSObject +{ + JSCocoaFFIArgument* arg; + JSCocoaMemoryBuffer* buffer; + int bufferIndex; +} +- (BOOL)mateWithJSCocoaFFIArgument:(JSCocoaFFIArgument*)arg; +- (JSValueRef)outJSValueRefInContext:(JSContextRef)ctx; + +@end + + + +@interface JSCocoaMemoryBuffer : NSObject +{ + void* buffer; + int bufferSize; + // NSString holding types + id typeString; + + // Indicates whether types are aligned. + // types not aligned (DEFAULT) + // size('fcf') = 4 + 1 + 4 = 9 + // types aligned + // size('fcf') = 4 + 4(align) + 4 = 12 + BOOL alignTypes; +} ++ (id)bufferWithTypes:(id)types; +- (id)initWithTypes:(id)types; +//- (id)initWithTypes:(id)types andValues:(id)values; +//- (id)initWithMemoryBuffers:(id)buffers; + +- (void*)pointerForIndex:(NSUInteger)index; +- (char)typeAtIndex:(NSUInteger)index; +- (JSValueRef)valueAtIndex:(NSUInteger)index inContext:(JSContextRef)ctx; +- (BOOL)setValue:(JSValueRef)jsValue atIndex:(NSUInteger)index inContext:(JSContextRef)ctx; +- (NSUInteger)typeCount; + +@end + + +@interface JSCocoaLib : NSObject + ++ (id)rootclasses; ++ (id)classes; ++ (id)protocols; ++ (id)imageNames; ++ (id)methods; ++ (id)runtimeReport; + +@end + + + +@interface NSObject(ClassWalker) ++ (id)__classImage; +- (id)__classImage; ++ (id)__derivationPath; +- (id)__derivationPath; ++ (NSUInteger)__derivationLevel; +- (NSUInteger)__derivationLevel; ++ (id)__ownMethods; +- (id)__ownMethods; ++ (id)__methods; +- (id)__methods; ++ (id)__subclasses; +- (id)__subclasses; ++ (id)__subclassTree; +- (id)__subclassTree; ++ (id)__ownIvars; +- (id)__ownIvars; ++ (id)__ivars; +- (id)__ivars; ++ (id)__ownProperties; +- (id)__ownProperties; ++ (id)__properties; +- (id)__properties; ++ (id)__ownProtocols; +- (id)__ownProtocols; ++ (id)__protocols; +- (id)__protocols; + +@end diff --git a/JSCocoa/JSCocoaLib.m b/JSCocoa/JSCocoaLib.m new file mode 100644 index 000000000..2af615ea9 --- /dev/null +++ b/JSCocoa/JSCocoaLib.m @@ -0,0 +1,830 @@ +// +// JSCocoaLib.m +// JSCocoa +// +// Created by Patrick Geiller on 21/12/08. +// Copyright 2008 __MyCompanyName__. All rights reserved. +// + +#import "JSCocoaLib.h" + + +// +// Handles out arguments of functions and methods. +// eg NSOpenGLGetVersion(int*, int*) asks for two pointers to int. +// JSCocoaOutArgument will alloc the memory through JSCocoaFFIArgument and get the result back to Javascript (check out value in JSCocoaController) +// +@implementation JSCocoaOutArgument + +- (id)init +{ + self = [super init]; + + arg = nil; + buffer = nil; + return self; +} +- (void)cleanUp +{ + [arg release]; + [buffer release]; +} +- (void)dealloc +{ + [self cleanUp]; + [super dealloc]; +} +- (void)finalize +{ + [self cleanUp]; + [super finalize]; +} + + +// +// convert the out value to a JSValue +// +- (JSValueRef)outJSValueRefInContext:(JSContextRef)ctx +{ + JSValueRef jsValue = NULL; + [arg toJSValueRef:&jsValue inContext:ctx]; + return jsValue; +} + +// Called from Javascript to extract the resulting value as an object (valueOfCallback returns a string) +- (JSValueRefAndContextRef)outValue +{ + JSValueRefAndContextRef r; + + id jsc = nil; + object_getInstanceVariable(self, "__jsCocoaController", (void**)&jsc); + if (!jsc) return r; + + r.ctx = [jsc ctx]; + r.value = [self outJSValueRefInContext:r.ctx]; + + return r; +} + + + +// +// JSCocoaOutArgument holds a JSCocoaFFIArgument around. +// it stays alive after ffi_call and can be queried by Javascript for type modifier values. +// +- (BOOL)mateWithJSCocoaFFIArgument:(JSCocoaFFIArgument*)_arg +{ + // If holding a memory buffer, use its pointer + if (buffer) + { + arg = _arg; + [arg retain]; + void* ptr = [buffer pointerForIndex:bufferIndex]; + if (!ptr) return NO; + [arg setTypeEncoding:[arg typeEncoding] withCustomStorage:ptr]; + return YES; + } + + // Standard pointer + void* p = [_arg allocatePointerStorage]; + if (!p) return NO; + + // Zero out storage + *(void**)p = NULL; + + + arg = _arg; + [arg retain]; + return YES; +} + +- (BOOL)mateWithMemoryBuffer:(id)b atIndex:(int)idx +{ + if (!b || ![b isKindOfClass:[JSCocoaMemoryBuffer class]]) return NSLog(@"mateWithMemoryBuffer called without a memory buffer (%@)", b), NO; + buffer = b; + [buffer retain]; + bufferIndex = idx; + return YES; +} + +@end + + + +// +// Instead of malloc(sizeof(float)*4), JSCocoaMemoryBuffer expects 'ffff' as an init string. +// The buffer can be manipulated like an array (buffer[2] = 0.5) +// * it can be filled, calling methods to copy data in it +// - (NSBezierPathElement)elementAtIndex:(NSInteger)index associatedPoints:(NSPointArray)points; +// * it can be used as data source, calling methods to copy data from it +// - (void)setAssociatedPoints:(NSPointArray)points atIndex:(NSInteger)index; +// +@implementation JSCocoaMemoryBuffer + ++ (id)bufferWithTypes:(id)types +{ + return [[[JSCocoaMemoryBuffer alloc] initWithTypes:types] autorelease]; +} + + +- (id)initWithTypes:(id)_types +{ + self = [super init]; + buffer = NULL; + + // Copy types string + typeString = [NSString stringWithString:_types]; + [typeString retain]; + + // Compute buffer size + const char* types = [typeString UTF8String]; + NSUInteger l = [typeString length]; + bufferSize = 0; + for (int i=0; i= [typeString length]) return NULL; + void* pointedValue = buffer; + for (int i=0; i= [typeString length]) return '\0'; + return [typeString UTF8String][idx]; +} + +- (NSUInteger)typeCount +{ + return [typeString length]; +} + +-(BOOL)referenceObject:(id)o usingPointerAtIndex:(NSUInteger)idx +{ + if ([self typeAtIndex:idx] != '^') return NO; + + void* v = *(void**)[self pointerForIndex:idx]; + if (!v) return NO; + *(id*)v = o; + return YES; +} + +- (id)dereferenceObjectAtIndex:(NSUInteger)idx +{ + if ([self typeAtIndex:idx] != '^') return nil; + void* v = *(void**)[self pointerForIndex:idx]; + if (!v) return NULL; + + id o = *(id*)v; + return o; + return *(id*)v; +} + +// +// Using JSValueRefAndContextRef as input to get the current context in which to create the return value +// +- (JSValueRef)valueAtIndex:(NSUInteger)idx inContext:(JSContextRef)ctx +{ + char typeEncoding = [self typeAtIndex:idx]; + void* pointedValue = [self pointerForIndex:idx]; + if (!pointedValue) return JSValueMakeUndefined(ctx); + JSValueRef returnValue; + [JSCocoaFFIArgument toJSValueRef:&returnValue inContext:ctx typeEncoding:typeEncoding fullTypeEncoding:nil fromStorage:pointedValue]; + return returnValue; +} + +- (BOOL)setValue:(JSValueRef)jsValue atIndex:(NSUInteger)idx inContext:(JSContextRef)ctx +{ + char typeEncoding = [self typeAtIndex:idx]; + void* pointedValue = [self pointerForIndex:idx]; + if (!pointedValue) return NO; + [JSCocoaFFIArgument fromJSValueRef:jsValue inContext:ctx typeEncoding:typeEncoding fullTypeEncoding:nil fromStorage:pointedValue]; + return YES; +} + + +@end + + + +@implementation JSCocoaLib + +// +// Class list +// Some classes are skipped as adding them to an array crashes (Zombie, classes derived from Object or NSProxy) +// ++ (NSArray*)classes +{ + int classCount = objc_getClassList(nil, 0); + Class* classList = malloc(sizeof(Class)*classCount); + objc_getClassList(classList, classCount); + + + NSMutableArray* classArray = [NSMutableArray array]; + for (int i=0; i +#import +#endif + +#import +#import +//#import +#import +#import + +// +// Boxing object +// +// type +// @ ObjC object +// struct C struct +// method ObjC method name +// function C function +// rawPointer raw C pointer (_C_PTR) +// jsFunction Javascript function +// jsValueRef raw jsvalue +// externalJSValueRef jsvalue coming from an external context (eg, a WebView) +// + +@interface JSCocoaPrivateObject : NSObject { + + NSString* type; + NSString* xml; + NSString* methodName; + NSString* structureName; + + NSString* declaredType; + void* rawPointer; + + id object; + + Method method; + + JSValueRef jsValue; + JSContextRef ctx; + unsigned int externalJSValueIndex; + // (test) when storing JSValues from a WebView, used to retain the WebView's context. + // Disabled for now. Just make sure the WebView has a longer life than the vars it uses. + // + // Disabled because retaining the context crashes in 32 bits, but works in 64 bit. + // May be reenabled someday. +// JSContextGroupRef contextGroup; + + BOOL isAutoCall; + BOOL retainObject; + // Disabled because of a crash on i386. Release globalContext last. +// BOOL retainContext; +} + +@property (copy) NSString* type; +@property (copy) NSString* xml; +@property (copy) NSString* methodName; +@property (copy) NSString* structureName; +@property (copy) NSString* declaredType; +@property BOOL isAutoCall; + +//- (void)setPtr:(void*)ptrValue; +//- (void*)ptr; + +- (void)setObject:(id)o; +- (void)setObjectNoRetain:(id)o; +- (BOOL)retainObject; +- (id)object; + +- (void)setMethod:(Method)m; +- (Method)method; + +- (void)setJSValueRef:(JSValueRef)v ctx:(JSContextRef)ctx; +- (JSValueRef)jsValueRef; +- (void)setCtx:(JSContextRef)ctx; +- (JSContextRef)ctx; +- (void)setExternalJSValueRef:(JSValueRef)v ctx:(JSContextRef)ctx; + +- (void*)rawPointer; +- (void)setRawPointer:(void*)rp encoding:(id)encoding; +- (id)rawPointerEncoding; + +@end diff --git a/JSCocoa/JSCocoaPrivateObject.m b/JSCocoa/JSCocoaPrivateObject.m new file mode 100644 index 000000000..7f4611b08 --- /dev/null +++ b/JSCocoa/JSCocoaPrivateObject.m @@ -0,0 +1,236 @@ +// +// JSCocoaPrivateObject.m +// JSCocoa +// +// Created by Patrick Geiller on 09/07/08. +// Copyright 2008 __MyCompanyName__. All rights reserved. +// + +#import "JSCocoaPrivateObject.h" +#import "JSCocoaController.h" + +@implementation JSCocoaPrivateObject + +@synthesize type, xml, declaredType, methodName, structureName, isAutoCall; + + +- (id)init +{ + self = [super init]; + type = xml = declaredType = methodName = nil; + object = nil; + isAutoCall = NO; + jsValue = NULL; + retainObject= YES; + rawPointer = NULL; + ctx = NULL; +// retainContext = NO; + externalJSValueIndex = 0; + + + [JSCocoaController upJSCocoaPrivateObjectCount]; + return self; +} + +- (void)cleanUp +{ + [JSCocoaController downJSCocoaPrivateObjectCount]; + if (object && retainObject) + { +// NSLog(@"commented downBoxedJSObjectCount"); +// [JSCocoaController downBoxedJSObjectCount:object]; +// NSLog(@"releasing %@(%d)", [object class], [object retainCount]); +// if ([object isKindOfClass:[JSCocoaController class]]) +// [object autorelease]; +// else + [object release]; + } + if (jsValue) + { + if (!externalJSValueIndex) JSValueUnprotect(ctx, jsValue); + [JSCocoaController downJSValueProtectCount]; + + // If holding a value from an external context, remove it from the GC-safe hash and release context. + if (externalJSValueIndex) + { + JSStringRef scriptJS = JSStringCreateWithUTF8CString("delete __gcprotect[arguments[0]]"); + JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); + JSStringRelease(scriptJS); + JSValueRef jsNumber = JSValueMakeNumber(ctx, externalJSValueIndex); + JSValueRef exception = NULL; + JSObjectCallAsFunction(ctx, fn, NULL, 1, (JSValueRef*)&jsNumber, &exception); +// JSGlobalContextRelease((JSGlobalContextRef)ctx); + + if (exception) + NSLog(@"Got an exception while trying to release externalJSValueRef %p of context %p", jsValue, ctx); + } + } +/* + if (retainContext) + { + NSLog(@"releasing %p", ctx); + JSContextGroupRelease(contextGroup); +// JSGlobalContextRelease((JSGlobalContextRef)ctx); + } +*/ + // Release properties + [type release]; + [xml release]; + [methodName release]; + [structureName release]; + [declaredType release]; +} + +- (void)dealloc +{ + [self cleanUp]; + [super dealloc]; +} +- (void)finalize +{ + [self cleanUp]; + [super finalize]; +} + +- (void)setObject:(id)o +{ +// if (object && retainObject) +// [object release]; + object = o; + if (object && [object retainCount] == -1) return; + [object retain]; +} + +- (void)setObjectNoRetain:(id)o +{ + object = o; + retainObject = NO; +} + +- (BOOL)retainObject +{ + return retainObject; +} + +- (id)object +{ + return object; +} + +- (void)setMethod:(Method)m +{ + method = m; +} +- (Method)method +{ + return method; +} + +- (void)setJSValueRef:(JSValueRef)v ctx:(JSContextRef)c +{ + // While autocalling we'll get a NULL value when boxing a void return type - just skip JSValueProtect + if (!v) + { +// NSLog(@"setJSValueRef: NULL value"); + jsValue = 0; + return; + } + jsValue = v; +// ctx = c; + // Register global context (this would crash the launcher as JSValueUnprotect was called on a destroyed context) + ctx = [[JSCocoaController controllerFromContext:c] ctx]; + JSValueProtect(ctx, jsValue); + [JSCocoaController upJSValueProtectCount]; +} +- (JSValueRef)jsValueRef +{ + return jsValue; +} + +- (void)setCtx:(JSContextRef)_ctx { + ctx = _ctx; +} +- (JSContextRef)ctx +{ + return ctx; +} + + +- (void)setExternalJSValueRef:(JSValueRef)v ctx:(JSContextRef)c +{ + if (!v) + { + jsValue = 0; + return; + } + jsValue = v; + ctx = c; + + // Register value in a global hash to protect it from GC. This sucks but JSValueProtect() fails. + JSStringRef scriptJS = JSStringCreateWithUTF8CString("if (!('__gcprotect' in this)) { __gcprotect = {}; __gcprotectidx = 1; } __gcprotect[__gcprotectidx] = arguments[0]; return __gcprotectidx++ "); + JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); + JSStringRelease(scriptJS); + + JSValueRef exception = NULL; + JSValueRef result = JSObjectCallAsFunction(ctx, fn, NULL, 1, (JSValueRef*)&jsValue, &exception); + if (exception) return; + + // Use hash index as key, will be used to remove value from hash upon deletion. + externalJSValueIndex = (unsigned int)JSValueToNumber(ctx, result, &exception); + if (exception) return; + +// JSGlobalContextRetain((JSGlobalContextRef)ctx); + [JSCocoaController upJSValueProtectCount]; +} + + +- (void*)rawPointer +{ + return rawPointer; +} +- (void)setRawPointer:(void*)rp encoding:(id)encoding +{ + rawPointer = rp; +// NSLog(@"RAWPOINTER=%@", encoding); + declaredType = encoding; + [declaredType retain]; +} + +- (id)rawPointerEncoding +{ + return declaredType; +} + + +- (id)description { + id extra = @""; + if ([type isEqualToString:@"rawPointer"]) + extra = [NSString stringWithFormat:@" rawPointer=%p declaredType=%@", rawPointer, declaredType]; + return [NSString stringWithFormat:@"<%@: %p holding %@%@>", + [self class], + self, + type, + extra + ]; +} + ++ (id)description { + return @""; +} + +- (id)dereferencedObject { + if (![type isEqualToString:@"rawPointer"] || !rawPointer) + return nil; + return *(void**)rawPointer; +} + +- (BOOL)referenceObject:(id)o { + if (![type isEqualToString:@"rawPointer"]) + return NO; + *(id*)rawPointer = o; + return YES; +} + + +@end + diff --git a/JSCocoa/class.js b/JSCocoa/class.js new file mode 100644 index 000000000..6c8bbf151 --- /dev/null +++ b/JSCocoa/class.js @@ -0,0 +1,1055 @@ + + // ObjC + var nil = null + var YES = true + var NO = false + + + if ('OSX' in this) + { + var JSCocoaController = OSX.JSCocoaController + var NSApp = null + } + + + function log(str) { __jsc__.log_('' + str) } + // This one is because I can't bring myself to not typing alert. + function alert(str) { log('********USE log(), not alert()*********'), log(str) } + + function dumpHash(o) { var str = ''; for (var i in o) str += i + '=' + o[i] + '\n'; return str } + + // + // ObjC type encodings + // http://developer.apple.com/mac/library/documentation/cocoa/conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html + // + // Used to write ObjC methods in Javascript + // + var types = ['char', 'int', 'short', 'long', 'long long', 'unsigned char', 'unsigned int', 'unsigned short', 'unsigned long', + 'unsigned long long', 'float', 'double', 'bool', 'void', 'char*', 'id', 'Class', 'selector', 'BOOL', 'void*', + 'NSInteger', 'NSUInteger', 'CGFloat']; + var encodings = {} + var l = types.length + // Encodings are architecture dependent and were encoded at compile time, retrieve them now + for (var i=0; i item + // -> id item + encoding = encoding.replace(/\s*<\s*\w+\s*>\s*/, '').toString() + + + // Structure arg +// if (encoding.indexOf(' ') != -1 && encoding.indexOf('*') == -1) + if (encoding.match(/struct \w+/)) + { + var structureName = encoding.split(' ')[1] + var structureEncoding = JSCocoaFFIArgument.structureFullTypeEncodingFromStructureName(structureName) + if (!structureEncoding) throw 'no encoding found for structure ' + structureName + + // + // Remove names of variables to keep only encodings + // + // {_NSPoint="x"f"y"f} + // becomes + // {_NSPoint=ff} + // +// JSCocoaController.log('*' + structureEncoding + '*' + String(String(structureEncoding).replace(/"[^"]+"/gi, "")) + '*') + return String(String(structureEncoding).replace(/"[^"]+"/gi, "")) + } + else + { + if (!(encoding in encodings)) + { + // Pointer to an ObjC object ? + var match = encoding.match(/^(\w+)\s*(\*+)$/) + if (match) + { + var className = match[1] + + // + // this[className]['class'] == this[className] + // can only work if each object is boxed only once : + // both expressions will return the same object, comparing one object to itself + // -> true + // + // BUT if both expressions each use their own box, comparison will come negative + // + var starCount = match[2].toString().length + if (className in this && this[className]['class'] == this[className]) + { + // ** is a pointer to class + return starCount > 1 ? '^@' : '@' + } + else + if (starCount == 1) + { + var rawEncoding = encoding.replace(/\*/, '') + rawEncoding = encodings[rawEncoding] + if (rawEncoding) return '^' + rawEncoding + } + } + // Structure ? + var structureEncoding = JSCocoaFFIArgument.structureFullTypeEncodingFromStructureName(encoding) + if (structureEncoding) return String(String(structureEncoding).replace(/"[^"]+"/gi, "")) + throw 'invalid encoding : "' + encoding + '"' + } + return encodings[encoding] + } + } + + function objc_encoding() + { + var encoding = objc_unary_encoding(arguments[0]) + encoding += '@:' + + for (var i=1; i + // + function describeStruct(o, level) + { + if (level == undefined) level = 0 + // Bail if structure contains a cycle + if (level > 100) return '' + + var str = '' + if (typeof(o) == 'object' || typeof(o) == 'function') + { + str += '{' + var elements = [] + for (var i in o) + elements.push(i + ':' + describeStruct(o[i], level+1)) + str += elements.join(', ') + str += '}' + } + else + str += o + + return str + } + + + // + // type o + // + function outArgument() + { + // Derive to store some javascript data in the internal hash + if (!('outArgument2' in this)) + JSCocoa.createClass_parentClass_('JSCocoaOutArgument2', 'JSCocoaOutArgument') + + var o = JSCocoaOutArgument2.instance + o.isOutArgument = true + if (arguments.length == 2) o.mateWithMemoryBuffer_atIndex_(arguments[0], arguments[1]) + + return o + } + + + function memoryBuffer(types) + { + var o = JSCocoaMemoryBuffer.instanceWithTypes(types) + o.isOutArgument = true + return o + } + + + // + // Dump the call stack with arguments.calle.caller (Called from JSCocoa) + // + // Eric Wendelin's Javascript stacktrace in any browser + // http://eriwen.com/javascript/js-stack-trace/ + // + function dumpCallStack() + { + var maxDumpDepth = 100 + var dumpDepth = 0 + var caller = arguments.callee.caller + // Skip ourselves + caller = caller.caller + + // Build call stack + var stack = [] + while (caller && dumpDepth < maxDumpDepth) + { + var fn = caller.toString() + var fname = fn.substring(fn.indexOf("function") + 9, fn.indexOf("(")) || "anonymous"; + var str = fname + if (caller.arguments.length) + { + str += ' (' + for (var i=0; i 0 && leftCount == rightCount) return { left : i, right : matchIndex } + } + return { left : -1, right : -1 } + } + + function trackRightFromOperator(stream, tokenIndex, operator) + { + var right = operator.right + var lastToken + while (right) + { + lastToken = right + right = right.rightParen || right.right + } + if (!lastToken) return -1 + var l = stream.length-1 + for (; tokenIndex' + transformed) + return transformed + } diff --git a/JSCocoa/jslint-jscocoa.js b/JSCocoa/jslint-jscocoa.js new file mode 100644 index 000000000..daa8f8361 --- /dev/null +++ b/JSCocoa/jslint-jscocoa.js @@ -0,0 +1,5686 @@ +// jslint.js +// 2009-05-06 + + +/* +Copyright (c) 2002 Douglas Crockford (www.JSLint.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/* + JSLINT is a global function. It takes two parameters. + + var myResult = JSLINT(source, option); + + The first parameter is either a string or an array of strings. If it is a + string, it will be split on '\n' or '\r'. If it is an array of strings, it + is assumed that each string represents one line. The source can be a + JavaScript text, or HTML text, or a Konfabulator text. + + The second parameter is an optional object of options which control the + operation of JSLINT. Most of the options are booleans: They are all are + optional and have a default value of false. + + If it checks out, JSLINT returns true. Otherwise, it returns false. + + If false, you can inspect JSLINT.errors to find out the problems. + JSLINT.errors is an array of objects containing these members: + + { + line : The line (relative to 0) at which the lint was found + character : The character (relative to 0) at which the lint was found + reason : The problem + evidence : The text line in which the problem occurred + raw : The raw message before the details were inserted + a : The first detail + b : The second detail + c : The third detail + d : The fourth detail + } + + If a fatal error was found, a null will be the last element of the + JSLINT.errors array. + + You can request a Function Report, which shows all of the functions + and the parameters and vars that they use. This can be used to find + implied global variables and other problems. The report is in HTML and + can be inserted in an HTML . + + var myReport = JSLINT.report(limited); + + If limited is true, then the report will be limited to only errors. +*/ + +/*jslint + evil: true, nomen: false, onevar: false, regexp: false , strict: true +*/ + +/*global JSLINT*/ + +/*members "\b", "\t", "\n", "\f", "\r", "\"", "%", "(begin)", + "(breakage)", "(context)", "(error)", "(global)", "(identifier)", + "(line)", "(loopage)", "(name)", "(onevar)", "(params)", "(scope)", + "(verb)", ")", "++", "--", "\/", ADSAFE, Array, Boolean, COM, Canvas, + CustomAnimation, Date, Debug, E, Error, EvalError, FadeAnimation, + Flash, FormField, Frame, Function, HotKey, Image, JSON, LN10, LN2, LOG10E, + LOG2E, MAX_VALUE, MIN_VALUE, Math, MenuItem, MoveAnimation, + NEGATIVE_INFINITY, Number, Object, Option, PI, POSITIVE_INFINITY, Point, + RangeError, Rectangle, ReferenceError, RegExp, ResizeAnimation, + RotateAnimation, SQRT1_2, SQRT2, ScrollBar, String, Style, SyntaxError, + System, Text, TextArea, Timer, TypeError, URIError, URL, Web, Window, + XMLDOM, XMLHttpRequest, "\\", "]", a, abbr, acronym, active, address, + adsafe, after, alert, aliceblue, animator, antiquewhite, appleScript, + applet, apply, approved, aqua, aquamarine, area, arguments, arity, + autocomplete, azure, b, background, "background-attachment", + "background-color", "background-image", "background-position", + "background-repeat", base, bdo, beep, before, beige, big, bisque, + bitwise, black, blanchedalmond, block, blockquote, blue, blueviolet, + blur, body, border, "border-bottom", "border-bottom-color", + "border-bottom-style", "border-bottom-width", "border-collapse", + "border-color", "border-left", "border-left-color", "border-left-style", + "border-left-width", "border-right", "border-right-color", + "border-right-style", "border-right-width", "border-spacing", + "border-style", "border-top", "border-top-color", "border-top-style", + "border-top-width", "border-width", bottom, br, brown, browser, + burlywood, button, bytesToUIString, c, cadetblue, call, callee, caller, + canvas, cap, caption, "caption-side", cases, center, charAt, charCodeAt, + character, chartreuse, chocolate, chooseColor, chooseFile, chooseFolder, + cite, clear, clearInterval, clearTimeout, clip, close, closeWidget, + closed, cm, code, col, colgroup, color, comment, condition, confirm, + console, constructor, content, convertPathToHFS, convertPathToPlatform, + coral, cornflowerblue, cornsilk, "counter-increment", "counter-reset", + create, crimson, css, cursor, cyan, d, darkblue, darkcyan, + darkgoldenrod, darkgray, darkgreen, darkkhaki, darkmagenta, + darkolivegreen, darkorange, darkorchid, darkred, darksalmon, + darkseagreen, darkslateblue, darkslategray, darkturquoise, darkviolet, + dd, debug, decodeURI, decodeURIComponent, deeppink, deepskyblue, + defaultStatus, defineClass, del, deserialize, dfn, dimgray, dir, + direction, display, div, dl, document, dodgerblue, dt, edition, else, em, + embed, empty, "empty-cells", encodeURI, encodeURIComponent, entityify, + eqeqeq, errors, escape, eval, event, evidence, evil, ex, exec, exps, + fieldset, filesystem, firebrick, first, "first-child", "first-letter", + "first-line", float, floor, floralwhite, focus, focusWidget, font, + "font-face", "font-family", "font-size", "font-size-adjust", + "font-stretch", "font-style", "font-variant", "font-weight", + forestgreen, forin, form, fragment, frame, frames, frameset, from, + fromCharCode, fuchsia, fud, funct, function, g, gainsboro, gc, + getComputedStyle, ghostwhite, gold, goldenrod, gray, green, greenyellow, + h1, h2, h3, h4, h5, h6, hasOwnProperty, head, height, help, history, + honeydew, hotpink, hover, hr, html, i, iTunes, id, identifier, iframe, + img, immed, import, in, include, indent, indexOf, indianred, indigo, + init, input, ins, isAlpha, isApplicationRunning, isDigit, isFinite, + isNaN, ivory, join, kbd, khaki, konfabulatorVersion, label, labelled, + lang, lavender, lavenderblush, lawngreen, laxbreak, lbp, led, left, + legend, lemonchiffon, length, "letter-spacing", li, lib, lightblue, + lightcoral, lightcyan, lightgoldenrodyellow, lightgreen, lightpink, + lightsalmon, lightseagreen, lightskyblue, lightslategray, + lightsteelblue, lightyellow, lime, limegreen, line, "line-height", + linen, link, "list-style", "list-style-image", "list-style-position", + "list-style-type", load, loadClass, location, log, m, magenta, map, + margin, "margin-bottom", "margin-left", "margin-right", "margin-top", + "marker-offset", maroon, match, "max-height", "max-width", md5, media, + mediumaquamarine, mediumblue, mediumorchid, mediumpurple, + mediumseagreen, mediumslateblue, mediumspringgreen, mediumturquoise, + mediumvioletred, menu, message, meta, midnightblue, "min-height", + "min-width", mintcream, mistyrose, mm, moccasin, moveBy, moveTo, name, + navajowhite, navigator, navy, new, newcap, noframes, nomen, noscript, + nud, object, ol, oldlace, olive, olivedrab, on, onblur, onerror, onevar, + onfocus, onload, onresize, onunload, opacity, open, openURL, opener, + opera, optgroup, option, orange, orangered, orchid, outer, outline, + "outline-color", "outline-style", "outline-width", overflow, p, padding, + "padding-bottom", "padding-left", "padding-right", "padding-top", page, + palegoldenrod, palegreen, paleturquoise, palevioletred, papayawhip, + param, parent, parseFloat, parseInt, passfail, pc, peachpuff, peru, + pink, play, plum, plusplus, pop, popupMenu, position, powderblue, pre, + predef, preferenceGroups, preferences, print, prompt, prototype, pt, + purple, push, px, q, quit, quotes, random, range, raw, reach, readFile, + readUrl, reason, red, regexp, reloadWidget, replace, report, reserved, + resizeBy, resizeTo, resolvePath, resumeUpdates, rhino, right, rosybrown, + royalblue, runCommand, runCommandInBg, saddlebrown, safe, salmon, samp, + sandybrown, saveAs, savePreferences, screen, script, scroll, scrollBy, + scrollTo, seagreen, seal, search, seashell, select, self, serialize, + setInterval, setTimeout, shift, showWidgetPreferences, sidebar, sienna, + silver, skyblue, slateblue, slategray, sleep, slice, small, snow, sort, + span, spawn, speak, split, springgreen, src, status, steelblue, strict, + strong, style, styleproperty, sub, substr, sup, supplant, + suppressUpdates, sync, system, table, "table-layout", tan, tbody, td, + teal, tellWidget, test, "text-align", "text-decoration", "text-indent", + "text-shadow", "text-transform", textarea, tfoot, th, thead, thistle, + title, toLowerCase, toString, toUpperCase, toint32, token, tomato, top, + tr, tt, turquoise, type, u, ul, undef, unescape, "unicode-bidi", + unwatch, updateNow, value, valueOf, var, version, "vertical-align", + violet, visibility, visited, watch, wheat, white, "white-space", + whitesmoke, widget, width, window, "word-spacing", yahooCheckLogin, + yahooLogin, yahooLogout, yellow, yellowgreen, "z-index" +*/ + +// We build the application inside a function so that we produce only a single +// global variable. The function will be invoked, its return value is the JSLINT +// application itself. + +// http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/ +"use strict"; + +function JSLintWithLogs(logs) +{ + var JSLINT + + var fn = function () {} + var logToken = logs.logToken || fn + var logParseStart = logs.logParseStart || fn + var logParseEnd = logs.logParseEnd || fn + var logFunctionStart= logs.logFunctionStart || fn + var logStatement = logs.logStatement || fn + var logFunctionEnd = logs.logFunctionEnd || fn + + // Things we need to replace + var logExtraSyntax = logs.logExtraSyntax || fn + + var logTokenLock = 0 + function disableLogToken() { logTokenLock++ } + function enableLogToken() { logTokenLock-- } + + +JSLINT = (function renamedJSLint () { + + // ## (internal) Guard against inner classes definitions + var parsingClass; + + var adsafe_id, // The widget's ADsafe id. + adsafe_may, // The widget may load approved scripts. + adsafe_went, // ADSAFE.go has been called. + anonname, // The guessed name for anonymous functions. + approved, // ADsafe approved urls. + + atrule = { + 'import' : true, + media : true, + 'font-face': true, + page : true + }, + +// These are members that should not be permitted in third party ads. + + banned = { // the member names that ADsafe prohibits. + apply : true, + 'arguments' : true, + call : true, + callee : true, + caller : true, + constructor : true, + 'eval' : true, + prototype : true, + unwatch : true, + valueOf : true, + watch : true + }, + + +// These are the JSLint boolean options. + + boolOptions = { + adsafe : true, // if ADsafe should be enforced + bitwise : true, // if bitwise operators should not be allowed + browser : true, // if the standard browser globals should be predefined + cap : true, // if upper case HTML should be allowed + css : true, // if CSS workarounds should be tolerated + debug : true, // if debugger statements should be allowed + eqeqeq : true, // if === should be required + evil : true, // if eval should be allowed + forin : true, // if for in statements must filter + fragment : true, // if HTML fragments should be allowed + immed : true, // if immediate invocations must be wrapped in parens + laxbreak : true, // if line breaks should not be checked + newcap : true, // if constructor names must be capitalized + nomen : true, // if names should be checked + on : true, // if HTML event handlers should be allowed + onevar : true, // if only one var statement per function should be allowed + passfail : true, // if the scan should stop on first error + plusplus : true, // if increment/decrement should not be allowed + regexp : true, // if the . should not be allowed in regexp literals + rhino : true, // if the Rhino environment globals should be predefined + undef : true, // if variables should be declared before used + safe : true, // if use of some browser features should be restricted + sidebar : true, // if the System object should be predefined + strict : true, // require the "use strict"; pragma + sub : true, // if all forms of subscript notation are tolerated + white : true, // if strict whitespace rules apply + widget : true // if the Yahoo Widgets globals should be predefined + }, + +// browser contains a set of global names which are commonly provided by a +// web browser environment. + + browser = { + alert : true, + blur : true, + clearInterval : true, + clearTimeout : true, + close : true, + closed : true, + confirm : true, + console : true, + Debug : true, + defaultStatus : true, + document : true, + event : true, + focus : true, + frames : true, + getComputedStyle: true, + history : true, + Image : true, + length : true, + location : true, + moveBy : true, + moveTo : true, + name : true, + navigator : true, + onblur : true, + onerror : true, + onfocus : true, + onload : true, + onresize : true, + onunload : true, + open : true, + opener : true, + opera : true, + Option : true, + parent : true, + print : true, + prompt : true, + resizeBy : true, + resizeTo : true, + screen : true, + scroll : true, + scrollBy : true, + scrollTo : true, + setInterval : true, + setTimeout : true, + status : true, + top : true, + XMLHttpRequest : true + }, + + cssAttributeData, + cssAny, + + cssColorData = { + "aliceblue" : true, + "antiquewhite" : true, + "aqua" : true, + "aquamarine" : true, + "azure" : true, + "beige" : true, + "bisque" : true, + "black" : true, + "blanchedalmond" : true, + "blue" : true, + "blueviolet" : true, + "brown" : true, + "burlywood" : true, + "cadetblue" : true, + "chartreuse" : true, + "chocolate" : true, + "coral" : true, + "cornflowerblue" : true, + "cornsilk" : true, + "crimson" : true, + "cyan" : true, + "darkblue" : true, + "darkcyan" : true, + "darkgoldenrod" : true, + "darkgray" : true, + "darkgreen" : true, + "darkkhaki" : true, + "darkmagenta" : true, + "darkolivegreen" : true, + "darkorange" : true, + "darkorchid" : true, + "darkred" : true, + "darksalmon" : true, + "darkseagreen" : true, + "darkslateblue" : true, + "darkslategray" : true, + "darkturquoise" : true, + "darkviolet" : true, + "deeppink" : true, + "deepskyblue" : true, + "dimgray" : true, + "dodgerblue" : true, + "firebrick" : true, + "floralwhite" : true, + "forestgreen" : true, + "fuchsia" : true, + "gainsboro" : true, + "ghostwhite" : true, + "gold" : true, + "goldenrod" : true, + "gray" : true, + "green" : true, + "greenyellow" : true, + "honeydew" : true, + "hotpink" : true, + "indianred" : true, + "indigo" : true, + "ivory" : true, + "khaki" : true, + "lavender" : true, + "lavenderblush" : true, + "lawngreen" : true, + "lemonchiffon" : true, + "lightblue" : true, + "lightcoral" : true, + "lightcyan" : true, + "lightgoldenrodyellow" : true, + "lightgreen" : true, + "lightpink" : true, + "lightsalmon" : true, + "lightseagreen" : true, + "lightskyblue" : true, + "lightslategray" : true, + "lightsteelblue" : true, + "lightyellow" : true, + "lime" : true, + "limegreen" : true, + "linen" : true, + "magenta" : true, + "maroon" : true, + "mediumaquamarine" : true, + "mediumblue" : true, + "mediumorchid" : true, + "mediumpurple" : true, + "mediumseagreen" : true, + "mediumslateblue" : true, + "mediumspringgreen" : true, + "mediumturquoise" : true, + "mediumvioletred" : true, + "midnightblue" : true, + "mintcream" : true, + "mistyrose" : true, + "moccasin" : true, + "navajowhite" : true, + "navy" : true, + "oldlace" : true, + "olive" : true, + "olivedrab" : true, + "orange" : true, + "orangered" : true, + "orchid" : true, + "palegoldenrod" : true, + "palegreen" : true, + "paleturquoise" : true, + "palevioletred" : true, + "papayawhip" : true, + "peachpuff" : true, + "peru" : true, + "pink" : true, + "plum" : true, + "powderblue" : true, + "purple" : true, + "red" : true, + "rosybrown" : true, + "royalblue" : true, + "saddlebrown" : true, + "salmon" : true, + "sandybrown" : true, + "seagreen" : true, + "seashell" : true, + "sienna" : true, + "silver" : true, + "skyblue" : true, + "slateblue" : true, + "slategray" : true, + "snow" : true, + "springgreen" : true, + "steelblue" : true, + "tan" : true, + "teal" : true, + "thistle" : true, + "tomato" : true, + "turquoise" : true, + "violet" : true, + "wheat" : true, + "white" : true, + "whitesmoke" : true, + "yellow" : true, + "yellowgreen" : true + }, + + cssBorderStyle, + + cssLengthData = { + '%': true, + 'cm': true, + 'em': true, + 'ex': true, + 'in': true, + 'mm': true, + 'pc': true, + 'pt': true, + 'px': true + }, + + escapes = { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '/' : '\\/', + '\\': '\\\\' + }, + + funct, // The current function + functions, // All of the functions + + global, // The global scope + htmltag = { + a: {}, + abbr: {}, + acronym: {}, + address: {}, + applet: {}, + area: {empty: true, parent: ' map '}, + b: {}, + base: {empty: true, parent: ' head '}, + bdo: {}, + big: {}, + blockquote: {}, + body: {parent: ' html noframes '}, + br: {empty: true}, + button: {}, + canvas: {parent: ' body p div th td '}, + caption: {parent: ' table '}, + center: {}, + cite: {}, + code: {}, + col: {empty: true, parent: ' table colgroup '}, + colgroup: {parent: ' table '}, + dd: {parent: ' dl '}, + del: {}, + dfn: {}, + dir: {}, + div: {}, + dl: {}, + dt: {parent: ' dl '}, + em: {}, + embed: {}, + fieldset: {}, + font: {}, + form: {}, + frame: {empty: true, parent: ' frameset '}, + frameset: {parent: ' html frameset '}, + h1: {}, + h2: {}, + h3: {}, + h4: {}, + h5: {}, + h6: {}, + head: {parent: ' html '}, + html: {parent: '*'}, + hr: {empty: true}, + i: {}, + iframe: {}, + img: {empty: true}, + input: {empty: true}, + ins: {}, + kbd: {}, + label: {}, + legend: {parent: ' fieldset '}, + li: {parent: ' dir menu ol ul '}, + link: {empty: true, parent: ' head '}, + map: {}, + menu: {}, + meta: {empty: true, parent: ' head noframes noscript '}, + noframes: {parent: ' html body '}, + noscript: {parent: ' body head noframes '}, + object: {}, + ol: {}, + optgroup: {parent: ' select '}, + option: {parent: ' optgroup select '}, + p: {}, + param: {empty: true, parent: ' applet object '}, + pre: {}, + q: {}, + samp: {}, + script: {empty: true, parent: ' body div frame head iframe p pre span '}, + select: {}, + small: {}, + span: {}, + strong: {}, + style: {parent: ' head ', empty: true}, + sub: {}, + sup: {}, + table: {}, + tbody: {parent: ' table '}, + td: {parent: ' tr '}, + textarea: {}, + tfoot: {parent: ' table '}, + th: {parent: ' tr '}, + thead: {parent: ' table '}, + title: {parent: ' head '}, + tr: {parent: ' table tbody thead tfoot '}, + tt: {}, + u: {}, + ul: {}, + 'var': {} + }, + + ids, // HTML ids + implied, // Implied globals + inblock, + indent, + jsonmode, + lines, + lookahead, + member, + membersOnly, + nexttoken, + noreach, + option, + predefined, // Global variables defined by option + prereg, + prevtoken, + + pseudorule = { + 'first-child': true, + link : true, + visited : true, + hover : true, + active : true, + focus : true, + lang : true, + 'first-letter' : true, + 'first-line' : true, + before : true, + after : true + }, + + rhino = { + defineClass : true, + deserialize : true, + gc : true, + help : true, + load : true, + loadClass : true, + print : true, + quit : true, + readFile : true, + readUrl : true, + runCommand : true, + seal : true, + serialize : true, + spawn : true, + sync : true, + toint32 : true, + version : true + }, + + scope, // The current scope + + sidebar = { + System : true + }, + + src, + stack, + +// standard contains the global names that are provided by the +// ECMAScript standard. + + standard = { + Array : true, + Boolean : true, + Date : true, + decodeURI : true, + decodeURIComponent : true, + encodeURI : true, + encodeURIComponent : true, + Error : true, + 'eval' : true, + EvalError : true, + Function : true, + isFinite : true, + isNaN : true, + JSON : true, + Math : true, + Number : true, + Object : true, + parseInt : true, + parseFloat : true, + RangeError : true, + ReferenceError : true, + RegExp : true, + String : true, + SyntaxError : true, + TypeError : true, + URIError : true + }, + + standard_member = { + E : true, + LN2 : true, + LN10 : true, + LOG2E : true, + LOG10E : true, + PI : true, + SQRT1_2 : true, + SQRT2 : true, + MAX_VALUE : true, + MIN_VALUE : true, + NEGATIVE_INFINITY : true, + POSITIVE_INFINITY : true + }, + + syntax = {}, + tab, + token, + urls, + warnings, + +// widget contains the global names which are provided to a Yahoo +// (fna Konfabulator) widget. + + widget = { + alert : true, + animator : true, + appleScript : true, + beep : true, + bytesToUIString : true, + Canvas : true, + chooseColor : true, + chooseFile : true, + chooseFolder : true, + closeWidget : true, + COM : true, + convertPathToHFS : true, + convertPathToPlatform : true, + CustomAnimation : true, + escape : true, + FadeAnimation : true, + filesystem : true, + Flash : true, + focusWidget : true, + form : true, + FormField : true, + Frame : true, + HotKey : true, + Image : true, + include : true, + isApplicationRunning : true, + iTunes : true, + konfabulatorVersion : true, + log : true, + md5 : true, + MenuItem : true, + MoveAnimation : true, + openURL : true, + play : true, + Point : true, + popupMenu : true, + preferenceGroups : true, + preferences : true, + print : true, + prompt : true, + random : true, + Rectangle : true, + reloadWidget : true, + ResizeAnimation : true, + resolvePath : true, + resumeUpdates : true, + RotateAnimation : true, + runCommand : true, + runCommandInBg : true, + saveAs : true, + savePreferences : true, + screen : true, + ScrollBar : true, + showWidgetPreferences : true, + sleep : true, + speak : true, + Style : true, + suppressUpdates : true, + system : true, + tellWidget : true, + Text : true, + TextArea : true, + Timer : true, + unescape : true, + updateNow : true, + URL : true, + Web : true, + widget : true, + Window : true, + XMLDOM : true, + XMLHttpRequest : true, + yahooCheckLogin : true, + yahooLogin : true, + yahooLogout : true + }, + +// xmode is used to adapt to the exceptions in html parsing. +// It can have these states: +// false .js script file +// html +// outer +// script +// style +// scriptstring +// styleproperty + + xmode, + xquote, + +// unsafe comment or string + ax = /@cc|<\/?|script|\]*s\]|<\s*!|</i, +// unsafe characters that are silently deleted by one or more browsers + cx = /[\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/, +// token +// tx = /^\s*([(){}\[.,:;'"~\?\]#@]|==?=?|\/(\*(global|extern|jslint|member|members)?|=|\/)?|\*[\/=]?|\+[+=]?|-[\-=]?|%=?|&[&=]?|\|[|=]?|>>?>?=?|<([\/=!]|\!(\[|--)?|<=?)?|\^=?|\!=?=?|[a-zA-Z_$][a-zA-Z0-9_$]*|[0-9]+([xX][0-9a-fA-F]+|\.[0-9]*)?([eE][+\-]?[0-9]+)?)/, + // ## Allow unicode identifiers as characters having a charcode > 0xc0. + // That's too broad. Maybe check if eval('var supposedIdentifier') throws ? + // added ext >?>?=?|<([\/=!]|\!(\[|--)?|<=?)?|\^=?|\!=?=?|[a-zA-Z\u00c0-\uffff_$][a-zA-Z0-9\u00c0-\uffff_$]*|[0-9]+([xX][0-9a-fA-F]+|\.[0-9]*)?([eE][+\-]?[0-9]+)?)/, + + tx = function () + { +// var a = /^\s*([(){}\[.,:;'"~\?\]#@]|==?=?|\/(\*(global|extern|jslint|member|members)?|=|\/)?|\*[\/=]?|\+[+=]?|-[\-=]?|%=?|&[&=]?|\|[|=]?|>>?>?=?|<([\/=!]|\!(\[|--)?|<=?)?|\^=?|\!=?=?|[a-zA-Z\u00c0-\uffff_$][a-zA-Z0-9\u00c0-\uffff_$]*|[0-9]+([xX][0-9a-fA-F]+|\.[0-9]*)?([eE][+\-]?[0-9]+)?)/ +// var r = "^\\s*([()}.,:;'\"~\\?\\]#@]|\\[\\+\\]|\\[|{\\+}|@implementation|@end|@selector|@|ƒ|{|==?=?|\\/(\\*(global|extern|jslint|member|members)?|=|\\/)?|\\*[\\/=]?|\\+[+=]?|-[\\-=]?|%=?|&[&=]?|\\|[|=]?|>>?>?=?|<([\\/=!]|<=?)?|\\^=?|\\!=?=?|[a-zA-Z\\u00c0-\\uffff_$][a-zA-Z0-9\\u00c0-\\uffff_$]*|[0-9]+([xX][0-9a-fA-F]+|\\.[0-9]*)?([eE][+\\-]?[0-9]+)?)" + var r = "^\\s*([()}.,:;'\"~\\?\\]#]|\\[\\+\\]|\\[|{\\+}|@=|@implementation|@end|@selector|@|ƒ|{|==?=?|\\/(\\*|=|\\/)?|\\*[\\/=]?|\\+[+=]?|-[\\-=]?|%=?|&[&=]?|\\|[|=]?|>>?>?=?|<([\\/=!]|<=?)?|\\^=?|\\!=?=?|[a-zA-Z\\u00c0-\\uffff_$][a-zA-Z0-9\\u00c0-\\uffff_$]*|[0-9]+([xX][0-9a-fA-F]+|\\.[0-9]*)?([eE][+\\-]?[0-9]+)?)" + return new RegExp(r) + + }(), +/* + tx = +^\s*([(){}\[.,:;'"~\?\]#@] +==?=? +\/(\*(global +extern +jslint +member +members)? += +\/)? +\*[\/=]? +\+[+=]? +-[\-=]? +%=? +&[&=]? +\ +[ +=]? +>>?>?=? +<([\/=!] +\!(\[ +--)? +<=?)? +\^=? +\!=?=? +[a-zA-Z\u00c0-\uffff_$][a-zA-Z0-9\u00c0-\uffff_$]* +[0-9]+([xX][0-9a-fA-F]+ +\.[0-9]*)?([eE][+\-]?[0-9]+)?) + +*/ + +// html token + hx = /^\s*(['"=>\/&#]|<(?:\/|\!(?:--)?)?|[a-zA-Z][a-zA-Z0-9_\-]*|[0-9]+|--|.)/, +// characters in strings that need escapement + nx = /[\u0000-\u001f&<"\/\\\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/, + nxg = /[\u0000-\u001f&<"\/\\\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, +// outer html token + ox = /[>&]|<[\/!]?|--/, +// star slash + lx = /\*\/|\/\*/, +// identifier +// ix = /^([a-zA-Z_$][a-zA-Z0-9_$]*)$/, + // ## unicode identifier grossfix + ix = /^([A-Z_$\u00c0-\uffff][a-zA-Z0-9_$]*)$/, +// javascript url + jx = /^(?:javascript|jscript|ecmascript|vbscript|mocha|livescript)\s*:/i, +// url badness + ux = /&|\+|\u00AD|\.\.|\/\*|%[^;]|base64|url|expression|data|mailto/i, +// style + sx = /^\s*([{:#*%.=,>+\[\]@()"';*]|[a-zA-Z0-9_][a-zA-Z0-9_\-]*|<\/|\/\*)/, + ssx = /^\s*([@#!"'};:\-%.=,+\[\]()*_]|[a-zA-Z][a-zA-Z0-9._\-]*|\/\*?|\d+(?:\.\d+)?|<\/)/, +// attributes characters + qx = /[^a-zA-Z0-9-_\/ ]/, +// query characters for ids + dx = /[\[\]\/\\"'*<>.&:(){}+=#]/, + + rx = { + outer: hx, + html: hx, + style: sx, + styleproperty: ssx + }; + + function F() {} + + if (typeof Object.create !== 'function') { + Object.create = function (o) { + F.prototype = o; + return new F(); + }; + } + + function combine(t, o) { + var n; + for (n in o) { + if (o.hasOwnProperty(n)) { + t[n] = o[n]; + } + } + } + + String.prototype.entityify = function () { + return this. + replace(/&/g, '&'). + replace(//g, '>'); + }; + + String.prototype.isAlpha = function () { + return (this >= 'a' && this <= 'z\uffff') || + (this >= 'A' && this <= 'Z\uffff'); + }; + + + String.prototype.isDigit = function () { + return (this >= '0' && this <= '9'); + }; + + + String.prototype.supplant = function (o) { + return this.replace(/\{([^{}]*)\}/g, function (a, b) { + var r = o[b]; + return typeof r === 'string' || typeof r === 'number' ? r : a; + }); + }; + + String.prototype.name = function () { + +// If the string looks like an identifier, then we can return it as is. +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can simply slap some quotes around it. +// Otherwise we must also replace the offending characters with safe +// sequences. + + if (ix.test(this)) { + return this; + } + if (nx.test(this)) { + return '"' + this.replace(nxg, function (a) { + var c = escapes[a]; + if (c) { + return c; + } + return '\\u' + ('0000' + a.charCodeAt().toString(16)).slice(-4); + }) + '"'; + } + return '"' + this + '"'; + }; + + + function assume() { + if (!option.safe) { + if (option.rhino) { + combine(predefined, rhino); + } + if (option.browser || option.sidebar) { + combine(predefined, browser); + } + if (option.sidebar) { + combine(predefined, sidebar); + } + if (option.widget) { + combine(predefined, widget); + } + } + } + + +// Produce an error warning. + + function quit(m, l, ch) { + throw { + name: 'JSLintError', + line: l, + character: ch, + message: m + " (" + Math.floor((l / lines.length) * 100) + + "% scanned)." + }; + } + + function warning(m, t, a, b, c, d) { + var ch, l, w; + t = t || nexttoken; + if (t.id === '(end)') { // `~ + t = token; + } + l = t.line || 0; + ch = t.from || 0; + w = { + id: '(error)', + raw: m, + evidence: lines[l] || '', + line: l, + character: ch, + a: a, + b: b, + c: c, + d: d + }; + w.reason = m.supplant(w); + JSLINT.errors.push(w); + if (option.passfail) { + quit('Stopping. ', l, ch); + } +/* + warnings += 1; + if (warnings === 50) { + quit("Too many errors.", l, ch); + } +*/ + return w; + } + + function warningAt(m, l, ch, a, b, c, d) { + return warning(m, { + line: l, + from: ch + }, a, b, c, d); + } + + function error(m, t, a, b, c, d) { + var w = warning(m, t, a, b, c, d); + quit("Stopping, unable to continue.", w.line, w.character); + } + + function errorAt(m, l, ch, a, b, c, d) { + return error(m, { + line: l, + from: ch + }, a, b, c, d); + } + + + +// lexical analysis + + var lex = (function lex() { + var character, from, line, s; + +// Private lex methods + + function nextLine() { + var at; + line += 1; + if (line >= lines.length) { + return false; + } + character = 0; + //## keep tabs +// s = lines[line].replace(/\t/g, tab); + s = lines[line] +/* at = s.search(cx); + if (at >= 0) { + warningAt("Unsafe character.", line, at); + }*/ + return true; + } + +// Produce a token object. The token inherits from a syntax symbol. + + function it(type, value) { + var i, t; + if (type === '(color)') { + t = {type: type}; + } else if (type === '(punctuator)' || + (type === '(identifier)' && syntax.hasOwnProperty(value))) { + t = syntax[value] || syntax['(error)']; + +// Mozilla bug workaround. + + if (!t.id) { + t = syntax[type]; + } + } else { + t = syntax[type]; + } + t = Object.create(t); + if (type === '(string)' || type === '(range)') { + if (jx.test(value)) { + warningAt("Script URL.", line, from); + } + } + if (type === '(identifier)') { + t.identifier = true; + if (option.nomen && value.charAt(0) === '_') { + warningAt("Unexpected '_' in '{a}'.", line, from, value); + } + } + t.value = value; + t.line = line; + t.character = character; + t.from = from; +//alert('NEWTOKEN\ntype=' + type + '\nvalue=' + value + '\nfrom=' + t.from + '\ncharacter=' + t.character) + i = t.id; + if (i !== '(endline)') { + prereg = i && + (('(,=:[!&|?{};'.indexOf(i.charAt(i.length - 1)) >= 0) || + i === 'return'); + } + return t; + } + +// Public lex methods + + return { + init: function (source) { + if (typeof source === 'string') { + lines = source. + replace(/\r\n/g, '\n'). + replace(/\r/g, '\n'). + split('\n'); + } else { + lines = source; + } + line = -1; + nextLine(); + from = 0; + }, + + range: function (begin, end) { + var c, value = ''; + from = character; + if (s.charAt(0) !== begin) { + errorAt("Expected '{a}' and instead saw '{b}'.", + line, character, begin, s.charAt(0)); + } + for (;;) { + s = s.slice(1); + character += 1; + c = s.charAt(0); + switch (c) { + case '': + errorAt("Missing '{a}'.", line, character, c); + break; + case end: + s = s.slice(1); + character += 1; + return it('(range)', value); + case xquote: + case '\\': + case '\'': + case '"': + warningAt("Unexpected '{a}'.", line, character, c); + } + value += c; + } + + }, + +// token -- this is called by advance to get the next token. + + token: function () { + var b, c, captures, d, depth, high, i, l, low, q, t; + + function match(x) { + var r = x.exec(s), r1; + if (r) { + l = r[0].length; + r1 = r[1]; + c = r1.charAt(0); + s = s.substr(l); + character += l; + from = character - r1.length; + return r1; + } + } + + function string(x) { + var c, j, r = ''; + + if (jsonmode && x !== '"') { + warningAt("Strings must use doublequote.", + line, character); + } + + if (xquote === x || (xmode === 'scriptstring' && !xquote)) { + return it('(punctuator)', x); + } + + function esc(n) { + var i = parseInt(s.substr(j + 1, n), 16); + j += n; + if (i >= 32 && i <= 126 && + i !== 34 && i !== 92 && i !== 39) { + warningAt("Unnecessary escapement.", line, character); + } + character += n; + c = String.fromCharCode(i); + } + j = 0; + for (;;) { + while (j >= s.length) { + j = 0; + if (xmode !== 'html' || !nextLine()) { + errorAt("Unclosed string.", line, from); + } + } + c = s.charAt(j); + if (c === x) { + character += 1; + s = s.substr(j + 1); + return it('(string)', r, x); + } + if (c < ' ') { +/* +## Why exit on newlines ? + if (c === '\n' || c === '\r') { + break; + } + warningAt("Control character in string: {a}.", + line, character + j, s.slice(0, j)); +*/ + } else if (c === xquote) { + warningAt("Bad HTML string", line, character + j); + } else if (c === '<') { + if (option.safe && xmode === 'html') { + warningAt("ADsafe string violation.", + line, character + j); + } else if (s.charAt(j + 1) === '/' && (xmode || option.safe)) { + warningAt("Expected '<\\/' and instead saw ' 0) { + character += 1; + s = s.slice(i); + break; + } else { + if (!nextLine()) { + return it('(end)', ''); + } + } + } + t = match(rx[xmode] || tx); + if (!t) { + if (xmode === 'html') { + return it('(error)', s.charAt(0)); + } else { + t = ''; + c = ''; + while (s && s < '!') { + s = s.substr(1); + } + if (s) { + errorAt("Unexpected '{a}'.", + line, character, s.substr(0, 1)); + } + } + } else { + + // identifier + + if (c.isAlpha() || c === '_' || c === '$' /* ## unicode identifier grossfix */|| c >= '\u00c0') { + return it('(identifier)', t); + } + + // number + + if (c.isDigit()) { + if (xmode !== 'style' && !isFinite(Number(t))) { + warningAt("Bad number '{a}'.", + line, character, t); + } + if (xmode !== 'styleproperty' && s.substr(0, 1).isAlpha()) { + warningAt("Missing space after '{a}'.", + line, character, t); + } + if (c === '0') { + d = t.substr(1, 1); + if (d.isDigit()) { + if (token.id !== '.' && xmode !== 'styleproperty') { + warningAt("Don't use extra leading zeros '{a}'.", + line, character, t); + } + } else if (jsonmode && (d === 'x' || d === 'X')) { + warningAt("Avoid 0x-. '{a}'.", + line, character, t); + } + } + if (t.substr(t.length - 1) === '.') { + warningAt( + "A trailing decimal point can be confused with a dot '{a}'.", + line, character, t); + } + return it('(number)', t); + } + switch (t) { + + // string + + case '"': + case "'": + return string(t); + // // comment + + case '//': + if (src || (xmode && xmode !== 'script')) { + warningAt("Unexpected comment.", line, character); + } else if (xmode === 'script' && /<\s*\//i.test(s)) { + warningAt("Unexpected <\/ in comment.", line, character); + } else if ((option.safe || xmode === 'script') && ax.test(s)) { + warningAt("Dangerous comment.", line, character); + } + // ## notify of comment token + var v = lines[line].substr(from, s.length) + var c = lines[line].length-1 + if (!s.match(/\n/)) + c++ + if (!logTokenLock) + { + var t = { type : '(comment)', line : line, from : from, value : v, character : c } + t.rawValue = lines[t.line] ? lines[t.line].substr(t.from, t.character-t.from) : '' + logToken(t) + } + s = ''; + token.comment = true; + break; + + // /* comment + + case '/*': + if (src || (xmode && xmode !== 'script' && xmode !== 'style' && xmode !== 'styleproperty')) { + warningAt("Unexpected comment.", line, character); + } + if (option.safe && ax.test(s)) { + warningAt("ADsafe comment violation.", line, character); + } + var commentLineIndex = 0 + var firstCommentPrefix = '/*' + // First line misses '*' in '/*' + for (;;) { + i = s.search(lx); + if (i >= 0) { + // ## notify of comment token + // Last line of comment +// var h = !commentLineIndex ? from : 0 + var v = firstCommentPrefix+s.substr(0, i+2) + if (!logTokenLock) + { + var t = { type : '(comment)', line : line, from : from, value : v, character: from+v.length } + t.rawValue = lines[t.line] ? lines[t.line].substr(t.from, t.character-t.from) : '' + logToken(t) + } + break; + } + else + { + // ## + // All comment lines but the last one go through here + var c = lines[line].substr(from).length + if (commentLineIndex == 0) c += firstCommentPrefix.length + if (!logTokenLock) + { + var t = { type : '(comment)', line : line, from : from, value : firstCommentPrefix + s, character : c+from } + t.rawValue = lines[t.line] ? lines[t.line].substr(t.from, t.character-t.from) : '' + logToken(t) + logToken( { id : '(endline)' } ) + } + from = 0 + } + if (!nextLine()) { + errorAt("Unclosed comment.", line, character); + } else { + if (option.safe && ax.test(s)) { + warningAt("ADsafe comment violation.", line, character); + } + } + commentLineIndex++ + firstCommentPrefix = '' + } + character += i + 2; + if (s.substr(i, 1) === '/') { + errorAt("Nested comment.", line, character); + } + s = s.substr(i + 2); + token.comment = true; + break; + + // /*global /*extern /*members /*jslint */ + + case '/*global': + case '/*extern': + case '/*members': + case '/*member': + case '/*jslint': + case '*/': + return { + value: t, + type: 'special', + line: line, + character: character, + from: from + }; + + case '': + break; + // / + case '/': + if (prereg) { + depth = 0; + captures = 0; + l = 0; + for (;;) { + b = true; + c = s.charAt(l); + l += 1; + switch (c) { + case '': + errorAt("Unclosed regular expression.", line, from); + return; + case '/': + if (depth > 0) { + warningAt("Unescaped '{a}'.", line, from + l, '/'); + } + c = s.substr(0, l - 1); + q = { + g: true, + i: true, + m: true + }; + while (q[s.charAt(l)] === true) { + q[s.charAt(l)] = false; + l += 1; + } + character += l; + s = s.substr(l); + return it('(regexp)', c); + case '\\': + c = s.charAt(l); + if (c < ' ') { + warningAt("Unexpected control character in regular expression.", line, from + l); + } else if (c === '<') { + warningAt("Unexpected escaped character '{a}' in regular expression.", line, from + l, c); + } + l += 1; + break; + case '(': + depth += 1; + b = false; + if (s.charAt(l) === '?') { + l += 1; + switch (s.charAt(l)) { + case ':': + case '=': + case '!': + l += 1; + break; + default: + warningAt("Expected '{a}' and instead saw '{b}'.", line, from + l, ':', s.charAt(l)); + } + } else { + captures += 1; + } + break; + case ')': + if (depth === 0) { + warningAt("Unescaped '{a}'.", line, from + l, ')'); + } else { + depth -= 1; + } + break; + case ' ': + q = 1; + while (s.charAt(l) === ' ') { + l += 1; + q += 1; + } + if (q > 1) { + warningAt("Spaces are hard to count. Use {{a}}.", line, from + l, q); + } + break; + case '[': + if (s.charAt(l) === '^') { + l += 1; + } + q = false; + klass: do { + c = s.charAt(l); + l += 1; + switch (c) { + case '[': + case '^': + warningAt("Unescaped '{a}'.", line, from + l, c); + q = true; + break; + case '-': + if (q) { + q = false; + } else { + warningAt("Unescaped '{a}'.", line, from + l, '-'); + q = true; + } + break; + case ']': + if (!q) { + warningAt("Unescaped '{a}'.", line, from + l - 1, '-'); + } + break klass; + case '\\': + c = s.charAt(l); + if (c < ' ') { + warningAt("Unexpected control character in regular expression.", line, from + l); + } else if (c === '<') { + warningAt("Unexpected escaped character '{a}' in regular expression.", line, from + l, c); + } + l += 1; + q = true; + break; + case '/': + warningAt("Unescaped '{a}'.", line, from + l - 1, '/'); + q = true; + break; + case '<': + if (xmode === 'script') { + c = s.charAt(l); + if (c === '!' || c === '/') { + warningAt("HTML confusion in regular expression '<{a}'.", line, from + l, c); + } + } + q = true; + break; + default: + q = true; + } + } while (c); + break; + case '.': + if (option.regexp) { + warningAt("Unexpected '{a}'.", line, from + l, c); + } + break; + case ']': + case '?': + case '{': + case '}': + case '+': + case '*': + warningAt("Unescaped '{a}'.", line, from + l, c); + break; + case '<': + if (xmode === 'script') { + c = s.charAt(l); + if (c === '!' || c === '/') { + warningAt("HTML confusion in regular expression '<{a}'.", line, from + l, c); + } + } + } + if (b) { + switch (s.charAt(l)) { + case '?': + case '+': + case '*': + l += 1; + if (s.charAt(l) === '?') { + l += 1; + } + break; + case '{': + l += 1; + c = s.charAt(l); + if (c < '0' || c > '9') { + warningAt("Expected a number and instead saw '{a}'.", line, from + l, c); + } + l += 1; + low = +c; + for (;;) { + c = s.charAt(l); + if (c < '0' || c > '9') { + break; + } + l += 1; + low = +c + (low * 10); + } + high = low; + if (c === ',') { + l += 1; + high = Infinity; + c = s.charAt(l); + if (c >= '0' && c <= '9') { + l += 1; + high = +c; + for (;;) { + c = s.charAt(l); + if (c < '0' || c > '9') { + break; + } + l += 1; + high = +c + (high * 10); + } + } + } + if (s.charAt(l) !== '}') { + warningAt("Expected '{a}' and instead saw '{b}'.", line, from + l, '}', c); + } else { + l += 1; + } + if (s.charAt(l) === '?') { + l += 1; + } + if (low > high) { + warningAt("'{a}' should not be greater than '{b}'.", line, from + l, low, high); + } + } + } + } + c = s.substr(0, l - 1); + character += l; + s = s.substr(l); + return it('(regexp)', c); + } + return it('(punctuator)', t); + + // punctuator + + case '#': + if (xmode === 'html' || xmode === 'styleproperty') { + for (;;) { + c = s.charAt(0); + if ((c < '0' || c > '9') && + (c < 'a' || c > 'f') && + (c < 'A' || c > 'F')) { + break; + } + character += 1; + s = s.substr(1); + t += c; + } + if (t.length !== 4 && t.length !== 7) { + warningAt("Bad hex color '{a}'.", line, + from + l, t); + } + return it('(color)', t); + } + return it('(punctuator)', t); + default: + if (xmode === 'outer' && c === '&') { + character += 1; + s = s.substr(1); + for (;;) { + c = s.charAt(0); + character += 1; + s = s.substr(1); + if (c === ';') { + break; + } + if (!((c >= '0' && c <= '9') || + (c >= 'a' && c <= 'z') || + c === '#')) { + errorAt("Bad entity", line, from + l, + character); + } + } + break; + } + return it('(punctuator)', t); + } + } + } + } + }; + }()); + + + function addlabel(t, type) { + + if (t === 'hasOwnProperty') { + error("'hasOwnProperty' is a really bad name."); + } + if (option.safe && funct['(global)']) { + warning('ADsafe global: ' + t + '.', token); + } + +// Define t in the current function in the current scope. +/* + // ## disabled. I like my redefining. + if (funct.hasOwnProperty(t)) { + warning(funct[t] === true ? + "'{a}' was used before it was defined." : + "'{a}' is already defined.", + nexttoken, t); + } +*/ + funct[t] = type; + if (type === 'label') { + scope[t] = funct; + } else if (funct['(global)']) { + global[t] = funct; +/* + // ## commented out + if (implied.hasOwnProperty(t)) { + // This one won't allow + // Define a function Fn1, use global var SomeVar + // define someVar + // call Fn1 + warning("'{a}' was used before it was defined.", nexttoken, t); + delete implied[t]; + } +*/ + } else { + funct['(scope)'][t] = funct; + } + } + + + function doOption() { + var b, obj, filter, o = nexttoken.value, t, v; + switch (o) { + case '*/': + error("Unbegun comment."); + break; + case '/*global': + case '/*extern': + if (option.safe) { + warning("ADsafe restriction."); + } + obj = predefined; + break; + case '/*members': + case '/*member': + o = '/*members'; + if (!membersOnly) { + membersOnly = {}; + } + obj = membersOnly; + break; + case '/*jslint': + if (option.safe) { + warning("ADsafe restriction."); + } + obj = option; + filter = boolOptions; + } + for (;;) { + t = lex.token(); + if (t.id === ',') { + t = lex.token(); + } + while (t.id === '(endline)') { + t = lex.token(); + } + if (t.type === 'special' && t.value === '*/') { + break; + } + if (t.type !== '(string)' && t.type !== '(identifier)' && + o !== '/*members') { + error("Bad option.", t); + } + if (filter) { + if (filter[t.value] !== true && t.value !== 'indent') { + error("Bad option.", t); + } + v = lex.token(); + if (v.id !== ':') { + error("Expected '{a}' and instead saw '{b}'.", + t, ':', t.value); + } + v = lex.token(); + if (t.value === 'indent') { + b = +v.value; + if (typeof b !== 'number' || !isFinite(b) || b <= 0 || + Math.floor(b) !== b) { + error("Expected a small integer and instead saw '{a}'.", + v, v.value); + } + obj.white = true; + } else if (v.value === 'true') { + b = true; + } else if (v.value === 'false') { + b = false; + } else { + error("Expected '{a}' and instead saw '{b}'.", + t, 'true', t.value); + } + } else { + b = true; + } + obj[t.value] = b; + } + if (filter) { + assume(); + } + } + + +// We need a peek function. If it has an argument, it peeks that much farther +// ahead. It is used to distinguish +// for ( var i in ... +// from +// for ( var i = ... + + function peek(p) { + var i = p || 0, j = 0, t; + + while (j <= i) { + t = lookahead[j]; + if (!t) { + t = lookahead[j] = lex.token(); + } + j += 1; + } + return t; + } + + + +// Produce the next token. It looks for programming errors. + + function advance(id, t) { + switch (token.id) { + case '(number)': + if (nexttoken.id === '.') { + warning("A dot following a number can be confused with a decimal point.", token); + } + break; + case '-': + if (nexttoken.id === '-' || nexttoken.id === '--') { + warning("Confusing minusses."); + } + break; + case '+': + if (nexttoken.id === '+' || nexttoken.id === '++') { + warning("Confusing plusses."); + } + break; + } + if (token.type === '(string)' || token.identifier) { + anonname = token.value; + } + + if (id && nexttoken.id !== id) { + if (t) { + if (nexttoken.id === '(end)') { + warning("Unmatched '{a}'.", t, t.id); + } else { + warning("Expected '{a}' to match '{b}' from line {c} and instead saw '{d}'.", + nexttoken, id, t.id, t.line + 1, nexttoken.value); + } + } else if (nexttoken.type !== '(identifier)' || + nexttoken.value !== id) { + warning("Expected '{a}' and instead saw '{b}'.", + nexttoken, id, nexttoken.value); + } + } + prevtoken = token; + token = nexttoken; + for (;;) { + nexttoken = lookahead.shift() || lex.token(); + // ## notify of new token + if (!logTokenLock) + { + nexttoken.rawValue = lines[nexttoken.line] ? lines[nexttoken.line].substr(nexttoken.from, nexttoken.character-nexttoken.from) : '' + logToken(nexttoken) + } + if (nexttoken.id === '(end)' || nexttoken.id === '(error)') { + return; + } + if (nexttoken.type === 'special') { + doOption(); + } else { + if (nexttoken.id !== '(endline)') { + break; + } + } + } + if (!option.evil && nexttoken.value === 'eval') { + warning("eval is evil.", nexttoken); + } + } + +// This is the heart of JSLINT, the Pratt parser. In addition to parsing, it +// is looking for ad hoc lint patterns. We add to Pratt's model .fud, which is +// like nud except that it is only used on the first token of a statement. +// Having .fud makes it much easier to define JavaScript. I retained Pratt's +// nomenclature. + +// .nud Null denotation +// .fud First null denotation +// .led Left denotation +// lbp Left binding power +// rbp Right binding power + +// They are key to the parsing method called Top Down Operator Precedence. +/* +// http://javascript.crockford.com/tdop/tdop.html +;; NUD -- NUll left Denotation (op has nothing to its left (prefix)) +;; LED -- LEft Denotation (op has something to left (postfix or infix)) + +;; LBP -- Left Binding Power (the stickiness to the left) +;; RBP -- Right Binding Power (the stickiness to the right) + +*/ + function parse(rbp, initial) { + var left, o; + + // ## Allow return a, b + function maybeParseCommas() + { + if (rbp > 20 || skipCommas) return + while (nexttoken.value == ',') + { + advance(',') + parse(rbp) + } + } + var skipCommas = initial == 'singleExpression' + if (skipCommas) initial = undefined + + // ## notify of expression parsing start + logParseStart(rbp, initial) + if (nexttoken.id === '(end)') { + error("Unexpected early end of program.", token); + } + advance(); + if (option.safe && predefined[token.value] === true && + (nexttoken.id !== '(' && nexttoken.id !== '.')) { + warning('ADsafe violation.', token); + } + if (initial) { + anonname = 'anonymous'; + funct['(verb)'] = token.value; + } + if (initial === true && token.fud) { + left = token.fud(); + } else { + if (token.nud) { + o = token.exps; + left = token.nud(); + } else { + if (nexttoken.type === '(number)' && token.id === '.') { + warning( +"A leading decimal point can be confused with a dot: '.{a}'.", + token, nexttoken.value); + advance(); + // ## + maybeParseCommas() + // ## notify of expression parsing end + logParseEnd(rbp, initial) + return token; + } else { + error("Expected an identifier and instead saw '{a}'.", + token, token.id); + } + } + + while (rbp < nexttoken.lbp) { + // Don't allow [ on a next line as it might start an ObjC call + if (token.line != nexttoken.line && nexttoken.id == '[') break; + + o = nexttoken.exps; + // ## As we don't force lines to end with a semi colon, break if we encounter a reserved word + if (nexttoken.reserved && nexttoken.line != token.line) break + advance(); + if (token.led) { + left = token.led(left); + } else { +// if (token.line == prevtoken.line) + error("Expected an operator and instead saw '{a}'.", + token, token.id); + } + } + // ## .exps is transfered to o, to check if expression can stand on its own : assignment, ++ (postinc), delete, ... + // Disabled because o.release is a valid call in JSCocoa (when autocall is on) +/* + if (initial && !o && !token.exps) { +//## + warning("Expected an assignment or function call and instead saw an expression.", token); + } +*/ + } + + // ## + maybeParseCommas() + + // ## notify of expression parsing end + logParseEnd(rbp, initial) + + + return left; + } + + +// Functions for conformance of style. + + function abut(left, right) { + left = left || token; + right = right || nexttoken; + if (left.line !== right.line || left.character !== right.from) { + warning("Unexpected space after '{a}'.", right, left.value); + } + } + + + function adjacent(left, right) { + left = left || token; + right = right || nexttoken; + if (option.white || xmode === 'styleproperty' || xmode === 'style') { + if (left.character !== right.from && left.line === right.line) { + warning("Unexpected space after '{a}'.", right, left.value); + } + } + } + + function nospace(left, right) { + left = left || token; + right = right || nexttoken; + if (option.white && !left.comment) { + if (left.line === right.line) { + adjacent(left, right); + } + } + } + + + function nonadjacent(left, right) { + if (option.white) { + left = left || token; + right = right || nexttoken; + if (left.character === right.from) { + warning("Missing space after '{a}'.", + nexttoken, left.value); + } + } + } + + function nobreaknonadjacent(left, right) { + left = left || token; + right = right || nexttoken; + if (!option.laxbreak && left.line !== right.line) { + warning("Bad line breaking before '{a}'.", right, right.id); + } else if (option.white) { + left = left || token; + right = right || nexttoken; + if (left.character === right.from) { + warning("Missing space after '{a}'.", + nexttoken, left.value); + } + } + } + + function indentation(bias) { + var i; + if (option.white && nexttoken.id !== '(end)') { + i = indent + (bias || 0); + if (nexttoken.from !== i) { + warning("Expected '{a}' to have an indentation of {b} instead of {c}.", + nexttoken, nexttoken.value, i, nexttoken.from); + } + } + } + + function nolinebreak(t) { + if (t.line !== nexttoken.line) { + warning("Line breaking error '{a}'.", t, t.value); + } + } + + + function comma() { + if (token.line !== nexttoken.line) { + if (!option.laxbreak) { + warning("Bad line breaking before '{a}'.", token, nexttoken.id); + } + } else if (token.character !== nexttoken.from && option.white) { + warning("Unexpected space after '{a}'.", nexttoken, token.value); + } + advance(','); + nonadjacent(token, nexttoken); + } + + +// Functional constructors for making the symbols that will be inherited by +// tokens. + + function symbol(s, p) { + var x = syntax[s]; + if (!x || typeof x !== 'object') { + syntax[s] = x = { + id: s, + lbp: p, + value: s + }; + } + return x; + } + + + function delim(s) { + return symbol(s, 0); + } + + + function stmt(s, f) { + var x = delim(s); + x.identifier = x.reserved = true; + x.fud = f; + return x; + } + + + function blockstmt(s, f) { + var x = stmt(s, f); + x.block = true; + return x; + } + + + function reserveName(x) { + var c = x.id.charAt(0); + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + x.identifier = x.reserved = true; + } + return x; + } + + + function prefix(s, f) { + var x = symbol(s, 150); + reserveName(x); + x.nud = (typeof f === 'function') ? f : function () { + if (option.plusplus && (this.id === '++' || this.id === '--')) { + warning("Unexpected use of '{a}'.", this, this.id); + } + this.right = parse(150); + this.arity = 'unary'; + return this; + }; + return x; + } + + + function type(s, f) { + var x = delim(s); + x.type = s; + x.nud = f; + return x; + } + + + function reserve(s, f) { + var x = type(s, f); + x.identifier = x.reserved = true; + return x; + } + + + function reservevar(s, v) { + return reserve(s, function () { + if (this.id === 'this') { + if (option.safe) { + warning("ADsafe violation.", this); + } + } + return this; + }); + } + + + function infix(s, f, p, w) { + var x = symbol(s, p); + reserveName(x); + x.led = function (left) { + if (!w) { + nobreaknonadjacent(prevtoken, token); + nonadjacent(token, nexttoken); + } + if (typeof f === 'function') { + return f(left, this); + } else { + this.left = left; + this.right = parse(p); + return this; + } + }; + return x; + } + + + function relation(s, f) { + var x = symbol(s, 100); + x.led = function (left) { + nobreaknonadjacent(prevtoken, token); + nonadjacent(token, nexttoken); + var right = parse(100); + if ((left && left.id === 'NaN') || (right && right.id === 'NaN')) { + warning("Use the isNaN function to compare with NaN.", this); + } else if (f) { + f.apply(this, [left, right]); + } + this.left = left; + this.right = right; + return this; + }; + return x; + } + + + function isPoorRelation(node) { + return (node.type === '(number)' && !+node.value) || + (node.type === '(string)' && !node.value) || + node.type === 'true' || + node.type === 'false' || + node.type === 'undefined' || + node.type === 'null'; + } + + + function assignop(s, f) { + symbol(s, 20).exps = true; + return infix(s, function (left, that) { + var l; + that.left = left; + if (option.safe) { + l = left; + do { + if (predefined[l.value] === true) { + warning('ADsafe violation.', l); + } + l = l.left; + } while (l); + } + if (left) { + if (left.id === '.' || left.id === '[') { + if (left.left.value === 'arguments') { + warning('Bad assignment.', that); + } + that.right = parse(19); + return that; + } else if (left.identifier && !left.reserved) { + if (funct[left.value] === 'exception') { + warning("Do not assign to the exception parameter.", left); + } + that.right = parse(19); + return that; + } + if (left === syntax['function']) { + warning( +"Expected an identifier in an assignment and instead saw a function invocation.", + token); + } + } + error("Bad assignment.", that); + }, 20); + } + + function bitwise(s, f, p) { + var x = symbol(s, p); + reserveName(x); + x.led = (typeof f === 'function') ? f : function (left) { + if (option.bitwise) { + warning("Unexpected use of '{a}'.", this, this.id); + } + this.left = left; + this.right = parse(p); + return this; + }; + return x; + } + + function bitwiseassignop(s) { + symbol(s, 20).exps = true; + return infix(s, function (left, that) { + if (option.bitwise) { + warning("Unexpected use of '{a}'.", that, that.id); + } + nonadjacent(prevtoken, token); + nonadjacent(token, nexttoken); + if (left) { + if (left.id === '.' || left.id === '[' || + (left.identifier && !left.reserved)) { + parse(19); + return left; + } + if (left === syntax['function']) { + warning( +"Expected an identifier in an assignment, and instead saw a function invocation.", + token); + } + } + error("Bad assignment.", that); + }, 20); + } + + + function suffix(s, f) { + var x = symbol(s, 150); + x.led = function (left) { + if (option.plusplus) { + warning("Unexpected use of '{a}'.", this, this.id); + } + this.left = left; + return this; + }; + return x; + } + + + function optionalidentifier() { + if (nexttoken.reserved && nexttoken.value !== 'arguments') { + warning("Expected an identifier and instead saw '{a}' (a reserved word).", + nexttoken, nexttoken.id); + } + if (nexttoken.identifier) { + advance(); + return token.value; + } + } + + + function identifier() { + var i = optionalidentifier(); + if (i) { + return i; + } + if (token.id === 'function' && nexttoken.id === '(') { + warning("Missing name in function statement."); + } else { + error("Expected an identifier and instead saw '{a}'.", + nexttoken, nexttoken.value); + } + } + + function reachable(s) { + var i = 0, t; + if (nexttoken.id !== ';' || noreach) { + return; + } + for (;;) { + t = peek(i); + if (t.reach) { + return; + } + if (t.id !== '(endline)') { + if (t.id === 'function') { + warning( +"Inner functions should be listed at the top of the outer function.", t); + break; + } + warning("Unreachable '{a}' after '{b}'.", t, t.value, s); + break; + } + i += 1; + } + } + + + function statement(noindent) { + var i = indent, r, s = scope, t = nexttoken; + +// We don't like the empty statement. + + if (t.id === ';') { + warning("Unnecessary semicolon.", t); + advance(';'); + return; + } + +// Is this a labelled statement? + + if (t.identifier && !t.reserved && peek().id === ':') { + advance(); + advance(':'); + scope = Object.create(s); + addlabel(t.value, 'label'); + if (!nexttoken.labelled) { + warning("Label '{a}' on {b} statement.", + nexttoken, t.value, nexttoken.value); + } + if (jx.test(t.value + ':')) { + warning("Label '{a}' looks like a javascript url.", + t, t.value); + } + nexttoken.label = t.value; + t = nexttoken; + } + +// Parse the statement. + + if (!noindent) { + indentation(); + } + r = parse(0, true); + +// Look for the final semicolon. + if (!t.block) { + // ## Only warn about missing semicolons when next token is on same line and not a closing brace (like in one line closures) + if (nexttoken.id !== ';') { + if (token.line == nexttoken.line && nexttoken.id != '}') + warningAt("Missing semicolon.", token.line, token.from + token.value.length); +//## +// warningAt("Missing semicolon.", token.line, +// token.from + token.value.length); + } else { + abut(token, nexttoken); + advance(';'); + nonadjacent(token, nexttoken); + } + } + +// Restore the indentation. + + indent = i; + scope = s; + return r; + } + + function use_strict() { + if (nexttoken.type === '(string)' && + /^use +strict(?:,.+)?$/.test(nexttoken.value)) { + advance(); + advance(';'); + return true; + } else { + return false; + } + } + + + function statements(begin) { + var a = [], f, p; + if (begin && !use_strict() && option.strict) { + warning('Missing "use strict" statement.', nexttoken); + } + if (option.adsafe) { + switch (begin) { + case 'script': + if (!adsafe_may) { + if (nexttoken.value !== 'ADSAFE' || + peek(0).id !== '.' || + (peek(1).value !== 'id' && + peek(1).value !== 'go')) { + error('ADsafe violation: Missing ADSAFE.id or ADSAFE.go.', + nexttoken); + } + } + if (nexttoken.value === 'ADSAFE' && + peek(0).id === '.' && + peek(1).value === 'id') { + if (adsafe_may) { + error('ADsafe violation.', nexttoken); + } + advance('ADSAFE'); + advance('.'); + advance('id'); + advance('('); + if (nexttoken.value !== adsafe_id) { + error('ADsafe violation: id does not match.', nexttoken); + } + advance('(string)'); + advance(')'); + advance(';'); + adsafe_may = true; + } + break; + case 'lib': + if (nexttoken.value === 'ADSAFE') { + advance('ADSAFE'); + advance('.'); + advance('lib'); + advance('('); + advance('(string)'); + comma(); + f = parse(0); + if (f.id !== 'function') { + error('The second argument to lib must be a function.', f); + } + p = f.funct['(params)']; + if (p && p !== 'lib') { + error("Expected '{a}' and instead saw '{b}'.", + f, 'lib', p); + } + advance(')'); + advance(';'); + return a; + } else { + error("ADsafe lib violation."); + } + } + } + while (!nexttoken.reach && nexttoken.id !== '(end)') { + if (nexttoken.id === ';') { + warning("Unnecessary semicolon."); + advance(';'); + } else { + var s = statement() +// logStatement(s) + a.push(s); + } + } + return a; + } + + + function block(f) { + var a, b = inblock, s = scope, t; + inblock = f; + if (f) { + scope = Object.create(scope); + } + nonadjacent(token, nexttoken); + t = nexttoken; + if (nexttoken.id === '{') { + advance('{'); + if (nexttoken.id !== '}' || token.line !== nexttoken.line) { + indent += option.indent; + if (!f && nexttoken.from === indent + option.indent) { + indent += option.indent; + } + if (!f) { + use_strict(); + } + a = statements(); + indent -= option.indent; + indentation(); + } + advance('}', t); + } else { +//## +// warning("Expected '{a}' and instead saw '{b}'.", +// nexttoken, '{', nexttoken.value); + noreach = true; + a = [statement()]; + noreach = false; + } + funct['(verb)'] = null; + scope = s; + inblock = b; + return a; + } + + +// An identity function, used by string and number tokens. + + function idValue() { + return this; + } + + + function countMember(m) { + // ## Causing errors while linting jslint. Disabled for now. + + if (0 && membersOnly && membersOnly[m] !== true) { + warning("Unexpected /*member '{a}'.", nexttoken, m); + } + if (typeof member[m] === 'number') { + member[m] += 1; + } else { + member[m] = 1; + } + } + + function note_implied(token) { + var name = token.value, line = token.line + 1, a = implied[name]; + if (typeof a === 'function') { + a = false; + } + if (!a) { + a = [line]; + implied[name] = a; + } else if (a[a.length - 1] !== line) { + a.push(line); + } + } + +// CSS parsing. +/* + + function cssName() { + if (nexttoken.identifier) { + advance(); + return true; + } + } + + function cssNumber() { + if (nexttoken.id === '-') { + advance('-'); + advance('(number)'); + } + if (nexttoken.type === '(number)') { + advance(); + return true; + } + } + + function cssString() { + if (nexttoken.type === '(string)') { + advance(); + return true; + } + } + + function cssColor() { + var i, number; + if (nexttoken.identifier) { + if (nexttoken.value === 'rgb') { + advance(); + advance('('); + for (i = 0; i < 3; i += 1) { + number = nexttoken.value; + if (nexttoken.type !== '(number)' || number < 0) { + warning("Expected a positive number and instead saw '{a}'", + nexttoken, number); + advance(); + } else { + advance(); + if (nexttoken.id === '%') { + advance('%'); + if (number > 100) { + warning("Expected a percentage and instead saw '{a}'", + token, number); + } + } else { + if (number > 255) { + warning("Expected a small number and instead saw '{a}'", + token, number); + } + } + } + } + advance(')'); + return true; + } else if (cssColorData[nexttoken.value] === true) { + advance(); + return true; + } + } else if (nexttoken.type === '(color)') { + advance(); + return true; + } + return false; + } + + function cssLength() { + if (nexttoken.id === '-') { + advance('-'); + adjacent(); + } + if (nexttoken.type === '(number)') { + advance(); + if (nexttoken.type !== '(string)' && + cssLengthData[nexttoken.value] === true) { + adjacent(); + advance(); + } else if (+token.value !== 0) { + warning("Expected a linear unit and instead saw '{a}'.", + nexttoken, nexttoken.value); + } + return true; + } + return false; + } + + function cssLineHeight() { + if (nexttoken.id === '-') { + advance('-'); + adjacent(); + } + if (nexttoken.type === '(number)') { + advance(); + if (nexttoken.type !== '(string)' && + cssLengthData[nexttoken.value] === true) { + adjacent(); + advance(); + } + return true; + } + return false; + } + + function cssWidth() { + if (nexttoken.identifier) { + switch (nexttoken.value) { + case 'thin': + case 'medium': + case 'thick': + advance(); + return true; + } + } else { + return cssLength(); + } + } + + function cssMargin() { + if (nexttoken.identifier) { + if (nexttoken.value === 'auto') { + advance(); + return true; + } + } else { + return cssLength(); + } + } + + function cssAttr() { + if (nexttoken.identifier && nexttoken.value === 'attr') { + advance(); + advance('('); + if (!nexttoken.identifier) { + warning("Expected a name and instead saw '{a}'.", + nexttoken, nexttoken.value); + } + advance(); + advance(')'); + return true; + } + return false; + } + + function cssCommaList() { + while (nexttoken.id !== ';') { + if (!cssName() && !cssString()) { + warning("Expected a name and instead saw '{a}'.", + nexttoken, nexttoken.value); + } + if (nexttoken.id !== ',') { + return true; + } + comma(); + } + } + + function cssCounter() { + if (nexttoken.identifier && nexttoken.value === 'counter') { + advance(); + advance('('); + if (!nexttoken.identifier) { + } + advance(); + if (nexttoken.id === ',') { + comma(); + if (nexttoken.type !== '(string)') { + warning("Expected a string and instead saw '{a}'.", + nexttoken, nexttoken.value); + } + advance(); + } + advance(')'); + return true; + } + if (nexttoken.identifier && nexttoken.value === 'counters') { + advance(); + advance('('); + if (!nexttoken.identifier) { + warning("Expected a name and instead saw '{a}'.", + nexttoken, nexttoken.value); + } + advance(); + if (nexttoken.id === ',') { + comma(); + if (nexttoken.type !== '(string)') { + warning("Expected a string and instead saw '{a}'.", + nexttoken, nexttoken.value); + } + advance(); + } + if (nexttoken.id === ',') { + comma(); + if (nexttoken.type !== '(string)') { + warning("Expected a string and instead saw '{a}'.", + nexttoken, nexttoken.value); + } + advance(); + } + advance(')'); + return true; + } + return false; + } + + + function cssShape() { + var i; + if (nexttoken.identifier && nexttoken.value === 'rect') { + advance(); + advance('('); + for (i = 0; i < 4; i += 1) { + if (!cssLength()) { + warning("Expected a number and instead saw '{a}'.", + nexttoken, nexttoken.value); + break; + } + } + advance(')'); + return true; + } + return false; + } + + function cssUrl() { + var url; + if (nexttoken.identifier && nexttoken.value === 'url') { + nexttoken = lex.range('(', ')'); + url = nexttoken.value; + advance(); + if (option.safe && ux.test(url)) { + error("ADsafe URL violation."); + } + urls.push(url); + return true; + } + return false; + } + + cssAny = [cssUrl, function () { + for (;;) { + if (nexttoken.identifier) { + switch (nexttoken.value.toLowerCase()) { + case 'url': + cssUrl(); + break; + case 'expression': + warning("Unexpected expression '{a}'.", + nexttoken, nexttoken.value); + advance(); + break; + default: + advance(); + } + } else { + if (nexttoken.id === ';' || nexttoken.id === '!' || + nexttoken.id === '(end)' || nexttoken.id === '}') { + return true; + } + advance(); + } + } + }]; + + cssBorderStyle = [ + 'none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'ridge', + 'inset', 'outset' + ]; + + cssAttributeData = { + background: [ + true, 'background-attachment', 'background-color', + 'background-image', 'background-position', 'background-repeat' + ], + 'background-attachment': ['scroll', 'fixed'], + 'background-color': ['transparent', cssColor], + 'background-image': ['none', cssUrl], + 'background-position': [ + 2, [cssLength, 'top', 'bottom', 'left', 'right', 'center'] + ], + 'background-repeat': [ + 'repeat', 'repeat-x', 'repeat-y', 'no-repeat' + ], + 'border': [true, 'border-color', 'border-style', 'border-width'], + 'border-bottom': [true, 'border-bottom-color', 'border-bottom-style', 'border-bottom-width'], + 'border-bottom-color': cssColor, + 'border-bottom-style': cssBorderStyle, + 'border-bottom-width': cssWidth, + 'border-collapse': ['collapse', 'separate'], + 'border-color': ['transparent', 4, cssColor], + 'border-left': [ + true, 'border-left-color', 'border-left-style', 'border-left-width' + ], + 'border-left-color': cssColor, + 'border-left-style': cssBorderStyle, + 'border-left-width': cssWidth, + 'border-right': [ + true, 'border-right-color', 'border-right-style', 'border-right-width' + ], + 'border-right-color': cssColor, + 'border-right-style': cssBorderStyle, + 'border-right-width': cssWidth, + 'border-spacing': [2, cssLength], + 'border-style': [4, cssBorderStyle], + 'border-top': [ + true, 'border-top-color', 'border-top-style', 'border-top-width' + ], + 'border-top-color': cssColor, + 'border-top-style': cssBorderStyle, + 'border-top-width': cssWidth, + 'border-width': [4, cssWidth], + bottom: [cssLength, 'auto'], + 'caption-side' : ['bottom', 'left', 'right', 'top'], + clear: ['both', 'left', 'none', 'right'], + clip: [cssShape, 'auto'], + color: cssColor, + content: [ + 'open-quote', 'close-quote', 'no-open-quote', 'no-close-quote', + cssString, cssUrl, cssCounter, cssAttr + ], + 'counter-increment': [ + cssName, 'none' + ], + 'counter-reset': [ + cssName, 'none' + ], + cursor: [ + cssUrl, 'auto', 'crosshair', 'default', 'e-resize', 'help', 'move', + 'n-resize', 'ne-resize', 'nw-resize', 'pointer', 's-resize', + 'se-resize', 'sw-resize', 'w-resize', 'text', 'wait' + ], + direction: ['ltr', 'rtl'], + display: [ + 'block', 'compact', 'inline', 'inline-block', 'inline-table', + 'list-item', 'marker', 'none', 'run-in', 'table', 'table-caption', + 'table-cell', 'table-column', 'table-column-group', 'table-footer-group', + 'table-header-group', 'table-row', 'table-row-group' + ], + 'empty-cells': ['show', 'hide'], + 'float': ['left', 'none', 'right'], + font: [ + 'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar', + true, 'font-size', 'font-style', 'font-weight', 'font-family' + ], + 'font-family': cssCommaList, + 'font-size': [ + 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', + 'xx-large', 'larger', 'smaller', cssLength + ], + 'font-size-adjust': ['none', cssNumber], + 'font-stretch': [ + 'normal', 'wider', 'narrower', 'ultra-condensed', + 'extra-condensed', 'condensed', 'semi-condensed', + 'semi-expanded', 'expanded', 'extra-expanded' + ], + 'font-style': [ + 'normal', 'italic', 'oblique' + ], + 'font-variant': [ + 'normal', 'small-caps' + ], + 'font-weight': [ + 'normal', 'bold', 'bolder', 'lighter', cssNumber + ], + height: [cssLength, 'auto'], + left: [cssLength, 'auto'], + 'letter-spacing': ['normal', cssLength], + 'line-height': ['normal', cssLineHeight], + 'list-style': [ + true, 'list-style-image', 'list-style-position', 'list-style-type' + ], + 'list-style-image': ['none', cssUrl], + 'list-style-position': ['inside', 'outside'], + 'list-style-type': [ + 'circle', 'disc', 'square', 'decimal', 'decimal-leading-zero', + 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', + 'lower-latin', 'upper-alpha', 'upper-latin', 'hebrew', 'katakana', + 'hiragana-iroha', 'katakana-oroha', 'none' + ], + margin: [4, cssMargin], + 'margin-bottom': cssMargin, + 'margin-left': cssMargin, + 'margin-right': cssMargin, + 'margin-top': cssMargin, + 'marker-offset': [cssLength, 'auto'], + 'max-height': [cssLength, 'none'], + 'max-width': [cssLength, 'none'], + 'min-height': cssLength, + 'min-width': cssLength, + opacity: cssNumber, + outline: [true, 'outline-color', 'outline-style', 'outline-width'], + 'outline-color': ['invert', cssColor], + 'outline-style': [ + 'dashed', 'dotted', 'double', 'groove', 'inset', 'none', + 'outset', 'ridge', 'solid' + ], + 'outline-width': cssWidth, + overflow: ['auto', 'hidden', 'scroll', 'visible'], + padding: [4, cssLength], + 'padding-bottom': cssLength, + 'padding-left': cssLength, + 'padding-right': cssLength, + 'padding-top': cssLength, + position: ['absolute', 'fixed', 'relative', 'static'], + quotes: [8, cssString], + right: [cssLength, 'auto'], + 'table-layout': ['auto', 'fixed'], + 'text-align': ['center', 'justify', 'left', 'right'], + 'text-decoration': ['none', 'underline', 'overline', 'line-through', 'blink'], + 'text-indent': cssLength, + 'text-shadow': ['none', 4, [cssColor, cssLength]], + 'text-transform': ['capitalize', 'uppercase', 'lowercase', 'none'], + top: [cssLength, 'auto'], + 'unicode-bidi': ['normal', 'embed', 'bidi-override'], + 'vertical-align': [ + 'baseline', 'bottom', 'sub', 'super', 'top', 'text-top', 'middle', + 'text-bottom', cssLength + ], + visibility: ['visible', 'hidden', 'collapse'], + 'white-space': ['normal', 'pre', 'nowrap'], + width: [cssLength, 'auto'], + 'word-spacing': ['normal', cssLength], + 'z-index': ['auto', cssNumber] + }; + + function styleAttribute() { + var v; + while (nexttoken.id === '*' || nexttoken.id === '#' || nexttoken.value === '_') { + if (!option.css) { + warning("Unexpected '{a}'.", nexttoken, nexttoken.value); + } + advance(); + } + if (nexttoken.id === '-') { + if (!option.css) { + warning("Unexpected '{a}'.", nexttoken, nexttoken.value); + } + advance('-'); + if (!nexttoken.identifier) { + warning("Expected a non-standard style attribute and instead saw '{a}'.", + nexttoken, nexttoken.value); + } + advance(); + return cssAny; + } else { + if (!nexttoken.identifier) { + warning("Excepted a style attribute, and instead saw '{a}'.", + nexttoken, nexttoken.value); + } else { + if (cssAttributeData.hasOwnProperty(nexttoken.value)) { + v = cssAttributeData[nexttoken.value]; + } else { + v = cssAny; + if (!option.css) { + warning("Unrecognized style attribute '{a}'.", + nexttoken, nexttoken.value); + } + } + } + advance(); + return v; + } + } + + function styleValue(v) { + var i = 0, + n, + once, + match, + round, + start = 0, + vi; + switch (typeof v) { + case 'function': + return v(); + case 'string': + if (nexttoken.identifier && nexttoken.value === v) { + advance(); + return true; + } + return false; + } + for (;;) { + if (i >= v.length) { + return false; + } + vi = v[i]; + i += 1; + if (vi === true) { + break; + } else if (typeof vi === 'number') { + n = vi; + vi = v[i]; + i += 1; + } else { + n = 1; + } + match = false; + while (n > 0) { + if (styleValue(vi)) { + match = true; + n -= 1; + } else { + break; + } + } + if (match) { + return true; + } + } + start = i; + once = []; + for (;;) { + round = false; + for (i = start; i < v.length; i += 1) { + if (!once[i]) { + if (styleValue(cssAttributeData[v[i]])) { + match = true; + round = true; + once[i] = true; + break; + } + } + } + if (!round) { + return match; + } + } + } + + function substyle() { + var v; + for (;;) { + if (nexttoken.id === '}' || nexttoken.id === '(end)' || + xquote && nexttoken.id === xquote) { + return; + } + while (nexttoken.id === ';') { + warning("Misplaced ';'."); + advance(';'); + } + v = styleAttribute(); + advance(':'); + if (nexttoken.identifier && nexttoken.value === 'inherit') { + advance(); + } else { + styleValue(v); + } + while (nexttoken.id !== ';' && nexttoken.id !== '!' && + nexttoken.id !== '}' && nexttoken.id !== '(end)' && + nexttoken.id !== xquote) { + warning("Unexpected token '{a}'.", nexttoken, nexttoken.value); + advance(); + } + if (nexttoken.id === '!') { + advance('!'); + adjacent(); + if (nexttoken.identifier && nexttoken.value === 'important') { + advance(); + } else { + warning("Expected '{a}' and instead saw '{b}'.", + nexttoken, 'important', nexttoken.value); + } + } + if (nexttoken.id === '}' || nexttoken.id === xquote) { + warning("Missing '{a}'.", nexttoken, ';'); + } else { + advance(';'); + } + } + } + + function stylePattern() { + var name; + if (nexttoken.id === '{') { + warning("Expected a style pattern, and instead saw '{a}'.", nexttoken, + nexttoken.id); + } else if (nexttoken.id === '@') { + advance('@'); + name = nexttoken.value; + if (nexttoken.identifier && atrule[name] === true) { + advance(); + return name; + } + warning("Expected an at-rule, and instead saw @{a}.", nexttoken, name); + } + for (;;) { + if (nexttoken.identifier) { + if (!htmltag.hasOwnProperty(nexttoken.value)) { + warning("Expected a tagName, and instead saw {a}.", + nexttoken, nexttoken.value); + } + advance(); + } else { + switch (nexttoken.id) { + case '>': + case '+': + advance(); + if (!nexttoken.identifier || + !htmltag.hasOwnProperty(nexttoken.value)) { + warning("Expected a tagName, and instead saw {a}.", + nexttoken, nexttoken.value); + } + advance(); + break; + case ':': + advance(':'); + if (pseudorule[nexttoken.value] !== true) { + warning("Expected a pseudo, and instead saw :{a}.", + nexttoken, nexttoken.value); + } + advance(); + if (nexttoken.value === 'lang') { + advance('('); + if (!nexttoken.identifier) { + warning("Expected a lang code, and instead saw :{a}.", + nexttoken, nexttoken.value); + } + advance(')'); + } + break; + case '#': + advance('#'); + if (!nexttoken.identifier) { + warning("Expected an id, and instead saw #{a}.", + nexttoken, nexttoken.value); + } + advance(); + break; + case '*': + advance('*'); + break; + case '.': + advance('.'); + if (!nexttoken.identifier) { + warning("Expected a class, and instead saw #.{a}.", + nexttoken, nexttoken.value); + } + advance(); + break; + case '[': + advance('['); + if (!nexttoken.identifier) { + warning("Expected an attribute, and instead saw [{a}].", + nexttoken, nexttoken.value); + } + advance(); + if (nexttoken.id === '=' || nexttoken.id === '~=' || + nexttoken.id === '|=') { + advance(); + if (nexttoken.type !== '(string)') { + warning("Expected a string, and instead saw {a}.", + nexttoken, nexttoken.value); + } + advance(); + } + advance(']'); + break; + default: + error("Expected a CSS selector, and instead saw {a}.", + nexttoken, nexttoken.value); + } + } + if (nexttoken.id === ' fragments and .js files.", token); + } + if (option.fragment) { + if (n !== 'div') { + error("ADsafe violation: Wrap the widget in a div.", token); + } + } else { + error("Use the fragment option.", token); + } + } + option.browser = true; + assume(); + } + + function doAttribute(n, a, v) { + var u, x; + if (a === 'id') { + u = typeof v === 'string' ? v.toUpperCase() : ''; + if (ids[u] === true) { + warning("Duplicate id='{a}'.", nexttoken, v); + } + if (option.adsafe) { + if (adsafe_id) { + if (v.slice(0, adsafe_id.length) !== adsafe_id) { + warning("ADsafe violation: An id must have a '{a}' prefix", + nexttoken, adsafe_id); + } else if (!/^[A-Z]+_[A-Z]+$/.test(v)) { + warning("ADSAFE violation: bad id."); + } + } else { + adsafe_id = v; + if (!/^[A-Z]+_$/.test(v)) { + warning("ADSAFE violation: bad id."); + } + } + } + x = v.search(dx); + if (x >= 0) { + warning("Unexpected character '{a}' in {b}.", token, v.charAt(x), a); + } + ids[u] = true; + } else if (a === 'class' || a === 'type' || a === 'name') { + x = v.search(qx); + if (x >= 0) { + warning("Unexpected character '{a}' in {b}.", token, v.charAt(x), a); + } + ids[u] = true; + } else if (a === 'href' || a === 'background' || + a === 'content' || a === 'data' || + a.indexOf('src') >= 0 || a.indexOf('url') >= 0) { + if (option.safe && ux.test(v)) { + error("ADsafe URL violation."); + } + urls.push(v); + } else if (a === 'for') { + if (option.adsafe) { + if (adsafe_id) { + if (v.slice(0, adsafe_id.length) !== adsafe_id) { + warning("ADsafe violation: An id must have a '{a}' prefix", + nexttoken, adsafe_id); + } else if (!/^[A-Z]+_[A-Z]+$/.test(v)) { + warning("ADSAFE violation: bad id."); + } + } else { + warning("ADSAFE violation: bad id."); + } + } + } else if (a === 'name') { + if (option.adsafe && v.indexOf('_') >= 0) { + warning("ADsafe name violation."); + } + } + } + + function doTag(n, a) { + var i, t = htmltag[n], x; + src = false; + if (!t) { + error("Unrecognized tag '<{a}>'.", + nexttoken, + n === n.toLowerCase() ? n : + n + ' (capitalization error)'); + } + if (stack.length > 0) { + if (n === 'html') { + error("Too many tags.", token); + } + x = t.parent; + if (x) { + if (x.indexOf(' ' + stack[stack.length - 1].name + ' ') < 0) { + error("A '<{a}>' must be within '<{b}>'.", + token, n, x); + } + } else if (!option.adsafe && !option.fragment) { + i = stack.length; + do { + if (i <= 0) { + error("A '<{a}>' must be within '<{b}>'.", + token, n, 'body'); + } + i -= 1; + } while (stack[i].name !== 'body'); + } + } + switch (n) { + case 'div': + if (option.adsafe && stack.length === 1 && !adsafe_id) { + warning("ADSAFE violation: missing ID_."); + } + break; + case 'script': + xmode = 'script'; + advance('>'); + indent = nexttoken.from; + if (a.lang) { + warning("lang is deprecated.", token); + } + if (option.adsafe && stack.length !== 1) { + warning("ADsafe script placement violation.", token); + } + if (a.src) { + if (option.adsafe && (!adsafe_may || !approved[a.src])) { + warning("ADsafe unapproved script source.", token); + } + if (a.type) { + warning("type is unnecessary.", token); + } + } else { + if (adsafe_went) { + error("ADsafe script violation.", token); + } + statements('script'); + } + xmode = 'html'; + advance(''); + styles(); + xmode = 'html'; + advance(''; + } + + function html() { + var a, attributes, e, n, q, t, v, w = option.white, wmode; + xmode = 'html'; + xquote = ''; + stack = null; + for (;;) { + switch (nexttoken.value) { + case '<': + xmode = 'html'; + advance('<'); + attributes = {}; + t = nexttoken; + if (!t.identifier) { + warning("Bad identifier {a}.", t, t.value); + } + n = t.value; + if (option.cap) { + n = n.toLowerCase(); + } + t.name = n; + advance(); + if (!stack) { + stack = []; + doBegin(n); + } + v = htmltag[n]; + if (typeof v !== 'object') { + error("Unrecognized tag '<{a}>'.", t, n); + } + e = v.empty; + t.type = n; + for (;;) { + if (nexttoken.id === '/') { + advance('/'); + if (nexttoken.id !== '>') { + warning("Expected '{a}' and instead saw '{b}'.", + nexttoken, '>', nexttoken.value); + } + break; + } + if (nexttoken.id && nexttoken.id.substr(0, 1) === '>') { + break; + } + if (!nexttoken.identifier) { + if (nexttoken.id === '(end)' || nexttoken.id === '(error)') { + error("Missing '>'.", nexttoken); + } + warning("Bad identifier."); + } + option.white = true; + nonadjacent(token, nexttoken); + a = nexttoken.value; + option.white = w; + advance(); + if (!option.cap && a !== a.toLowerCase()) { + warning("Attribute '{a}' not all lower case.", nexttoken, a); + } + a = a.toLowerCase(); + xquote = ''; + if (attributes.hasOwnProperty(a)) { + warning("Attribute '{a}' repeated.", nexttoken, a); + } + if (a.slice(0, 2) === 'on') { + if (!option.on) { + warning("Avoid HTML event handlers."); + } + xmode = 'scriptstring'; + advance('='); + q = nexttoken.id; + if (q !== '"' && q !== "'") { + error("Missing quote."); + } + xquote = q; + wmode = option.white; + option.white = false; + advance(q); + statements('on'); + option.white = wmode; + if (nexttoken.id !== q) { + error("Missing close quote on script attribute."); + } + xmode = 'html'; + xquote = ''; + advance(q); + v = false; + } else if (a === 'style') { + xmode = 'scriptstring'; + advance('='); + q = nexttoken.id; + if (q !== '"' && q !== "'") { + error("Missing quote."); + } + xmode = 'styleproperty'; + xquote = q; + advance(q); + substyle(); + xmode = 'html'; + xquote = ''; + advance(q); + v = false; + } else { + if (nexttoken.id === '=') { + advance('='); + v = nexttoken.value; + if (!nexttoken.identifier && + nexttoken.id !== '"' && + nexttoken.id !== '\'' && + nexttoken.type !== '(string)' && + nexttoken.type !== '(number)' && + nexttoken.type !== '(color)') { + warning("Expected an attribute value and instead saw '{a}'.", token, a); + } + advance(); + } else { + v = true; + } + } + attributes[a] = v; + doAttribute(n, a, v); + } + doTag(n, attributes); + if (!e) { + stack.push(t); + } + xmode = 'outer'; + advance('>'); + break; + case '') { + error("Missing '{a}'.", nexttoken, '>'); + } + xmode = 'outer'; + advance('>'); + break; + case '' || nexttoken.id === '(end)') { + break; + } + if (nexttoken.id === '--') { + warning("Unexpected --."); + } + } + xmode = 'outer'; + advance('>'); + break; + case ''."); + } + if (nexttoken.id === ''); + break; + case '(end)': + return; + default: + if (nexttoken.id === '(end)') { + error("Missing '{a}'.", nexttoken, + ''); + } else { + advance(); + } + } + if (stack && stack.length === 0 && (option.adsafe || + !option.fragment || nexttoken.id === '(end)')) { + break; + } + } + if (nexttoken.id !== '(end)') { + error("Unexpected material after the end."); + } + } +*/ + +// Build the syntax table by declaring the syntactic elements of the language. + + type('(number)', idValue); + type('(string)', idValue); + + syntax['(identifier)'] = { + type: '(identifier)', + lbp: 0, + identifier: true, + nud: function () { + var v = this.value, + s = scope[v]; + if (typeof s === 'function') { + s = false; + } + +// The name is in scope and defined in the current function. + + if (s && (s === funct || s === funct['(global)'])) { + +// If we are not also in the global scope, change 'unused' to 'var', +// and reject labels. + + if (!funct['(global)']) { + switch (funct[v]) { + case 'unused': + funct[v] = 'var'; + break; + case 'label': + warning("'{a}' is a statement label.", token, v); + break; + } + } + +// The name is not defined in the function. If we are in the global scope, +// then we have an undefined variable. + + } else if (funct['(global)']) { + if (option.undef) { + warning("'{a}' is not defined.", token, v); + } + note_implied(token); + +// If the name is already defined in the current +// function, but not as outer, then there is a scope error. + + } else { + switch (funct[v]) { + case 'closure': + case 'function': + case 'var': + case 'unused': + warning("'{a}' used out of scope.", token, v); + break; + case 'label': + warning("'{a}' is a statement label.", token, v); + break; + case 'outer': + case true: + break; + default: + +// If the name is defined in an outer function, make an outer entry, and if +// it was unused, make it var. + + if (s === true) { + funct[v] = true; + } else if (typeof s !== 'object') { + if (option.undef) { + warning("'{a}' is not defined.", token, v); + } else { + funct[v] = true; + } + note_implied(token); + } else { + switch (s[v]) { + case 'function': + case 'var': + case 'unused': + s[v] = 'closure'; + funct[v] = 'outer'; + break; + case 'closure': + case 'parameter': + funct[v] = 'outer'; + break; + case 'label': + warning("'{a}' is a statement label.", token, v); + } + } + } + } + return this; + }, + led: function () { + error("Expected an operator and instead saw '{a}'.", + nexttoken, nexttoken.value); + } + }; + + type('(regexp)', function () { + return this; + }); + + delim('(endline)'); + delim('(begin)'); + delim('(end)').reach = true; + delim(''); + delim('(error)').reach = true; + delim('}').reach = true; + delim(')'); + delim(']'); + delim('"').reach = true; + delim("'").reach = true; + delim(';'); + delim(':').reach = true; + delim(','); + delim('#'); +// delim('@'); + reserve('else'); + reserve('case').reach = true; + reserve('catch'); + reserve('default').reach = true; + reserve('finally'); + reservevar('arguments'); + reservevar('eval'); + reservevar('false'); + reservevar('Infinity'); + reservevar('NaN'); + reservevar('null'); + reservevar('this'); + reservevar('true'); + reservevar('undefined'); + + + assignop('=', 'assign', 20); + assignop('+=', 'assignadd', 20); + assignop('-=', 'assignsub', 20); + assignop('*=', 'assignmult', 20); + assignop('/=', 'assigndiv', 20).nud = function () { + error("A regular expression literal can be confused with '/='."); + }; + assignop('%=', 'assignmod', 20); + bitwiseassignop('&=', 'assignbitand', 20); + bitwiseassignop('|=', 'assignbitor', 20); + bitwiseassignop('^=', 'assignbitxor', 20); + bitwiseassignop('<<=', 'assignshiftleft', 20); + bitwiseassignop('>>=', 'assignshiftright', 20); + bitwiseassignop('>>>=', 'assignshiftrightunsigned', 20); + + assignop('@=', 'objCSetValueForKey', 20); + + infix('?', function (left, that) { + that.left = left; + that.right = parse(10); + advance(':'); + that['else'] = parse(10); + return that; + }, 30); + + infix('||', 'or', 40); + infix('&&', 'and', 50); + bitwise('|', 'bitor', 70); + bitwise('^', 'bitxor', 80); + bitwise('&', 'bitand', 90); + relation('==', function (left, right) { +/* // ## I don't understand these. + if (option.eqeqeq) { + warning("Expected '{a}' and instead saw '{b}'.", + this, '===', '=='); + } else if (isPoorRelation(left)) { + warning("Use '{a}' to compare with '{b}'.", + this, '===', left.value); + } else if (isPoorRelation(right)) { + warning("Use '{a}' to compare with '{b}'.", + this, '===', right.value); + } +*/ + return this; + }); + relation('==='); + relation('!=', function (left, right) { +/* // ## I don't understand these. +// if (b != true) will throw this error. Why ? + if (option.eqeqeq) { + warning("Expected '{a}' and instead saw '{b}'.", + this, '!==', '!='); + } else if (isPoorRelation(left)) { + warning("Use '{a}' to compare with '{b}'.", + this, '!==', left.value); + } else if (isPoorRelation(right)) { + warning("Use '{a}' to compare with '{b}'.", + this, '!==', right.value); + } +*/ + return this; + }); + relation('!=='); + relation('<'); + relation('>'); + relation('<='); + relation('>='); + bitwise('<<', 'shiftleft', 120); + bitwise('>>', 'shiftright', 120); + bitwise('>>>', 'shiftrightunsigned', 120); + infix('in', 'in', 120); + infix('instanceof', 'instanceof', 120); + infix('+', function (left, that) { + var right = parse(130); +/* +## String constant folding messes up token stream : logToken outputs token stream on the fly. +## Disabled. + if (left && right && left.id === '(string)' && right.id === '(string)') { + left.value += right.value; + left.character = right.character; + if (jx.test(left.value)) { + warning("JavaScript URL.", left); + } + return left; + } +*/ + that.left = left; + that.right = right; + return that; + }, 130); + prefix('+', 'num'); + infix('-', 'sub', 130); + +//## + infix('[+]', 'arrayAdd', 120); + infix('{+}', 'hashAdd', 120); + + // Handle @ prefix for NSStrings + prefix('@', function () + { + advance() + var c = token + if (token.id != '(string)') + warningAt('ObjC string immediate : Expected a Javascript string here', token.line, token.from) + + }) + + // Handle @selector + prefix('@selector', function () + { + var c = token + logExtraSyntax('@selector', token) + token.id = token.value + disableLogToken() + advance('(') + var from = nexttoken.from + var line = nexttoken.line + var selector = '' + while (nexttoken && nexttoken.value != ')' && line == nexttoken.line) + { + advance() + selector += token.value + } + // Wrong but better than nothing +// if (!selector.match(/^[\w:]+/)) +// warningAt('Invalid selector', line, from ) + + var t = { value : selector, type : '(string)', from : from, character : token.character, line : line } + t.rawValue = lines[t.line] ? lines[t.line].substr(t.from, t.character-t.from) : '' + logToken(t) + nexttoken.rawValue = lines[nexttoken.line] ? lines[nexttoken.line].substr(nexttoken.from, nexttoken.character-nexttoken.from) : '' + logToken(nexttoken) + + enableLogToken() + advance(')') + }) + + + prefix('-', 'neg'); + infix('*', 'mult', 140); + infix('/', 'div', 140); + infix('%', 'mod', 140); + + suffix('++', 'postinc'); + prefix('++', 'preinc'); + syntax['++'].exps = true; + + suffix('--', 'postdec'); + prefix('--', 'predec'); + syntax['--'].exps = true; + prefix('delete', function () { + var p = parse(0); + if (!p || (p.id !== '.' && p.id !== '[')) { + warning("Expected '{a}' and instead saw '{b}'.", + nexttoken, '.', nexttoken.value); + } + }).exps = true; + + + prefix('~', function () { + if (option.bitwise) { + warning("Unexpected '{a}'.", this, '~'); + } + parse(150); + return this; + }); + prefix('!', 'not'); + prefix('typeof', 'typeof'); + prefix('new', function () { + var c = parse(155), i; + if (c && c.id !== 'function') { + if (c.identifier) { + c['new'] = true; + switch (c.value) { + case 'Object': + warning("Use the object literal notation {}.", token); + break; + case 'Array': + warning("Use the array literal notation [].", token); + break; + case 'Number': + case 'String': + case 'Boolean': + case 'Math': + warning("Do not use {a} as a constructor.", token, c.value); + break; + case 'Function': + if (!option.evil) { + warning("The Function constructor is eval."); + } + break; + case 'Date': + case 'RegExp': + break; + default: + if (c.id !== 'function') { + i = c.value.substr(0, 1); + if (option.newcap && (i < 'A' || i > 'Z')) { + warning( + "A constructor name should start with an uppercase letter.", + token); + } + } + } + } else { + if (c.id !== '.' && c.id !== '[' && c.id !== '(') { + warning("Bad constructor.", token); + } + } + } else { + warning("Weird construction. Delete 'new'.", this); + } + adjacent(token, nexttoken); +/* + // ## Allow paramless constructors + if (nexttoken.id !== '(') { + warning("Missing '()' invoking a constructor."); + } +*/ + this.first = c; + return this; + }); + syntax['new'].exps = true; + + infix('.', function (left, that) { + adjacent(prevtoken, token); + var m = identifier(); + if (typeof m === 'string') { + countMember(m); + } + that.left = left; + that.right = m; + if (!option.evil && left && left.value === 'document' && + (m === 'write' || m === 'writeln')) { + warning("document.write can be a form of eval.", left); + } + if (option.adsafe) { + if (left && left.value === 'ADSAFE') { + if (m === 'id' || m === 'lib') { + warning("ADsafe violation.", that); + } else if (m === 'go') { + if (xmode !== 'script') { + warning("ADsafe violation.", that); + } else if (adsafe_went || nexttoken.id !== '(' || + peek(0).id !== '(string)' || + peek(0).value !== adsafe_id || + peek(1).id !== ',') { + error("ADsafe violation: go.", that); + } + adsafe_went = true; + adsafe_may = false; + } + } + } + if (option.safe) { + for (;;) { + if (banned[m] === true) { + warning("ADsafe restricted word '{a}'.", token, m); + } + if (predefined[left.value] !== true || + nexttoken.id === '(') { + break; + } + if (standard_member[m] === true) { + if (nexttoken.id === '.') { + warning("ADsafe violation.", that); + } + break; + } + if (nexttoken.id !== '.') { + warning("ADsafe violation.", that); + break; + } + advance('.'); + token.left = that; + token.right = m; + that = token; + m = identifier(); + if (typeof m === 'string') { + countMember(m); + } + } + } + return that; + }, 160, true); + + infix('(', function (left, that) { + var left = token + abut(prevtoken, token); + nospace(); + var n = 0, + p = []; + if (left) { + if (left.type === '(identifier)') { + if (left.value.match(/^[A-Z]([A-Z0-9_$]*[a-z][A-Za-z0-9_$]*)?$/)) { + if (left.value !== 'Number' && left.value !== 'String' && + left.value !== 'Boolean' && left.value !== 'Date') { + if (left.value === 'Math') { + warning("Math is not a function.", left); + } else if (option.newcap) { + warning("Missing 'new' prefix when invoking a constructor.", + left); + } + } + } + } else if (left.id === '.') { + if (option.safe && left.left.value === 'Math' && + left.right === 'random') { + warning("ADsafe violation.", left); + } + } + } + if (nexttoken.id !== ')') { + for (;;) { + // ## singleExpression + p[p.length] = parse(10, 'singleExpression'); + n += 1; + if (nexttoken.id !== ',') { + break; + } + comma(); + } + } + var lasttoken = token + advance(')'); + + // ## + var right = token + left.right = right + right.left = left + lasttoken.rightParen = right + + if (option.immed && left.id === 'function' && nexttoken.id !== ')') { + warning("Wrap the entire immediate function invocation in parens.", + that); + } + nospace(prevtoken, token); + if (typeof left === 'object') { + if (left.value === 'parseInt' && n === 1) { + warning("Missing radix parameter.", left); + } + if (!option.evil) { + if (left.value === 'eval' || left.value === 'Function' || + left.value === 'execScript') { + warning("eval is evil.", left); + } else if (p[0] && p[0].id === '(string)' && + (left.value === 'setTimeout' || + left.value === 'setInterval')) { + warning( + "Implied eval is evil. Pass a function instead of a string.", left); + } + } + if (!left.identifier && left.id !== '.' && left.id !== '[' && + left.id !== '(' && left.id !== '&&' && left.id !== '||' && + left.id !== '?') { + warning("Bad invocation.", left); + } + + } + that.left = left; + return that; + }, 155, true).exps = true; + + prefix('(', function () { + var left = token + nospace(); + var v = parse(0); + var lasttoken = token + advance(')', this); + // ## + var right = token + left.right = right + right.left = left + lasttoken.rightParen = right + + nospace(prevtoken, token); + if (option.immed && v.id === 'function') { + if (nexttoken.id === '(') { + warning( +"Move the invocation into the parens that contain the function.", nexttoken); + } else { + warning( +"Do not wrap function literals in parens unless they are to be immediately invoked.", + this); + } + } + return v; + }); + + + // + // ## ObjC call handling + // + function ObjCParams(firstToken) + { + logExtraSyntax('objc', token) +// if (token.value != ']') + + token.isObjCFirstCall = true + token.isObjCCall = true + nexttoken.isObjCCall = true + nexttoken.isObjCFirstParam = true + firstToken.isObjCCallOpener = true + firstToken.isObjCCall = true + + var isObjCSuperCall = (token.value == 'original' || token.value == 'super') + token.isObjCSuperCall = isObjCSuperCall + nexttoken.isObjCSuperCall = isObjCSuperCall + + function advanceParameterName() + { + advance() +// if (!token.value.match(/^[a-zA-Z_]\w*/)) warningAt('Invalid selector name "' + token.value + '"', token.line, token.from) + } + + var instanceToken = token + var firstMessagePart = nexttoken + + var parameterCount = 0 + // Advance parameter name + advanceParameterName() + // Parameter(s) + var jsselector = token.value + if (nexttoken.id == ':') + { + nexttoken.isObjCParameterSeparator = true + parameterCount++ + advance(':') + jsselector += '_' + // Parameter value : any javascript expression, including anon functions + parse(0) + // Remaining parameters + while (nexttoken && nexttoken.value != ']') + { + if (nexttoken.id === '(end)') return warningAt('Unexpected end of ObjC call "' + token.value + '"', token.line, token.from) + // Handle variadic calls : must be comma list, last values before ] + if (nexttoken.value == ',') + { + while (nexttoken.value == ',') + { + advance(',') + parse(0) + } + // We're done for this message + continue + } + parameterCount++ + jsselector += nexttoken.value + '_' + nexttoken.isObjCCall = true + nexttoken.isObjCMultiCall = true + advanceParameterName() + // Next token must be a semicolon + if (nexttoken.id != ':') warning("ObjC message missing last paramater") + nexttoken.isObjCParameterSeparator = true + advance(':') + // Parameter value + parse(0) + } + } + + if (parameterCount) + firstMessagePart.objCJSSelector = jsselector + + if (nexttoken.value != ']') warning('ObjC call not closed') + nexttoken.isObjCCallCloser = true + nexttoken.isObjCCall = true + nexttoken.isObjCSuperCall = isObjCSuperCall + instanceToken.objCParameterCountOpener = parameterCount + nexttoken.objCParameterCountCloser = parameterCount + // Mark expression as valid standalone (skip warning in parse(), 'Expected an assignment or function call and instead saw an expression') + nexttoken.exps = true + } + + infix('[', function (left, that) { + var firstToken = token + nospace(); + var e = parse(0), s; + if (e && e.type === '(string)') { + if (option.safe && banned[e.value] === true) { + warning("ADsafe restricted word '{a}'.", that, e.value); + } + countMember(e.value); + if (!option.sub && ix.test(e.value)) { + s = syntax[e.value]; + if (!s || !s.reserved) { + warning("['{a}'] is better written in dot notation.", + e, e.value); + } + } + } else if (!e || (e.type !== '(number)' && + (e.id !== '+' || e.arity !== 'unary'))) { + if (option.safe) { + warning('ADsafe subscripting.'); + } + } + + // ## Parse an ObjC message (as a statement) + if (nexttoken.value != ']') + ObjCParams(firstToken) + advance(']', that); + + // Mark each token as siblings of each other + token.left = firstToken + firstToken.right = token + + nospace(prevtoken, token); + that.left = left; + that.right = e; + return that; + }, 160, true); + + prefix('[', function () { + var firstToken = token + // Doesn't seem to be used + this.first = []; + // Empty array + if (nexttoken.id === ']') { + advance(']'); + return; + } + var b = token.line !== nexttoken.line; + // Indentation check + if (b) { + indent += option.indent; + if (nexttoken.from === indent + option.indent) { + indent += option.indent; + } + } + for (;;) { + // Indentation check + if (b && token.line !== nexttoken.line) { + indentation(); + } + parse(10); + if (nexttoken.id === ',') { + comma(); + if (nexttoken.id === ',') { + warning("Extra comma.", token); + } else if (nexttoken.id === ']') { + warning("Extra comma.", token); + break; + } + } else { + if (b) { + indent -= option.indent; + indentation(); + } + break; + } + } + + +//alert(dumpHashNoFunction(token) + '\n*****************\n' + dumpHashNoFunction(nexttoken)) + // ## Parse an ObjC message (as an assign expression) + if (nexttoken.value != ']') + ObjCParams(firstToken) + advance(']', this); + + // Mark each token as siblings of each other + token.left = firstToken + firstToken.right = token + + // ## as [+] can push an array immediate : a [+] ['b'] + return this; + }, 160); + + (function (x) { + x.nud = function () { + + // ## firstToken + var firstToken = token + + var b, i, s, seen = {}; + b = token.line !== nexttoken.line; + if (b) { + indent += option.indent; + if (nexttoken.from === indent + option.indent) { + indent += option.indent; + } + } + for (;;) { + if (nexttoken.id === '}') { + break; + } + if (b) { + indentation(); + } + i = optionalidentifier(true); + if (!i) { + if (nexttoken.id === '(string)') { + i = nexttoken.value; + if (ix.test(i)) { + s = syntax[i]; + } + advance(); + } else if (nexttoken.id === '(number)') { + i = nexttoken.value.toString(); + advance(); + } else { + error("Expected '{a}' and instead saw '{b}'.", + nexttoken, '}', nexttoken.value); + } + } + if (seen[i] === true) { + warning("Duplicate member '{a}'.", nexttoken, i); + } + seen[i] = true; + countMember(i); + advance(':'); + nonadjacent(token, nexttoken); + // ## Don't allow comma expressions (a, b, c) + parse(10, 'singleExpression'); + if (nexttoken.id === ',') { + comma(); + if (nexttoken.id === ',' || nexttoken.id === '}') { + warning("Extra comma.", token); + } + } else { + break; + } + } + if (b) { + indent -= option.indent; + indentation(); + } + advance('}', this); + + // ## + token.left = firstToken + firstToken.right = token + return this + + return; + }; + x.fud = function () { + error("Expected to see a statement and instead saw a block.", token); + }; + }(delim('{'))); + + + function varstatement(prefix) { + +// JavaScript does not have block scope. It only has function scope. So, +// declaring a variable in a block can have unexpected consequences. + + var name, value; + + if (funct['(onevar)'] && option.onevar) { + warning("Too many var statements."); + } else if (!funct['(global)']) { + funct['(onevar)'] = true; + } + this.first = []; + for (;;) { + nonadjacent(token, nexttoken); + addlabel(identifier(), 'unused'); + if (prefix) { + break; + } + name = token; + this.first.push(token); + if (nexttoken.id === '=') { + nonadjacent(token, nexttoken); + advance('='); + nonadjacent(token, nexttoken); + if (peek(0).id === '=' && nexttoken.identifier) { + error("Variable {a} was not declared correctly.", + nexttoken, nexttoken.value); + } + // ## 'var' + value = parse(0, 'singleExpression'); + name.first = value; + } + if (nexttoken.id !== ',') { + break; + } + comma(); + } + return this; + } + + + stmt('var', varstatement); + + stmt('new', function () { + warning("'new' should not be used as a statement."); + }); + + + function functionparams() { + var i, t = nexttoken, p = []; + advance('('); + nospace(); + if (nexttoken.id === ')') { + advance(')'); + nospace(prevtoken, token); + return; + } + for (;;) { + i = identifier(); + p.push(i); + addlabel(i, 'parameter'); + if (nexttoken.id === ',') { + comma(); + } else { + advance(')', t); + nospace(prevtoken, token); + return p.join(', '); + } + } + } + + function doFunction(i) { + var s = scope; + scope = Object.create(s); + funct = { + '(name)' : i || '"' + anonname + '"', + '(line)' : nexttoken.line + 1, + '(context)' : funct, + '(breakage)': 0, + '(loopage)' : 0, + '(scope)' : scope + }; + token.funct = funct; + + // ## + var startToken = token + logFunctionStart(token) + + functions.push(funct); + if (i) { + addlabel(i, 'function'); + } + // ## May omit params + if (nexttoken.value == '(') + funct['(params)'] = functionparams(); + block(false); + scope = s; + funct = funct['(context)']; + // ## + logFunctionEnd(startToken, token) + } + + + // ## + var functionBlockFunction = function () { + if (inblock) { + // ## Allowed ! +// warning("Function statements cannot be placed in blocks. Use a function expression or move the statement to the top of the outer function.", token); + } + var i = identifier(); + adjacent(token, nexttoken); + addlabel(i, 'unused'); + doFunction(i); + if (nexttoken.id === '(' && nexttoken.line === token.line) { + error( +"Function statements are not invocable. Wrap the whole function invocation in parens."); + } + } + blockstmt('function', functionBlockFunction); + blockstmt('ƒ', functionBlockFunction); + + // ## + var functionPrefixFunction = function () { + var i = optionalidentifier(); + if (i) { + adjacent(token, nexttoken); + } else { + nonadjacent(token, nexttoken); + } + doFunction(i); + if (funct['(loopage)'] && nexttoken.id !== '(') { + warning("Be careful when making functions within a loop. Consider putting the function in a closure."); + } + return this; + } + + prefix('function', functionPrefixFunction); + prefix('ƒ', functionPrefixFunction); + + blockstmt('if', function () { + var t = nexttoken; + advance('('); + nonadjacent(this, t); + nospace(); + parse(20); + if (nexttoken.id === '=') { + warning("Expected a conditional expression and instead saw an assignment."); + advance('='); + parse(20); + } + advance(')', t); + nospace(prevtoken, token); + block(true); + if (nexttoken.id === 'else') { + nonadjacent(token, nexttoken); + advance('else'); + if (nexttoken.id === 'if' || nexttoken.id === 'switch') { + statement(true); + } else { + block(true); + } + } + return this; + }); + + blockstmt('try', function () { + var b, e, s; + if (option.adsafe) { + warning("ADsafe try violation.", this); + } + block(false); + if (nexttoken.id === 'catch') { + advance('catch'); + nonadjacent(token, nexttoken); + advance('('); + s = scope; + scope = Object.create(s); + e = nexttoken.value; + if (nexttoken.type !== '(identifier)') { + warning("Expected an identifier and instead saw '{a}'.", + nexttoken, e); + } else { + addlabel(e, 'exception'); + } + advance(); + advance(')'); + block(false); + b = true; + scope = s; + } + if (nexttoken.id === 'finally') { + advance('finally'); + block(false); + return; + } else if (!b) { + error("Expected '{a}' and instead saw '{b}'.", + nexttoken, 'catch', nexttoken.value); + } + }); + + blockstmt('while', function () { + var t = nexttoken; + funct['(breakage)'] += 1; + funct['(loopage)'] += 1; + advance('('); + nonadjacent(this, t); + nospace(); + parse(20); + if (nexttoken.id === '=') { + warning("Expected a conditional expression and instead saw an assignment."); + advance('='); + parse(20); + } + advance(')', t); + nospace(prevtoken, token); + block(true); + funct['(breakage)'] -= 1; + funct['(loopage)'] -= 1; + }).labelled = true; + + reserve('with'); + + blockstmt('switch', function () { + var t = nexttoken, + g = false; + funct['(breakage)'] += 1; + advance('('); + nonadjacent(this, t); + nospace(); + this.condition = parse(20); + advance(')', t); + nospace(prevtoken, token); + nonadjacent(token, nexttoken); + t = nexttoken; + advance('{'); + nonadjacent(token, nexttoken); + indent += option.indent; + this.cases = []; + for (;;) { + switch (nexttoken.id) { + case 'case': + switch (funct['(verb)']) { + case 'break': + case 'case': + case 'continue': + case 'return': + case 'switch': + case 'throw': + break; + default: + warning( + "Expected a 'break' statement before 'case'.", + token); + } + indentation(-option.indent); + advance('case'); + this.cases.push(parse(20)); + g = true; + advance(':'); + funct['(verb)'] = 'case'; + break; + case 'default': + switch (funct['(verb)']) { + case 'break': + case 'continue': + case 'return': + case 'throw': + break; + default: + warning( + "Expected a 'break' statement before 'default'.", + token); + } + indentation(-option.indent); + advance('default'); + g = true; + advance(':'); + break; + case '}': + indent -= option.indent; + indentation(); + advance('}', t); + if (this.cases.length === 1 || this.condition.id === 'true' || + this.condition.id === 'false') { + warning("This 'switch' should be an 'if'.", this); + } + funct['(breakage)'] -= 1; + funct['(verb)'] = undefined; + return; + case '(end)': + error("Missing '{a}'.", nexttoken, '}'); + return; + default: + if (g) { + switch (token.id) { + case ',': + error("Each value should have its own case label."); + return; + case ':': + statements(); + break; + default: + error("Missing ':' on a case clause.", token); + } + } else { + error("Expected '{a}' and instead saw '{b}'.", + nexttoken, 'case', nexttoken.value); + } + } + } + }).labelled = true; + + stmt('debugger', function () { + if (!option.debug) { + warning("All 'debugger' statements should be removed."); + } + }); + + stmt('do', function () { + funct['(breakage)'] += 1; + funct['(loopage)'] += 1; + block(true); + advance('while'); + var t = nexttoken; + nonadjacent(token, t); + advance('('); + nospace(); + parse(20); + if (nexttoken.id === '=') { + warning("Expected a conditional expression and instead saw an assignment."); + advance('='); + parse(20); + } + advance(')', t); + nospace(prevtoken, token); + funct['(breakage)'] -= 1; + funct['(loopage)'] -= 1; + }).labelled = true; + + blockstmt('for', function () { + var s, t = nexttoken; + funct['(breakage)'] += 1; + funct['(loopage)'] += 1; + advance('('); + nonadjacent(this, t); + nospace(); + if (peek(nexttoken.id === 'var' ? 1 : 0).id === 'in') { + if (nexttoken.id === 'var') { + advance('var'); + varstatement(true); + } else { + switch (funct[nexttoken.value]) { + case 'unused': + funct[nexttoken.value] = 'var'; + break; + case 'var': + break; + default: + warning("Bad for in variable '{a}'.", + nexttoken, nexttoken.value); + } + advance(); + } + advance('in'); + parse(20); + advance(')', t); + s = block(true); + if (!option.forin && (s.length > 1 || typeof s[0] !== 'object' || + s[0].value !== 'if')) { + warning("The body of a for in should be wrapped in an if statement to filter unwanted properties from the prototype.", this); + } + funct['(breakage)'] -= 1; + funct['(loopage)'] -= 1; + return this; + } else { + if (nexttoken.id !== ';') { + if (nexttoken.id === 'var') { + advance('var'); + varstatement(); + } else { + for (;;) { + parse(0, 'for'); + if (nexttoken.id !== ',') { + break; + } + comma(); + } + } + } + nolinebreak(token); + advance(';'); + if (nexttoken.id !== ';') { + parse(20); + if (nexttoken.id === '=') { + warning("Expected a conditional expression and instead saw an assignment."); + advance('='); + parse(20); + } + } + nolinebreak(token); + advance(';'); + if (nexttoken.id === ';') { + error("Expected '{a}' and instead saw '{b}'.", + nexttoken, ')', ';'); + } + if (nexttoken.id !== ')') { + for (;;) { + parse(0, 'for'); + if (nexttoken.id !== ',') { + break; + } + comma(); + } + } + advance(')', t); + nospace(prevtoken, token); + block(true); + funct['(breakage)'] -= 1; + funct['(loopage)'] -= 1; + } + }).labelled = true; + + + stmt('break', function () { + var v = nexttoken.value; + if (funct['(breakage)'] === 0) { + warning("Unexpected '{a}'.", nexttoken, this.value); + } + // ## Allow missing semicolon + if (this.line !== nexttoken.line) return + nolinebreak(this); + if (nexttoken.id !== ';') { + if (token.line === nexttoken.line) { + if (funct[v] !== 'label') { + warning("'{a}' is not a statement label.", nexttoken, v); + } else if (scope[v] !== funct) { + warning("'{a}' is out of scope.", nexttoken, v); + } + advance(); + } + } + reachable('break'); + }); + + + stmt('continue', function () { + var v = nexttoken.value; + if (funct['(breakage)'] === 0) { + warning("Unexpected '{a}'.", nexttoken, this.value); + } + // ## Allow missing semicolon + if (this.line !== nexttoken.line) return + nolinebreak(this); + if (nexttoken.id !== ';') { + if (token.line === nexttoken.line) { + if (funct[v] !== 'label') { + warning("'{a}' is not a statement label.", nexttoken, v); + } else if (scope[v] !== funct) { + warning("'{a}' is out of scope.", nexttoken, v); + } + advance(); + } + } + reachable('continue'); + }); + + + stmt('return', function () { + // ## Allow 'return' without semicolon +// nolinebreak(this); + + if (nexttoken.id === '(regexp)') { + warning("Wrap the /regexp/ literal in parens to disambiguate the slash operator."); + } + + // Ruby-like if return : everything must fit on the line + // return exp if (exp) + // return exp unless (exp) + function isIfReturn() + { + var b = (nexttoken.id == 'if' || nexttoken.value == 'unless') && token.line == nexttoken.line + returnToken.isIfReturn = b + return b + } + function parseIfReturn() + { + logExtraSyntax('ifReturn', token) + if (nexttoken.id == 'if') advance('if') + else advance('unless') + token.isIfReturnOpener = token + // Manually mark unless as reserved + token.reserved = true + advance('(') + parse(20) + advance(')') + token.isIfReturnCloser = token + } + + var returnToken = token + // Maybe if return + if (isIfReturn()) { + parseIfReturn() + } + else + if (nexttoken.id !== ';' && !nexttoken.reach /*## only parse what's on current line */ && token.line == nexttoken.line) { + nonadjacent(token, nexttoken); + parse(20); +/* + // ## Allow multiple expression return + // return a(), b(), 'hello' + while (nexttoken.value == ',') + { + advance(',') + parse(20) + } +*/ + // ## Maybe if return + if (isIfReturn()) parseIfReturn() + } + + reachable('return'); + }); + + + stmt('throw', function () { + nolinebreak(this); + nonadjacent(token, nexttoken); + parse(20); + reachable('throw'); + }); + + // ## JSCocoa class syntax + function parseObjJClass(style) { + // Protect against inner definitions + if (parsingClass) return warningAt('Inner classes are not of this world', token.line, token.from) + parsingClass = true + + logExtraSyntax('class', token) + token.isObjCClassStart = true + advance() + var className = token + if (className.type != '(identifier)') warningAt('Class name must be an identifier', token.line, token.from) + // If we have a '<', we're deriving from the following class. + // If not, we're just adding methods to the class. + if (nexttoken.value == '<' || nexttoken.value == ':') + { + if (style == '@implementation') advance(':') + else advance('<') + var parentClassName = token + advance() + if (token.type != '(identifier)') warningAt('Parent class name must be an identifier', token.line, token.from) + } + else + // Category + if (nexttoken.value == '(') + { + advance('(') + token.isObjCCategory = true + advance() + if (token.type != '(identifier)') warningAt('Category name must be an identifier', token.line, token.from) + advance(')') + } + // Parse class params + if (style == '@implementation' && nexttoken.value == '{') + { + advance('{') + token.isObjCVarList = true + // Pretty basic for now : skipping everything + while (nexttoken && nexttoken.value != '}') + { + if (nexttoken.id === '(end)') return warningAt('Unexpected end of ObjC call "' + token.value + '"', token.line, token.from) + advance() + } + advance('}') + } + + if (style != '@implementation') advance('{') + + var validTokens = { '-' : true, '+' : true, 'IBOutlet' : true, 'IBAction' : true, 'swizzle' : true, 'Swizzle' : true, 'Key' : true, 'function' : true, 'ƒ' : true } + var parsingClassDefinition = true + + + function type() + { + var line = nexttoken.line + advance('(') + var type = '' + var typeTokenCount = 0 + var startToken = nexttoken + while (nexttoken && nexttoken.value != ')' && nexttoken.line == line) + { + advance() + type += token.value + typeTokenCount++ + } + if (!typeTokenCount) warningAt('Missing parameter type', token.line, token.from) + var endToken = token + if (endToken.line != token.line) warningAt('Parameter must be defined on one line', token.line, token.from) + type = lines[startToken.line].substr(startToken.from, endToken.character-startToken.from) + encodings.push("'" + type + "'") + advance(')') + } + + while (validTokens[nexttoken.value] && parsingClassDefinition) + { + nexttoken.isObjCClassItemStart = true + var dataHolder = nexttoken + + // Advance type + advance() + // Advance method swizzler + if (token.value.toLowerCase() == 'swizzle') token.reserved = true, advance() + + // Method + if (token.value == '-' || token.value == '+') + { + var methodName = '' + var encodings = [] + var paramNames = [] + // Advance return type + type() + + nexttoken.isObjCCall = true + advance() + methodName += token.value + + var needToParseNameAndType = false + while (nexttoken && (nexttoken.value == ':' || needToParseNameAndType)) + { + advance(':') + methodName += ':' + type() + // param name + advance() + paramNames.push(token.value) + needToParseNameAndType = false + if (nexttoken.type == '(identifier)') + { + needToParseNameAndType = true + nexttoken.isObjCCall = true + advance() + methodName += token.value + } + } + dataHolder.objCMethodName = methodName + dataHolder.objCMethodEncodings = encodings + dataHolder.objCMethodParamNames = paramNames + + block(false) + } + // Outlet + else if (token.value == 'IBOutlet') + { + token.reserved = true + // Outlet name + advance() + // Outlet setter name may have been redefined + if (nexttoken.value == '(') + { + advance('(') + advance() + if (token.type != '(identifier)') warningAt('Outlet param must be an identifier', token.line, token.from) + advance(')') + block(0) + } + } + // Action : name + possible parameter name + else if (token.value == 'IBAction') + { + token.reserved = true + // Action name + advance() + // Param name may have been redefined + if (nexttoken.value == '(') + { + advance('(') + advance() + if (token.type != '(identifier)') warningAt('Outlet param must be an identifier', token.line, token.from) + advance(')') + } + block(0) + } + // Key + else if (token.value == 'Key') + { + token.reserved = true + // Key name + advance() + if (token.type != '(identifier)') warningAt('Key param must be an identifier', token.line, token.from) + } + // Javascript function + else if (token.value == 'function' || token.value == 'ƒ') + { + var jsfn = token + jsfn.isClassJSFunction = true + var i = identifier() + jsfn.jsFunctionName = token + doFunction(i) + } + else + { + warningAt('missing valid token handler for "' + token.value + '" while parsing JSCocoa class', token.line, token.from) + parsingClassDefinition = false + } + } + + if (style == '@implementation') + { + // Advance only checks token.id, @end is stored in token.value + if (nexttoken.value == '@end') nexttoken.id = nexttoken.value + advance('@end') + } + else advance('}') + parsingClass = false + } + stmt('class', parseObjJClass); + + stmt('@implementation', function () { + parseObjJClass('@implementation') + }) + + + reserve('void'); + +// Superfluous reserved words + +// reserve('class'); + reserve('const'); + reserve('enum'); + reserve('export'); + reserve('extends'); + reserve('float'); + reserve('goto'); + reserve('import'); + reserve('let'); + // ## We use this for ObjJ super call +// reserve('super'); + + function jsonValue() { + function jsonObject() { + var o = {}, t = nexttoken; + advance('{'); + if (nexttoken.id !== '}') { + for (;;) { + if (nexttoken.id === '(end)') { + error("Missing '}' to match '{' from line {a}.", + nexttoken, t.line + 1); + } else if (nexttoken.id === '}') { + warning("Unexpected comma.", token); + break; + } else if (nexttoken.id === ',') { + error("Unexpected comma.", nexttoken); + } else if (nexttoken.id !== '(string)') { + warning("Expected a string and instead saw {a}.", + nexttoken, nexttoken.value); + } + if (o[nexttoken.value] === true) { + warning("Duplicate key '{a}'.", + nexttoken, nexttoken.value); + } else if (nexttoken.value === '__proto__') { + warning("Stupid key '{a}'.", + nexttoken, nexttoken.value); + } else { + o[nexttoken.value] = true; + } + advance(); +//## + advance(':'); + jsonValue(); + if (nexttoken.id !== ',') { + break; + } + advance(','); + } + } + advance('}'); + } + + function jsonArray() { + var t = nexttoken; + advance('['); + + if (nexttoken.id !== ']') { + for (;;) { + if (nexttoken.id === '(end)') { + error("Missing ']' to match '[' from line {a}.", + nexttoken, t.line + 1); + } else if (nexttoken.id === ']') { + warning("Unexpected comma.", token); + break; + } else if (nexttoken.id === ',') { + error("Unexpected comma.", nexttoken); + } + jsonValue(); + if (nexttoken.id !== ',') { + break; + } + advance(','); + } + } + advance(']'); + } + switch (nexttoken.id) { + case '{': + jsonObject(); + break; + case '[': + jsonArray(); + break; + case 'true': + case 'false': + case 'null': + case '(number)': + case '(string)': + advance(); + break; + case '-': + advance('-'); + if (token.character !== nexttoken.from) { + warning("Unexpected space after '-'.", token); + } + adjacent(token, nexttoken); + advance('(number)'); + break; + default: + error("Expected a JSON value.", nexttoken); + } + } + + +// The actual JSLINT function itself. + + var itself = function (s, o) { + var a, i; + JSLINT.errors = []; + predefined = Object.create(standard); + if (o) { + a = o.predef; + if (a instanceof Array) { + for (i = 0; i < a.length; i += 1) { + predefined[a[i]] = true; + } + } + if (o.adsafe) { + o.safe = true; + } + if (o.safe) { + o.browser = false; + o.css = false; + o.debug = false; + o.eqeqeq = true; + o.evil = false; + o.forin = false; + o.nomen = true; + o.on = false; + o.rhino = false; + o.safe = true; + o.sidebar = false; + o.strict = true; + o.sub = false; + o.undef = true; + o.widget = false; + predefined.Date = false; + predefined['eval'] = false; + predefined.Function = false; + predefined.Object = false; + predefined.ADSAFE = true; + predefined.lib = true; + } + option = o; + } else { + option = {}; + } + option.indent = option.indent || 4; + adsafe_id = ''; + adsafe_may = false; + adsafe_went = false; + approved = {}; + if (option.approved) { + for (i = 0; i < option.approved.length; i += 1) { + approved[option.approved[i]] = option.approved[i]; + } + } else { + approved.test = 'test'; + } + tab = ''; + for (i = 0; i < option.indent; i += 1) { + tab += ' '; + } + indent = 0; + global = Object.create(predefined); + scope = global; + funct = { + '(global)': true, + '(name)': '(global)', + '(scope)': scope, + '(breakage)': 0, + '(loopage)': 0 + }; + functions = []; + ids = {}; + urls = []; + src = false; + xmode = false; + stack = null; + member = {}; + membersOnly = null; + implied = {}; + inblock = false; + lookahead = []; + jsonmode = false; + warnings = 0; + lex.init(s); + prereg = true; + + // ## (internal) Guard against Inner class + parsingClass = false + + prevtoken = token = nexttoken = syntax['(begin)']; + assume(); + + try { + advance(); + if (nexttoken.value.charAt(0) === '<') { + html(); + if (option.adsafe && !adsafe_went) { + warning("ADsafe violation: Missing ADSAFE.go.", this); + } + } else { + switch (nexttoken.id) { +/* + // ## Don't handle json values as they interfere with ObjC messaging + case '{': + case '[': + option.laxbreak = true; + jsonmode = true; + jsonValue(); + break;*/ +/* + // ## Don't handle css classes as @implementation is valid ObjJ syntax + case '@': + case '*': + case '#': + case '.': + case ':': + xmode = 'style'; + advance(); + if (token.id !== '@' || !nexttoken.identifier || + nexttoken.value !== 'charset') { + error('A css file should begin with @charset "UTF-8";'); + } + advance(); + if (nexttoken.type !== '(string)' && + nexttoken.value !== 'UTF-8') { + error('A css file should begin with @charset "UTF-8";'); + } + advance(); + advance(';'); + styles(); + break; +*/ + default: + if (option.adsafe && option.fragment) { + error("Expected '{a}' and instead saw '{b}'.", + nexttoken, '
', nexttoken.value); + } + statements('lib'); + } + } + advance('(end)'); + } catch (e) { + if (e) { + JSLINT.errors.push({ + reason : e.message, + line : e.line || nexttoken.line, + // ## fails on alert(' + character : e.character || (nexttoken ? nexttoken.from : 0) + }, null); + } + } + return JSLINT.errors.length === 0; + }; + + function to_array(o) { + var a = [], k; + for (k in o) { + if (o.hasOwnProperty(k)) { + a.push(k); + } + } + return a; + } + +// Report generator. + + itself.report = function (option, sep) { + var a = [], c, e, f, i, k, l, m = '', n, o = [], s, v, + cl, ex, va, un, ou, gl, la; + + function detail(h, s, sep) { + if (s.length) { + o.push('
' + h + ' ' + + s.sort().join(sep || ', ') + '
'); + } + } + + s = to_array(implied); + + k = JSLINT.errors.length; + if (k || s.length > 0) { + o.push('
Error:'); + if (s.length > 0) { + s.sort(); + for (i = 0; i < s.length; i += 1) { + s[i] = '' + s[i] + ' ' + + implied[s[i]].join(' ') + + ''; + } + o.push('

Implied global: ' + s.join(', ') + '

'); + c = true; + } + for (i = 0; i < k; i += 1) { + c = JSLINT.errors[i]; + if (c) { + e = c.evidence || ''; + o.push('

Problem' + (isFinite(c.line) ? ' at line ' + (c.line + 1) + + ' character ' + (c.character + 1) : '') + + ': ' + c.reason.entityify() + + '

' + + (e && (e.length > 80 ? e.slice(0, 77) + '...' : + e).entityify()) + '

'); + } + } + o.push('
'); + if (!c) { + return o.join(''); + } + } + + if (!option) { + + o.push('
'); + + if (urls.length > 0) { + detail("URLs
", urls, '
'); + } + + s = to_array(scope); + if (s.length === 0) { + if (jsonmode) { + if (k === 0) { + o.push('

JSON: good.

'); + } else { + o.push('

JSON: bad.

'); + } + } else { + o.push('
No new global variables introduced.
'); + } + } else { + o.push('
Global ' + s.sort().join(', ') + '
'); + } + + for (i = 0; i < functions.length; i += 1) { + f = functions[i]; + cl = []; + ex = []; + va = []; + un = []; + ou = []; + gl = []; + la = []; + for (k in f) { + if (f.hasOwnProperty(k) && k.charAt(0) !== '(') { + v = f[k]; + switch (v) { + case 'closure': + cl.push(k); + break; + case 'exception': + ex.push(k); + break; + case 'var': + va.push(k); + break; + case 'unused': + un.push(k); + break; + case 'label': + la.push(k); + break; + case 'outer': + ou.push(k); + break; + case true: + gl.push(k); + break; + } + } + } + o.push('
' + f['(line)'] + ' ' + + (f['(name)'] || '') + '(' + + (f['(params)'] || '') + ')
'); + detail('Closure', cl); + detail('Variable', va); + detail('Exception', ex); + detail('Outer', ou); + detail('Global', gl); + detail('Unused', un); + detail('Label', la); + } + a = []; + for (k in member) { + if (typeof member[k] === 'number') { + a.push(k); + } + } + if (a.length) { + a = a.sort(); + m = '
/*members ';
+                l = 10;
+                for (i = 0; i < a.length; i += 1) {
+                    k = a[i];
+                    n = k.name();
+                    if (l + n.length > 72) {
+                        o.push(m + '
'); + m = ' '; + l = 1; + } + l += n.length + 2; + if (member[k] === 1) { + n = '' + n + ''; + } + if (i < a.length - 1) { + n += ', '; + } + m += n; + } + o.push(m + '
*/
'); + } + o.push('
'); + } + return o.join(''); + }; + + itself.edition = '2009-05-06'; + + var JSLINTGLOBALS = {} + var _varstatement = varstatement + varstatement = function () { + return _varstatement.apply(JSLINTGLOBALS, arguments) + } + return itself; + +}); + + return JSLINT +} \ No newline at end of file diff --git a/main.m b/main.m index dd7a01760..39a150df6 100644 --- a/main.m +++ b/main.m @@ -8,6 +8,7 @@ #import #import +#import "JSCocoa.h" int main(int argc, char *argv[]) {