commit 2cbece8593253b99db6620945508e87caf95d02a Author: Stuart Stent Date: Fri Apr 4 11:21:58 2014 -0400 backing up sublime settings diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82c1c45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +Cache/ +Index/ +Local/ diff --git a/Installed Packages/Expand Tabs on Save.sublime-package b/Installed Packages/Expand Tabs on Save.sublime-package new file mode 100644 index 0000000..5af3921 Binary files /dev/null and b/Installed Packages/Expand Tabs on Save.sublime-package differ diff --git a/Installed Packages/GitGutter.sublime-package b/Installed Packages/GitGutter.sublime-package new file mode 100644 index 0000000..df7627e Binary files /dev/null and b/Installed Packages/GitGutter.sublime-package differ diff --git a/Installed Packages/HTMLBeautify.sublime-package b/Installed Packages/HTMLBeautify.sublime-package new file mode 100644 index 0000000..50324ba Binary files /dev/null and b/Installed Packages/HTMLBeautify.sublime-package differ diff --git a/Installed Packages/Jade.sublime-package b/Installed Packages/Jade.sublime-package new file mode 100644 index 0000000..5f03edb Binary files /dev/null and b/Installed Packages/Jade.sublime-package differ diff --git a/Installed Packages/LiveStyle.sublime-package b/Installed Packages/LiveStyle.sublime-package new file mode 100644 index 0000000..f199885 Binary files /dev/null and b/Installed Packages/LiveStyle.sublime-package differ diff --git a/Installed Packages/Package Control.sublime-package b/Installed Packages/Package Control.sublime-package new file mode 100644 index 0000000..cc9aa19 Binary files /dev/null and b/Installed Packages/Package Control.sublime-package differ diff --git a/Installed Packages/PyV8/linux64-p3/PyV8.py b/Installed Packages/PyV8/linux64-p3/PyV8.py new file mode 100644 index 0000000..15e1fdb --- /dev/null +++ b/Installed Packages/PyV8/linux64-p3/PyV8.py @@ -0,0 +1,2750 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + + +import sys, os, re +import logging +import collections + +is_py3k = sys.version_info[0] > 2 + +if is_py3k: + import _thread as thread + + from io import StringIO + + str = str + raw_input = input +else: + import _thread + +try: + from io import StringIO +except ImportError: + from io import StringIO + +try: + import json +except ImportError: + import simplejson as json + +import _PyV8 + +__author__ = 'Flier Lu ' +__version__ = '1.0' + +__all__ = ["ReadOnly", "DontEnum", "DontDelete", "Internal", + "JSError", "JSObject", "JSArray", "JSFunction", + "JSClass", "JSEngine", "JSContext", + "JSObjectSpace", "JSAllocationAction", + "JSStackTrace", "JSStackFrame", "profiler", + "JSExtension", "JSLocker", "JSUnlocker", "AST"] + +class JSAttribute(object): + def __init__(self, name): + self.name = name + + def __call__(self, func): + setattr(func, "__%s__" % self.name, True) + + return func + +ReadOnly = JSAttribute(name='readonly') +DontEnum = JSAttribute(name='dontenum') +DontDelete = JSAttribute(name='dontdel') +Internal = JSAttribute(name='internal') + +class JSError(Exception): + def __init__(self, impl): + Exception.__init__(self) + + self._impl = impl + + def __str__(self): + return str(self._impl) + + def __unicode__(self, *args, **kwargs): + return str(self._impl) + + def __getattribute__(self, attr): + impl = super(JSError, self).__getattribute__("_impl") + + try: + return getattr(impl, attr) + except AttributeError: + return super(JSError, self).__getattribute__(attr) + + RE_FRAME = re.compile(r"\s+at\s(?:new\s)?(?P.+)\s\((?P[^:]+):?(?P\d+)?:?(?P\d+)?\)") + RE_FUNC = re.compile(r"\s+at\s(?:new\s)?(?P.+)\s\((?P[^\)]+)\)") + RE_FILE = re.compile(r"\s+at\s(?P[^:]+):?(?P\d+)?:?(?P\d+)?") + + @staticmethod + def parse_stack(value): + stack = [] + + def int_or_nul(value): + return int(value) if value else None + + for line in value.split('\n')[1:]: + m = JSError.RE_FRAME.match(line) + + if m: + stack.append((m.group('func'), m.group('file'), int_or_nul(m.group('row')), int_or_nul(m.group('col')))) + continue + + m = JSError.RE_FUNC.match(line) + + if m: + stack.append((m.group('func'), m.group('file'), None, None)) + continue + + m = JSError.RE_FILE.match(line) + + if m: + stack.append((None, m.group('file'), int_or_nul(m.group('row')), int_or_nul(m.group('col')))) + continue + + assert line + + return stack + + @property + def frames(self): + return self.parse_stack(self.stackTrace) + +_PyV8._JSError._jsclass = JSError + +JSObject = _PyV8.JSObject +JSArray = _PyV8.JSArray +JSFunction = _PyV8.JSFunction + +# contribute by e.generalov + +JS_ESCAPABLE = re.compile(r'([^\x00-\x7f])') +HAS_UTF8 = re.compile(r'[\x80-\xff]') + +def _js_escape_unicode_re_callack(match): + n = ord(match.group(0)) + if n < 0x10000: + return '\\u%04x' % (n,) + else: + # surrogate pair + n -= 0x10000 + s1 = 0xd800 | ((n >> 10) & 0x3ff) + s2 = 0xdc00 | (n & 0x3ff) + return '\\u%04x\\u%04x' % (s1, s2) + +def js_escape_unicode(text): + """Return an ASCII-only representation of a JavaScript string""" + if isinstance(text, str): + if HAS_UTF8.search(text) is None: + return text + + text = text.decode('UTF-8') + + return str(JS_ESCAPABLE.sub(_js_escape_unicode_re_callack, text)) + +class JSExtension(_PyV8.JSExtension): + def __init__(self, name, source, callback=None, dependencies=[], register=True): + _PyV8.JSExtension.__init__(self, js_escape_unicode(name), js_escape_unicode(source), callback, dependencies, register) + +def func_apply(self, thisArg, argArray=[]): + if isinstance(thisArg, JSObject): + return self.invoke(thisArg, argArray) + + this = JSContext.current.eval("(%s)" % json.dumps(thisArg)) + + return self.invoke(this, argArray) + +JSFunction.apply = func_apply + +class JSLocker(_PyV8.JSLocker): + def __enter__(self): + self.enter() + + if JSContext.entered: + self.leave() + raise RuntimeError("Lock should be acquired before enter the context") + + return self + + def __exit__(self, exc_type, exc_value, traceback): + if JSContext.entered: + self.leave() + raise RuntimeError("Lock should be released after leave the context") + + self.leave() + + if is_py3k: + def __bool__(self): + return self.entered() + else: + def __nonzero__(self): + return self.entered() + +class JSUnlocker(_PyV8.JSUnlocker): + def __enter__(self): + self.enter() + + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.leave() + + if is_py3k: + def __bool__(self): + return self.entered() + else: + def __nonzero__(self): + return self.entered() + +class JSClass(object): + __properties__ = {} + __watchpoints__ = {} + + def __getattr__(self, name): + if name == 'constructor': + return JSClassConstructor(self.__class__) + + if name == 'prototype': + return JSClassPrototype(self.__class__) + + prop = self.__dict__.setdefault('__properties__', {}).get(name, None) + + if prop and isinstance(prop[0], collections.Callable): + return prop[0]() + + raise AttributeError(name) + + def __setattr__(self, name, value): + prop = self.__dict__.setdefault('__properties__', {}).get(name, None) + + if prop and isinstance(prop[1], collections.Callable): + return prop[1](value) + + return object.__setattr__(self, name, value) + + def toString(self): + "Returns a string representation of an object." + return "[object %s]" % self.__class__.__name__ + + def toLocaleString(self): + "Returns a value as a string value appropriate to the host environment's current locale." + return self.toString() + + def valueOf(self): + "Returns the primitive value of the specified object." + return self + + def hasOwnProperty(self, name): + "Returns a Boolean value indicating whether an object has a property with the specified name." + return hasattr(self, name) + + def isPrototypeOf(self, obj): + "Returns a Boolean value indicating whether an object exists in the prototype chain of another object." + raise NotImplementedError() + + def __defineGetter__(self, name, getter): + "Binds an object's property to a function to be called when that property is looked up." + self.__properties__[name] = (getter, self.__lookupSetter__(name)) + + def __lookupGetter__(self, name): + "Return the function bound as a getter to the specified property." + return self.__properties__.get(name, (None, None))[0] + + def __defineSetter__(self, name, setter): + "Binds an object's property to a function to be called when an attempt is made to set that property." + self.__properties__[name] = (self.__lookupGetter__(name), setter) + + def __lookupSetter__(self, name): + "Return the function bound as a setter to the specified property." + return self.__properties__.get(name, (None, None))[1] + + def watch(self, prop, handler): + "Watches for a property to be assigned a value and runs a function when that occurs." + self.__watchpoints__[prop] = handler + + def unwatch(self, prop): + "Removes a watchpoint set with the watch method." + del self.__watchpoints__[prop] + +class JSClassConstructor(JSClass): + def __init__(self, cls): + self.cls = cls + + @property + def name(self): + return self.cls.__name__ + + def toString(self): + return "function %s() {\n [native code]\n}" % self.name + + def __call__(self, *args, **kwds): + return self.cls(*args, **kwds) + +class JSClassPrototype(JSClass): + def __init__(self, cls): + self.cls = cls + + @property + def constructor(self): + return JSClassConstructor(self.cls) + + @property + def name(self): + return self.cls.__name__ + +class JSDebugProtocol(object): + """ + Support the V8 debugger JSON based protocol. + + + """ + class Packet(object): + REQUEST = 'request' + RESPONSE = 'response' + EVENT = 'event' + + def __init__(self, payload): + self.data = json.loads(payload) if type(payload) in [str, str] else payload + + @property + def seq(self): + return self.data['seq'] + + @property + def type(self): + return self.data['type'] + + class Request(Packet): + @property + def cmd(self): + return self.data['command'] + + @property + def args(self): + return self.data['args'] + + class Response(Packet): + @property + def request_seq(self): + return self.data['request_seq'] + + @property + def cmd(self): + return self.data['command'] + + @property + def body(self): + return self.data['body'] + + @property + def running(self): + return self.data['running'] + + @property + def success(self): + return self.data['success'] + + @property + def message(self): + return self.data['message'] + + class Event(Packet): + @property + def event(self): + return self.data['event'] + + @property + def body(self): + return self.data['body'] + + def __init__(self): + self.seq = 0 + + def nextSeq(self): + seq = self.seq + self.seq += 1 + + return seq + + def parsePacket(self, payload): + obj = json.loads(payload) + + return JSDebugProtocol.Event(obj) if obj['type'] == 'event' else JSDebugProtocol.Response(obj) + +class JSDebugEvent(_PyV8.JSDebugEvent): + class FrameData(object): + def __init__(self, frame, count, name, value): + self.frame = frame + self.count = count + self.name = name + self.value = value + + def __len__(self): + return self.count(self.frame) + + def __iter__(self): + for i in range(self.count(self.frame)): + yield (self.name(self.frame, i), self.value(self.frame, i)) + + class Frame(object): + def __init__(self, frame): + self.frame = frame + + @property + def index(self): + return int(self.frame.index()) + + @property + def function(self): + return self.frame.func() + + @property + def receiver(self): + return self.frame.receiver() + + @property + def isConstructCall(self): + return bool(self.frame.isConstructCall()) + + @property + def isDebuggerFrame(self): + return bool(self.frame.isDebuggerFrame()) + + @property + def argumentCount(self): + return int(self.frame.argumentCount()) + + def argumentName(self, idx): + return str(self.frame.argumentName(idx)) + + def argumentValue(self, idx): + return self.frame.argumentValue(idx) + + @property + def arguments(self): + return JSDebugEvent.FrameData(self, self.argumentCount, self.argumentName, self.argumentValue) + + def localCount(self, idx): + return int(self.frame.localCount()) + + def localName(self, idx): + return str(self.frame.localName(idx)) + + def localValue(self, idx): + return self.frame.localValue(idx) + + @property + def locals(self): + return JSDebugEvent.FrameData(self, self.localCount, self.localName, self.localValue) + + @property + def sourcePosition(self): + return self.frame.sourcePosition() + + @property + def sourceLine(self): + return int(self.frame.sourceLine()) + + @property + def sourceColumn(self): + return int(self.frame.sourceColumn()) + + @property + def sourceLineText(self): + return str(self.frame.sourceLineText()) + + def evaluate(self, source, disable_break = True): + return self.frame.evaluate(source, disable_break) + + @property + def invocationText(self): + return str(self.frame.invocationText()) + + @property + def sourceAndPositionText(self): + return str(self.frame.sourceAndPositionText()) + + @property + def localsText(self): + return str(self.frame.localsText()) + + def __str__(self): + return str(self.frame.toText()) + + class Frames(object): + def __init__(self, state): + self.state = state + + def __len__(self): + return self.state.frameCount + + def __iter__(self): + for i in range(self.state.frameCount): + yield self.state.frame(i) + + class State(object): + def __init__(self, state): + self.state = state + + @property + def frameCount(self): + return int(self.state.frameCount()) + + def frame(self, idx = None): + return JSDebugEvent.Frame(self.state.frame(idx)) + + @property + def selectedFrame(self): + return int(self.state.selectedFrame()) + + @property + def frames(self): + return JSDebugEvent.Frames(self) + + def __repr__(self): + s = StringIO() + + try: + for frame in self.frames: + s.write(str(frame)) + + return s.getvalue() + finally: + s.close() + + class DebugEvent(object): + pass + + class StateEvent(DebugEvent): + __state = None + + @property + def state(self): + if not self.__state: + self.__state = JSDebugEvent.State(self.event.executionState()) + + return self.__state + + class BreakEvent(StateEvent): + type = _PyV8.JSDebugEvent.Break + + def __init__(self, event): + self.event = event + + class ExceptionEvent(StateEvent): + type = _PyV8.JSDebugEvent.Exception + + def __init__(self, event): + self.event = event + + class NewFunctionEvent(DebugEvent): + type = _PyV8.JSDebugEvent.NewFunction + + def __init__(self, event): + self.event = event + + class Script(object): + def __init__(self, script): + self.script = script + + @property + def source(self): + return self.script.source() + + @property + def id(self): + return self.script.id() + + @property + def name(self): + return self.script.name() + + @property + def lineOffset(self): + return self.script.lineOffset() + + @property + def lineCount(self): + return self.script.lineCount() + + @property + def columnOffset(self): + return self.script.columnOffset() + + @property + def type(self): + return self.script.type() + + def __repr__(self): + return "<%s script %s @ %d:%d> : '%s'" % (self.type, self.name, + self.lineOffset, self.columnOffset, + self.source) + + class CompileEvent(StateEvent): + def __init__(self, event): + self.event = event + + @property + def script(self): + if not hasattr(self, "_script"): + setattr(self, "_script", JSDebugEvent.Script(self.event.script())) + + return self._script + + def __str__(self): + return str(self.script) + + class BeforeCompileEvent(CompileEvent): + type = _PyV8.JSDebugEvent.BeforeCompile + + def __init__(self, event): + JSDebugEvent.CompileEvent.__init__(self, event) + + def __repr__(self): + return "before compile script: %s\n%s" % (repr(self.script), repr(self.state)) + + class AfterCompileEvent(CompileEvent): + type = _PyV8.JSDebugEvent.AfterCompile + + def __init__(self, event): + JSDebugEvent.CompileEvent.__init__(self, event) + + def __repr__(self): + return "after compile script: %s\n%s" % (repr(self.script), repr(self.state)) + + onMessage = None + onBreak = None + onException = None + onNewFunction = None + onBeforeCompile = None + onAfterCompile = None + +class JSDebugger(JSDebugProtocol, JSDebugEvent): + def __init__(self): + JSDebugProtocol.__init__(self) + JSDebugEvent.__init__(self) + + def __enter__(self): + self.enabled = True + + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.enabled = False + + @property + def context(self): + if not hasattr(self, '_context'): + self._context = JSContext(ctxt=_PyV8.debug().context) + + return self._context + + def isEnabled(self): + return _PyV8.debug().enabled + + def setEnabled(self, enable): + dbg = _PyV8.debug() + + if enable: + dbg.onDebugEvent = self.onDebugEvent + dbg.onDebugMessage = self.onDebugMessage + dbg.onDispatchDebugMessages = self.onDispatchDebugMessages + else: + dbg.onDebugEvent = None + dbg.onDebugMessage = None + dbg.onDispatchDebugMessages = None + + dbg.enabled = enable + + enabled = property(isEnabled, setEnabled) + + def onDebugMessage(self, msg, data): + if self.onMessage: + self.onMessage(json.loads(msg)) + + def onDebugEvent(self, type, state, evt): + if type == JSDebugEvent.Break: + if self.onBreak: self.onBreak(JSDebugEvent.BreakEvent(evt)) + elif type == JSDebugEvent.Exception: + if self.onException: self.onException(JSDebugEvent.ExceptionEvent(evt)) + elif type == JSDebugEvent.NewFunction: + if self.onNewFunction: self.onNewFunction(JSDebugEvent.NewFunctionEvent(evt)) + elif type == JSDebugEvent.BeforeCompile: + if self.onBeforeCompile: self.onBeforeCompile(JSDebugEvent.BeforeCompileEvent(evt)) + elif type == JSDebugEvent.AfterCompile: + if self.onAfterCompile: self.onAfterCompile(JSDebugEvent.AfterCompileEvent(evt)) + + def onDispatchDebugMessages(self): + return True + + def debugBreak(self): + _PyV8.debug().debugBreak() + + def debugBreakForCommand(self): + _PyV8.debug().debugBreakForCommand() + + def cancelDebugBreak(self): + _PyV8.debug().cancelDebugBreak() + + def processDebugMessages(self): + _PyV8.debug().processDebugMessages() + + def sendCommand(self, cmd, *args, **kwds): + request = json.dumps({ + 'seq': self.nextSeq(), + 'type': 'request', + 'command': cmd, + 'arguments': kwds + }) + + _PyV8.debug().sendCommand(request) + + return request + + def debugContinue(self, action='next', steps=1): + return self.sendCommand('continue', stepaction=action) + + def stepNext(self, steps=1): + """Step to the next statement in the current function.""" + return self.debugContinue(action='next', steps=steps) + + def stepIn(self, steps=1): + """Step into new functions invoked or the next statement in the current function.""" + return self.debugContinue(action='in', steps=steps) + + def stepOut(self, steps=1): + """Step out of the current function.""" + return self.debugContinue(action='out', steps=steps) + + def stepMin(self, steps=1): + """Perform a minimum step in the current function.""" + return self.debugContinue(action='out', steps=steps) + +class JSProfiler(_PyV8.JSProfiler): + @property + def logs(self): + pos = 0 + + while True: + size, buf = self.getLogLines(pos) + + if size == 0: + break + + for line in buf.split('\n'): + yield line + + pos += size + +profiler = JSProfiler() + +JSObjectSpace = _PyV8.JSObjectSpace +JSAllocationAction = _PyV8.JSAllocationAction + +class JSEngine(_PyV8.JSEngine): + def __init__(self): + _PyV8.JSEngine.__init__(self) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + del self + +JSScript = _PyV8.JSScript + +JSStackTrace = _PyV8.JSStackTrace +JSStackTrace.Options = _PyV8.JSStackTraceOptions +JSStackFrame = _PyV8.JSStackFrame + +class JSIsolate(_PyV8.JSIsolate): + def __enter__(self): + self.enter() + + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.leave() + + del self + +class JSContext(_PyV8.JSContext): + def __init__(self, obj=None, extensions=None, ctxt=None): + if JSLocker.active: + self.lock = JSLocker() + self.lock.enter() + + if ctxt: + _PyV8.JSContext.__init__(self, ctxt) + else: + _PyV8.JSContext.__init__(self, obj, extensions or []) + + def __enter__(self): + self.enter() + + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.leave() + + if hasattr(JSLocker, 'lock'): + self.lock.leave() + self.lock = None + + del self + +# contribute by marc boeker +def convert(obj): + if type(obj) == _PyV8.JSArray: + return [convert(v) for v in obj] + + if type(obj) == _PyV8.JSObject: + return dict([[str(k), convert(obj.__getattr__(str(k)))] for k in (obj.__dir__() if is_py3k else obj.__members__)]) + + return obj + +class AST: + Scope = _PyV8.AstScope + VarMode = _PyV8.AstVariableMode + Var = _PyV8.AstVariable + Label = _PyV8.AstLabel + NodeType = _PyV8.AstNodeType + Node = _PyV8.AstNode + Statement = _PyV8.AstStatement + Expression = _PyV8.AstExpression + Breakable = _PyV8.AstBreakableStatement + Block = _PyV8.AstBlock + Declaration = _PyV8.AstDeclaration + VariableDeclaration = _PyV8.AstVariableDeclaration + Module = _PyV8.AstModule + ModuleDeclaration = _PyV8.AstModuleDeclaration + ModuleLiteral = _PyV8.AstModuleLiteral + ModuleVariable = _PyV8.AstModuleVariable + ModulePath = _PyV8.AstModulePath + Iteration = _PyV8.AstIterationStatement + DoWhile = _PyV8.AstDoWhileStatement + While = _PyV8.AstWhileStatement + For = _PyV8.AstForStatement + ForIn = _PyV8.AstForInStatement + ExpressionStatement = _PyV8.AstExpressionStatement + Continue = _PyV8.AstContinueStatement + Break = _PyV8.AstBreakStatement + Return = _PyV8.AstReturnStatement + With = _PyV8.AstWithStatement + Case = _PyV8.AstCaseClause + Switch = _PyV8.AstSwitchStatement + Try = _PyV8.AstTryStatement + TryCatch = _PyV8.AstTryCatchStatement + TryFinally = _PyV8.AstTryFinallyStatement + Debugger = _PyV8.AstDebuggerStatement + Empty = _PyV8.AstEmptyStatement + Literal = _PyV8.AstLiteral + MaterializedLiteral = _PyV8.AstMaterializedLiteral + PropertyKind = _PyV8.AstPropertyKind + ObjectProperty = _PyV8.AstObjectProperty + Object = _PyV8.AstObjectLiteral + RegExp = _PyV8.AstRegExpLiteral + Array = _PyV8.AstArrayLiteral + VarProxy = _PyV8.AstVariableProxy + Property = _PyV8.AstProperty + Call = _PyV8.AstCall + CallNew = _PyV8.AstCallNew + CallRuntime = _PyV8.AstCallRuntime + Op = _PyV8.AstOperation + UnaryOp = _PyV8.AstUnaryOperation + BinOp = _PyV8.AstBinaryOperation + CountOp = _PyV8.AstCountOperation + CompOp = _PyV8.AstCompareOperation + Conditional = _PyV8.AstConditional + Assignment = _PyV8.AstAssignment + Throw = _PyV8.AstThrow + Function = _PyV8.AstFunctionLiteral + SharedFunction = _PyV8.AstSharedFunctionInfoLiteral + This = _PyV8.AstThisFunction + +from datetime import * +import unittest +import traceback + +if is_py3k: + def toNativeString(s): + return s + def toUnicodeString(s): + return s +else: + def toNativeString(s, encoding='utf-8'): + return s.encode(encoding) if isinstance(s, str) else s + + def toUnicodeString(s, encoding='utf-8'): + return s if isinstance(s, str) else str(s, encoding) + +class TestContext(unittest.TestCase): + def testMultiNamespace(self): + self.assertTrue(not bool(JSContext.inContext)) + self.assertTrue(not bool(JSContext.entered)) + + class Global(object): + name = "global" + + g = Global() + + with JSContext(g) as ctxt: + self.assertTrue(bool(JSContext.inContext)) + self.assertEqual(g.name, str(JSContext.entered.locals.name)) + self.assertEqual(g.name, str(JSContext.current.locals.name)) + + class Local(object): + name = "local" + + l = Local() + + with JSContext(l): + self.assertTrue(bool(JSContext.inContext)) + self.assertEqual(l.name, str(JSContext.entered.locals.name)) + self.assertEqual(l.name, str(JSContext.current.locals.name)) + + self.assertTrue(bool(JSContext.inContext)) + self.assertEqual(g.name, str(JSContext.entered.locals.name)) + self.assertEqual(g.name, str(JSContext.current.locals.name)) + + self.assertTrue(not bool(JSContext.entered)) + self.assertTrue(not bool(JSContext.inContext)) + + def _testMultiContext(self): + # Create an environment + with JSContext() as ctxt0: + ctxt0.securityToken = "password" + + global0 = ctxt0.locals + global0.custom = 1234 + + self.assertEqual(1234, int(global0.custom)) + + # Create an independent environment + with JSContext() as ctxt1: + ctxt1.securityToken = ctxt0.securityToken + + global1 = ctxt1.locals + global1.custom = 1234 + + with ctxt0: + self.assertEqual(1234, int(global0.custom)) + self.assertEqual(1234, int(global1.custom)) + + # Now create a new context with the old global + with JSContext(global1) as ctxt2: + ctxt2.securityToken = ctxt1.securityToken + + with ctxt1: + self.assertEqual(1234, int(global1.custom)) + + def _testSecurityChecks(self): + with JSContext() as env1: + env1.securityToken = "foo" + + # Create a function in env1. + env1.eval("spy=function(){return spy;}") + + spy = env1.locals.spy + + self.assertTrue(isinstance(spy, _PyV8.JSFunction)) + + # Create another function accessing global objects. + env1.eval("spy2=function(){return 123;}") + + spy2 = env1.locals.spy2 + + self.assertTrue(isinstance(spy2, _PyV8.JSFunction)) + + # Switch to env2 in the same domain and invoke spy on env2. + env2 = JSContext() + + env2.securityToken = "foo" + + with env2: + result = spy.apply(env2.locals) + + self.assertTrue(isinstance(result, _PyV8.JSFunction)) + + env2.securityToken = "bar" + + # Call cross_domain_call, it should throw an exception + with env2: + self.assertRaises(JSError, spy2.apply, env2.locals) + + def _testCrossDomainDelete(self): + with JSContext() as env1: + env2 = JSContext() + + # Set to the same domain. + env1.securityToken = "foo" + env2.securityToken = "foo" + + env1.locals.prop = 3 + + env2.locals.env1 = env1.locals + + # Change env2 to a different domain and delete env1.prop. + #env2.securityToken = "bar" + + self.assertEqual(3, int(env1.eval("prop"))) + + with env2: + self.assertEqual(3, int(env2.eval("this.env1.prop"))) + self.assertEqual("false", str(env2.eval("delete env1.prop"))) + + # Check that env1.prop still exists. + self.assertEqual(3, int(env1.locals.prop)) + +class TestWrapper(unittest.TestCase): + def testObject(self): + with JSContext() as ctxt: + o = ctxt.eval("new Object()") + + self.assertTrue(hash(o) > 0) + + o1 = o.clone() + + self.assertEqual(hash(o1), hash(o)) + self.assertTrue(o != o1) + + self.assertRaises(UnboundLocalError, o.clone) + + def testAutoConverter(self): + with JSContext() as ctxt: + ctxt.eval(""" + var_i = 1; + var_f = 1.0; + var_s = "test"; + var_b = true; + var_s_obj = new String("test"); + var_b_obj = new Boolean(true); + var_f_obj = new Number(1.5); + """) + + vars = ctxt.locals + + var_i = vars.var_i + + self.assertTrue(var_i) + self.assertEqual(1, int(var_i)) + + var_f = vars.var_f + + self.assertTrue(var_f) + self.assertEqual(1.0, float(vars.var_f)) + + var_s = vars.var_s + self.assertTrue(var_s) + self.assertEqual("test", str(vars.var_s)) + + var_b = vars.var_b + self.assertTrue(var_b) + self.assertTrue(bool(var_b)) + + self.assertEqual("test", vars.var_s_obj) + self.assertTrue(vars.var_b_obj) + self.assertEqual(1.5, vars.var_f_obj) + + attrs = dir(ctxt.locals) + + self.assertTrue(attrs) + self.assertTrue("var_i" in attrs) + self.assertTrue("var_f" in attrs) + self.assertTrue("var_s" in attrs) + self.assertTrue("var_b" in attrs) + self.assertTrue("var_s_obj" in attrs) + self.assertTrue("var_b_obj" in attrs) + self.assertTrue("var_f_obj" in attrs) + + def testExactConverter(self): + class MyInteger(int, JSClass): + pass + + class MyString(str, JSClass): + pass + + class MyUnicode(str, JSClass): + pass + + class MyDateTime(time, JSClass): + pass + + class Global(JSClass): + var_bool = True + var_int = 1 + var_float = 1.0 + var_str = 'str' + var_unicode = 'unicode' + var_datetime = datetime.now() + var_date = date.today() + var_time = time() + + var_myint = MyInteger() + var_mystr = MyString('mystr') + var_myunicode = MyUnicode('myunicode') + var_mytime = MyDateTime() + + with JSContext(Global()) as ctxt: + typename = ctxt.eval("(function (name) { return this[name].constructor.name; })") + typeof = ctxt.eval("(function (name) { return typeof(this[name]); })") + + self.assertEqual('Boolean', typename('var_bool')) + self.assertEqual('Number', typename('var_int')) + self.assertEqual('Number', typename('var_float')) + self.assertEqual('String', typename('var_str')) + self.assertEqual('String', typename('var_unicode')) + self.assertEqual('Date', typename('var_datetime')) + self.assertEqual('Date', typename('var_date')) + self.assertEqual('Date', typename('var_time')) + + self.assertEqual('MyInteger', typename('var_myint')) + self.assertEqual('MyString', typename('var_mystr')) + self.assertEqual('MyUnicode', typename('var_myunicode')) + self.assertEqual('MyDateTime', typename('var_mytime')) + + self.assertEqual('object', typeof('var_myint')) + self.assertEqual('object', typeof('var_mystr')) + self.assertEqual('object', typeof('var_myunicode')) + self.assertEqual('object', typeof('var_mytime')) + + def testJavascriptWrapper(self): + with JSContext() as ctxt: + self.assertEqual(type(None), type(ctxt.eval("null"))) + self.assertEqual(type(None), type(ctxt.eval("undefined"))) + self.assertEqual(bool, type(ctxt.eval("true"))) + self.assertEqual(str, type(ctxt.eval("'test'"))) + self.assertEqual(int, type(ctxt.eval("123"))) + self.assertEqual(float, type(ctxt.eval("3.14"))) + self.assertEqual(datetime, type(ctxt.eval("new Date()"))) + self.assertEqual(JSArray, type(ctxt.eval("[1, 2, 3]"))) + self.assertEqual(JSFunction, type(ctxt.eval("(function() {})"))) + self.assertEqual(JSObject, type(ctxt.eval("new Object()"))) + + def testPythonWrapper(self): + with JSContext() as ctxt: + typeof = ctxt.eval("(function type(value) { return typeof value; })") + protoof = ctxt.eval("(function protoof(value) { return Object.prototype.toString.apply(value); })") + + self.assertEqual('[object Null]', protoof(None)) + self.assertEqual('boolean', typeof(True)) + self.assertEqual('number', typeof(123)) + self.assertEqual('number', typeof(3.14)) + self.assertEqual('string', typeof('test')) + self.assertEqual('string', typeof('test')) + + self.assertEqual('[object Date]', protoof(datetime.now())) + self.assertEqual('[object Date]', protoof(date.today())) + self.assertEqual('[object Date]', protoof(time())) + + def test(): + pass + + self.assertEqual('[object Function]', protoof(abs)) + self.assertEqual('[object Function]', protoof(test)) + self.assertEqual('[object Function]', protoof(self.testPythonWrapper)) + self.assertEqual('[object Function]', protoof(int)) + + def testFunction(self): + with JSContext() as ctxt: + func = ctxt.eval(""" + (function () + { + function a() + { + return "abc"; + } + + return a(); + }) + """) + + self.assertEqual("abc", str(func())) + self.assertTrue(func != None) + self.assertFalse(func == None) + + func = ctxt.eval("(function test() {})") + + self.assertEqual("test", func.name) + self.assertEqual("", func.resname) + self.assertEqual(0, func.linenum) + self.assertEqual(14, func.colnum) + self.assertEqual(0, func.lineoff) + self.assertEqual(0, func.coloff) + + #TODO fix me, why the setter doesn't work? + # func.name = "hello" + # it seems __setattr__ was called instead of CJavascriptFunction::SetName + + func.setName("hello") + + self.assertEqual("hello", func.name) + + def testCall(self): + class Hello(object): + def __call__(self, name): + return "hello " + name + + class Global(JSClass): + hello = Hello() + + with JSContext(Global()) as ctxt: + self.assertEqual("hello flier", ctxt.eval("hello('flier')")) + + def testJSFunction(self): + with JSContext() as ctxt: + hello = ctxt.eval("(function (name) { return 'hello ' + name; })") + + self.assertTrue(isinstance(hello, _PyV8.JSFunction)) + self.assertEqual("hello flier", hello('flier')) + self.assertEqual("hello flier", hello.invoke(['flier'])) + + obj = ctxt.eval("({ 'name': 'flier', 'hello': function (name) { return 'hello ' + name + ' from ' + this.name; }})") + hello = obj.hello + self.assertTrue(isinstance(hello, JSFunction)) + self.assertEqual("hello flier from flier", hello('flier')) + + tester = ctxt.eval("({ 'name': 'tester' })") + self.assertEqual("hello flier from tester", hello.invoke(tester, ['flier'])) + self.assertEqual("hello flier from json", hello.apply({ 'name': 'json' }, ['flier'])) + + def testConstructor(self): + with JSContext() as ctx: + ctx.eval(""" + var Test = function() { + this.trySomething(); + }; + Test.prototype.trySomething = function() { + this.name = 'flier'; + }; + + var Test2 = function(first_name, last_name) { + this.name = first_name + ' ' + last_name; + }; + """) + + self.assertTrue(isinstance(ctx.locals.Test, _PyV8.JSFunction)) + + test = JSObject.create(ctx.locals.Test) + + self.assertTrue(isinstance(ctx.locals.Test, _PyV8.JSObject)) + self.assertEqual("flier", test.name); + + test2 = JSObject.create(ctx.locals.Test2, ('Flier', 'Lu')) + + self.assertEqual("Flier Lu", test2.name); + + test3 = JSObject.create(ctx.locals.Test2, ('Flier', 'Lu'), { 'email': 'flier.lu@gmail.com' }) + + self.assertEqual("flier.lu@gmail.com", test3.email); + + def testJSError(self): + with JSContext() as ctxt: + try: + ctxt.eval('throw "test"') + self.fail() + except: + self.assertTrue(JSError, sys.exc_info()[0]) + + def testErrorInfo(self): + with JSContext() as ctxt: + with JSEngine() as engine: + try: + engine.compile(""" + function hello() + { + throw Error("hello world"); + } + + hello();""", "test", 10, 10).run() + self.fail() + except JSError as e: + self.assertTrue(str(e).startswith('JSError: Error: hello world ( test @ 14 : 34 ) ->')) + self.assertEqual("Error", e.name) + self.assertEqual("hello world", e.message) + self.assertEqual("test", e.scriptName) + self.assertEqual(14, e.lineNum) + self.assertEqual(102, e.startPos) + self.assertEqual(103, e.endPos) + self.assertEqual(34, e.startCol) + self.assertEqual(35, e.endCol) + self.assertEqual('throw Error("hello world");', e.sourceLine.strip()) + self.assertEqual('Error: hello world\n' + + ' at Error ()\n' + + ' at hello (test:14:35)\n' + + ' at test:17:25', e.stackTrace) + + def testParseStack(self): + self.assertEqual([ + ('Error', 'unknown source', None, None), + ('test', 'native', None, None), + ('', 'test0', 3, 5), + ('f', 'test1', 2, 19), + ('g', 'test2', 1, 15), + (None, 'test3', 1, None), + (None, 'test3', 1, 1), + ], JSError.parse_stack("""Error: err + at Error (unknown source) + at test (native) + at new (test0:3:5) + at f (test1:2:19) + at g (test2:1:15) + at test3:1 + at test3:1:1""")) + + def testStackTrace(self): + class Global(JSClass): + def GetCurrentStackTrace(self, limit): + return JSStackTrace.GetCurrentStackTrace(4, JSStackTrace.Options.Detailed) + + with JSContext(Global()) as ctxt: + st = ctxt.eval(""" + function a() + { + return GetCurrentStackTrace(10); + } + function b() + { + return eval("a()"); + } + function c() + { + return new b(); + } + c();""", "test") + + self.assertEqual(4, len(st)) + self.assertEqual("\tat a (test:4:28)\n\tat (eval)\n\tat b (test:8:28)\n\tat c (test:12:28)\n", str(st)) + self.assertEqual("test.a (4:28)\n. (1:1) eval\ntest.b (8:28) constructor\ntest.c (12:28)", + "\n".join(["%s.%s (%d:%d)%s%s" % ( + f.scriptName, f.funcName, f.lineNum, f.column, + ' eval' if f.isEval else '', + ' constructor' if f.isConstructor else '') for f in st])) + + def testPythonException(self): + class Global(JSClass): + def raiseException(self): + raise RuntimeError("Hello") + + with JSContext(Global()) as ctxt: + r = ctxt.eval(""" + msg =""; + try + { + this.raiseException() + } + catch(e) + { + msg += "catch " + e + ";"; + } + finally + { + msg += "finally"; + }""") + self.assertEqual("catch Error: Hello;finally", str(ctxt.locals.msg)) + + def testExceptionMapping(self): + class TestException(Exception): + pass + + class Global(JSClass): + def raiseIndexError(self): + return [1, 2, 3][5] + + def raiseAttributeError(self): + None.hello() + + def raiseSyntaxError(self): + eval("???") + + def raiseTypeError(self): + int(sys) + + def raiseNotImplementedError(self): + raise NotImplementedError("Not support") + + def raiseExceptions(self): + raise TestException() + + with JSContext(Global()) as ctxt: + ctxt.eval("try { this.raiseIndexError(); } catch (e) { msg = e; }") + + self.assertEqual("RangeError: list index out of range", str(ctxt.locals.msg)) + + ctxt.eval("try { this.raiseAttributeError(); } catch (e) { msg = e; }") + + self.assertEqual("ReferenceError: 'NoneType' object has no attribute 'hello'", str(ctxt.locals.msg)) + + ctxt.eval("try { this.raiseSyntaxError(); } catch (e) { msg = e; }") + + self.assertEqual("SyntaxError: invalid syntax", str(ctxt.locals.msg)) + + ctxt.eval("try { this.raiseTypeError(); } catch (e) { msg = e; }") + + self.assertEqual("TypeError: int() argument must be a string or a number, not 'module'", str(ctxt.locals.msg)) + + ctxt.eval("try { this.raiseNotImplementedError(); } catch (e) { msg = e; }") + + self.assertEqual("Error: Not support", str(ctxt.locals.msg)) + + self.assertRaises(TestException, ctxt.eval, "this.raiseExceptions();") + + def testArray(self): + with JSContext() as ctxt: + array = ctxt.eval(""" + var array = new Array(); + + for (i=0; i<10; i++) + { + array[i] = 10-i; + } + + array; + """) + + self.assertTrue(isinstance(array, _PyV8.JSArray)) + self.assertEqual(10, len(array)) + + self.assertTrue(5 in array) + self.assertFalse(15 in array) + + self.assertEqual(10, len(array)) + + for i in range(10): + self.assertEqual(10-i, array[i]) + + array[5] = 0 + + self.assertEqual(0, array[5]) + + del array[5] + + self.assertEqual(None, array[5]) + + # array [10, 9, 8, 7, 6, None, 4, 3, 2, 1] + # array[4:7] 4^^^^^^^^^7 + # array[-3:-1] -3^^^^^^-1 + # array[0:0] [] + + self.assertEqual([6, None, 4], array[4:7]) + self.assertEqual([3, 2], array[-3:-1]) + self.assertEqual([], array[0:0]) + + array[1:3] = [9, 9, 9] + + self.assertEqual([10, 9, 9, 9, 7, 6, None, 4, 3, 2, 1], list(array)) + + array[5:8] = [8, 8] + + self.assertEqual([10, 9, 9, 9, 7, 8, 8, 3, 2, 1], list(array)) + + del array[1:4] + + self.assertEqual([10, 7, 8, 8, 3, 2, 1], list(array)) + + ctxt.locals.array1 = JSArray(5) + ctxt.locals.array2 = JSArray([1, 2, 3, 4, 5]) + + for i in range(len(ctxt.locals.array2)): + ctxt.locals.array1[i] = ctxt.locals.array2[i] * 10 + + ctxt.eval(""" + var sum = 0; + + for (i=0; i now1 else now1 - now2 + + self.assertTrue(delta < timedelta(seconds=1)) + + func = ctxt.eval("(function (d) { return d.toString(); })") + + now = datetime.now() + + self.assertTrue(str(func(now)).startswith(now.strftime("%a %b %d %Y %H:%M:%S"))) + + def testUnicode(self): + with JSContext() as ctxt: + self.assertEqual("人", toUnicodeString(ctxt.eval("\"人\""))) + self.assertEqual("é", toUnicodeString(ctxt.eval("\"é\""))) + + func = ctxt.eval("(function (msg) { return msg.length; })") + + self.assertEqual(2, func("测试")) + + def testClassicStyleObject(self): + class FileSystemWarpper: + @property + def cwd(self): + return os.getcwd() + + class Global: + @property + def fs(self): + return FileSystemWarpper() + + with JSContext(Global()) as ctxt: + self.assertEqual(os.getcwd(), ctxt.eval("fs.cwd")) + + def testRefCount(self): + count = sys.getrefcount(None) + + class Global(JSClass): + pass + + with JSContext(Global()) as ctxt: + ctxt.eval(""" + var none = null; + """) + + self.assertEqual(count+1, sys.getrefcount(None)) + + ctxt.eval(""" + var none = null; + """) + + self.assertEqual(count+1, sys.getrefcount(None)) + + def testProperty(self): + class Global(JSClass): + def __init__(self, name): + self._name = name + def getname(self): + return self._name + def setname(self, name): + self._name = name + def delname(self): + self._name = 'deleted' + + name = property(getname, setname, delname) + + g = Global('world') + + with JSContext(g) as ctxt: + self.assertEqual('world', ctxt.eval("name")) + self.assertEqual('flier', ctxt.eval("this.name = 'flier';")) + self.assertEqual('flier', ctxt.eval("name")) + self.assertTrue(ctxt.eval("delete name")) + ### + # FIXME replace the global object with Python object + # + #self.assertEqual('deleted', ctxt.eval("name")) + #ctxt.eval("__defineGetter__('name', function() { return 'fixed'; });") + #self.assertEqual('fixed', ctxt.eval("name")) + + def testGetterAndSetter(self): + class Global(JSClass): + def __init__(self, testval): + self.testval = testval + + with JSContext(Global("Test Value A")) as ctxt: + self.assertEqual("Test Value A", ctxt.locals.testval) + ctxt.eval(""" + this.__defineGetter__("test", function() { + return this.testval; + }); + this.__defineSetter__("test", function(val) { + this.testval = val; + }); + """) + self.assertEqual("Test Value A", ctxt.locals.test) + + ctxt.eval("test = 'Test Value B';") + + self.assertEqual("Test Value B", ctxt.locals.test) + + def testDestructor(self): + import gc + + owner = self + owner.deleted = False + + class Hello(object): + def say(self): + pass + + def __del__(self): + owner.deleted = True + + def test(): + with JSContext() as ctxt: + fn = ctxt.eval("(function (obj) { obj.say(); })") + + obj = Hello() + + self.assertEqual(2, sys.getrefcount(obj)) + + fn(obj) + + self.assertEqual(4, sys.getrefcount(obj)) + + del obj + + test() + + self.assertFalse(owner.deleted) + + JSEngine.collect() + gc.collect() + + self.assertTrue(owner.deleted) + + def testNullInString(self): + with JSContext() as ctxt: + fn = ctxt.eval("(function (s) { return s; })") + + self.assertEqual("hello \0 world", fn("hello \0 world")) + + def testLivingObjectCache(self): + class Global(JSClass): + i = 1 + b = True + o = object() + + with JSContext(Global()) as ctxt: + self.assertTrue(ctxt.eval("i == i")) + self.assertTrue(ctxt.eval("b == b")) + self.assertTrue(ctxt.eval("o == o")) + + def testNamedSetter(self): + class Obj(JSClass): + @property + def p(self): + return self._p + + @p.setter + def p(self, value): + self._p = value + + class Global(JSClass): + def __init__(self): + self.obj = Obj() + self.d = {} + self.p = None + + with JSContext(Global()) as ctxt: + ctxt.eval(""" + x = obj; + x.y = 10; + x.p = 10; + d.y = 10; + """) + self.assertEqual(10, ctxt.eval("obj.y")) + self.assertEqual(10, ctxt.eval("obj.p")) + self.assertEqual(10, ctxt.locals.d['y']) + + def testWatch(self): + class Obj(JSClass): + def __init__(self): + self.p = 1 + + class Global(JSClass): + def __init__(self): + self.o = Obj() + + with JSContext(Global()) as ctxt: + ctxt.eval(""" + o.watch("p", function (id, oldval, newval) { + return oldval + newval; + }); + """) + + self.assertEqual(1, ctxt.eval("o.p")) + + ctxt.eval("o.p = 2;") + + self.assertEqual(3, ctxt.eval("o.p")) + + ctxt.eval("delete o.p;") + + self.assertEqual(None, ctxt.eval("o.p")) + + ctxt.eval("o.p = 2;") + + self.assertEqual(2, ctxt.eval("o.p")) + + ctxt.eval("o.unwatch('p');") + + ctxt.eval("o.p = 1;") + + self.assertEqual(1, ctxt.eval("o.p")) + + def testReferenceError(self): + class Global(JSClass): + def __init__(self): + self.s = self + + with JSContext(Global()) as ctxt: + self.assertRaises(ReferenceError, ctxt.eval, 'x') + + self.assertTrue(ctxt.eval("typeof(x) === 'undefined'")) + + self.assertTrue(ctxt.eval("typeof(String) === 'function'")) + + self.assertTrue(ctxt.eval("typeof(s.String) === 'undefined'")) + + self.assertTrue(ctxt.eval("typeof(s.z) === 'undefined'")) + + def testRaiseExceptionInGetter(self): + class Document(JSClass): + def __getattr__(self, name): + if name == 'y': + raise TypeError() + + return JSClass.__getattr__(self, name) + + class Global(JSClass): + def __init__(self): + self.document = Document() + + with JSContext(Global()) as ctxt: + self.assertEqual(None, ctxt.eval('document.x')) + self.assertRaises(TypeError, ctxt.eval, 'document.y') + +class TestMultithread(unittest.TestCase): + def testLocker(self): + self.assertFalse(JSLocker.active) + self.assertFalse(JSLocker.locked) + + with JSLocker() as outter_locker: + self.assertTrue(JSLocker.active) + self.assertTrue(JSLocker.locked) + + self.assertTrue(outter_locker) + + with JSLocker() as inner_locker: + self.assertTrue(JSLocker.locked) + + self.assertTrue(outter_locker) + self.assertTrue(inner_locker) + + with JSUnlocker() as unlocker: + self.assertFalse(JSLocker.locked) + + self.assertTrue(outter_locker) + self.assertTrue(inner_locker) + + self.assertTrue(JSLocker.locked) + + self.assertTrue(JSLocker.active) + self.assertFalse(JSLocker.locked) + + locker = JSLocker() + + with JSContext(): + self.assertRaises(RuntimeError, locker.__enter__) + self.assertRaises(RuntimeError, locker.__exit__, None, None, None) + + del locker + + def testMultiPythonThread(self): + import time, threading + + class Global: + count = 0 + started = threading.Event() + finished = threading.Semaphore(0) + + def sleep(self, ms): + time.sleep(ms / 1000.0) + + self.count += 1 + + g = Global() + + def run(): + with JSContext(g) as ctxt: + ctxt.eval(""" + started.wait(); + + for (i=0; i<10; i++) + { + sleep(100); + } + + finished.release(); + """) + + threading.Thread(target=run).start() + + now = time.time() + + self.assertEqual(0, g.count) + + g.started.set() + g.finished.acquire() + + self.assertEqual(10, g.count) + + self.assertTrue((time.time() - now) >= 1) + + def testMultiJavascriptThread(self): + import time, threading + + class Global: + result = [] + + def add(self, value): + with JSUnlocker(): + time.sleep(0.1) + + self.result.append(value) + + g = Global() + + def run(): + with JSContext(g) as ctxt: + ctxt.eval(""" + for (i=0; i<10; i++) + add(i); + """) + + threads = [threading.Thread(target=run), threading.Thread(target=run)] + + with JSLocker(): + for t in threads: t.start() + + for t in threads: t.join() + + self.assertEqual(20, len(g.result)) + + def _testPreemptionJavascriptThreads(self): + import time, threading + + class Global: + result = [] + + def add(self, value): + # we use preemption scheduler to switch between threads + # so, just comment the JSUnlocker + # + # with JSUnlocker() as unlocker: + time.sleep(0.1) + + self.result.append(value) + + g = Global() + + def run(): + with JSContext(g) as ctxt: + ctxt.eval(""" + for (i=0; i<10; i++) + add(i); + """) + + threads = [threading.Thread(target=run), threading.Thread(target=run)] + + with JSLocker() as locker: + JSLocker.startPreemption(100) + + for t in threads: t.start() + + for t in threads: t.join() + + self.assertEqual(20, len(g.result)) + +class TestEngine(unittest.TestCase): + def testClassProperties(self): + with JSContext() as ctxt: + self.assertTrue(str(JSEngine.version).startswith("3.")) + self.assertFalse(JSEngine.dead) + + def testCompile(self): + with JSContext() as ctxt: + with JSEngine() as engine: + s = engine.compile("1+2") + + self.assertTrue(isinstance(s, _PyV8.JSScript)) + + self.assertEqual("1+2", s.source) + self.assertEqual(3, int(s.run())) + + self.assertRaises(SyntaxError, engine.compile, "1+") + + def testPrecompile(self): + with JSContext() as ctxt: + with JSEngine() as engine: + data = engine.precompile("1+2") + + self.assertTrue(data) + self.assertEqual(28, len(data)) + + s = engine.compile("1+2", precompiled=data) + + self.assertTrue(isinstance(s, _PyV8.JSScript)) + + self.assertEqual("1+2", s.source) + self.assertEqual(3, int(s.run())) + + self.assertRaises(SyntaxError, engine.precompile, "1+") + + def testUnicodeSource(self): + class Global(JSClass): + var = '测试' + + def __getattr__(self, name): + if (name if is_py3k else name.decode('utf-8')) == '变量': + return self.var + + return JSClass.__getattr__(self, name) + + g = Global() + + with JSContext(g) as ctxt: + with JSEngine() as engine: + src = """ + function 函数() { return 变量.length; } + + 函数(); + + var func = function () {}; + """ + + data = engine.precompile(src) + + self.assertTrue(data) + self.assertEqual(68, len(data)) + + s = engine.compile(src, precompiled=data) + + self.assertTrue(isinstance(s, _PyV8.JSScript)) + + self.assertEqual(toNativeString(src), s.source) + self.assertEqual(2, s.run()) + + func_name = toNativeString('函数') + + self.assertTrue(hasattr(ctxt.locals, func_name)) + + func = getattr(ctxt.locals, func_name) + + self.assertTrue(isinstance(func, _PyV8.JSFunction)) + + self.assertEqual(func_name, func.name) + self.assertEqual("", func.resname) + self.assertEqual(1, func.linenum) + self.assertEqual(0, func.lineoff) + self.assertEqual(0, func.coloff) + + var_name = toNativeString('变量') + + setattr(ctxt.locals, var_name, '测试长字符串') + + self.assertEqual(6, func()) + + self.assertEqual("func", ctxt.locals.func.inferredname) + + def testExtension(self): + extSrc = """function hello(name) { return "hello " + name + " from javascript"; }""" + extJs = JSExtension("hello/javascript", extSrc) + + self.assertTrue(extJs) + self.assertEqual("hello/javascript", extJs.name) + self.assertEqual(extSrc, extJs.source) + self.assertFalse(extJs.autoEnable) + self.assertTrue(extJs.registered) + + TestEngine.extJs = extJs + + with JSContext(extensions=['hello/javascript']) as ctxt: + self.assertEqual("hello flier from javascript", ctxt.eval("hello('flier')")) + + # test the auto enable property + + with JSContext() as ctxt: + self.assertRaises(ReferenceError, ctxt.eval, "hello('flier')") + + extJs.autoEnable = True + self.assertTrue(extJs.autoEnable) + + with JSContext() as ctxt: + self.assertEqual("hello flier from javascript", ctxt.eval("hello('flier')")) + + extJs.autoEnable = False + self.assertFalse(extJs.autoEnable) + + with JSContext() as ctxt: + self.assertRaises(ReferenceError, ctxt.eval, "hello('flier')") + + extUnicodeSrc = """function helloW(name) { return "hello " + name + " from javascript"; }""" + extUnicodeJs = JSExtension("helloW/javascript", extUnicodeSrc) + + self.assertTrue(extUnicodeJs) + self.assertEqual("helloW/javascript", extUnicodeJs.name) + self.assertEqual(toNativeString(extUnicodeSrc), extUnicodeJs.source) + self.assertFalse(extUnicodeJs.autoEnable) + self.assertTrue(extUnicodeJs.registered) + + TestEngine.extUnicodeJs = extUnicodeJs + + with JSContext(extensions=['helloW/javascript']) as ctxt: + self.assertEqual("hello flier from javascript", ctxt.eval("helloW('flier')")) + + ret = ctxt.eval("helloW('世界')") + + self.assertEqual("hello 世界 from javascript", ret if is_py3k else ret.decode('UTF-8')) + + def testNativeExtension(self): + extSrc = "native function hello();" + extPy = JSExtension("hello/python", extSrc, lambda func: lambda name: "hello " + name + " from python", register=False) + self.assertTrue(extPy) + self.assertEqual("hello/python", extPy.name) + self.assertEqual(extSrc, extPy.source) + self.assertFalse(extPy.autoEnable) + self.assertFalse(extPy.registered) + extPy.register() + self.assertTrue(extPy.registered) + + TestEngine.extPy = extPy + + with JSContext(extensions=['hello/python']) as ctxt: + self.assertEqual("hello flier from python", ctxt.eval("hello('flier')")) + + def _testSerialize(self): + data = None + + self.assertFalse(JSContext.entered) + + with JSContext() as ctxt: + self.assertTrue(JSContext.entered) + + #ctxt.eval("function hello(name) { return 'hello ' + name; }") + + data = JSEngine.serialize() + + self.assertTrue(data) + self.assertTrue(len(data) > 0) + + self.assertFalse(JSContext.entered) + + #JSEngine.deserialize() + + self.assertTrue(JSContext.entered) + + self.assertEqual('hello flier', JSContext.current.eval("hello('flier');")) + + def testEval(self): + with JSContext() as ctxt: + self.assertEqual(3, int(ctxt.eval("1+2"))) + + def testGlobal(self): + class Global(JSClass): + version = "1.0" + + with JSContext(Global()) as ctxt: + vars = ctxt.locals + + # getter + self.assertEqual(Global.version, str(vars.version)) + self.assertEqual(Global.version, str(ctxt.eval("version"))) + + self.assertRaises(ReferenceError, ctxt.eval, "nonexists") + + # setter + self.assertEqual(2.0, float(ctxt.eval("version = 2.0"))) + + self.assertEqual(2.0, float(vars.version)) + + def testThis(self): + class Global(JSClass): + version = 1.0 + + with JSContext(Global()) as ctxt: + self.assertEqual("[object Global]", str(ctxt.eval("this"))) + + self.assertEqual(1.0, float(ctxt.eval("this.version"))) + + def testObjectBuildInMethods(self): + class Global(JSClass): + version = 1.0 + + with JSContext(Global()) as ctxt: + self.assertEqual("[object Global]", str(ctxt.eval("this.toString()"))) + self.assertEqual("[object Global]", str(ctxt.eval("this.toLocaleString()"))) + self.assertEqual(Global.version, float(ctxt.eval("this.valueOf()").version)) + + self.assertTrue(bool(ctxt.eval("this.hasOwnProperty(\"version\")"))) + + self.assertFalse(ctxt.eval("this.hasOwnProperty(\"nonexistent\")")) + + def testPythonWrapper(self): + class Global(JSClass): + s = [1, 2, 3] + d = {'a': {'b': 'c'}, 'd': ['e', 'f']} + + g = Global() + + with JSContext(g) as ctxt: + ctxt.eval(""" + s[2] = s[1] + 2; + s[0] = s[1]; + delete s[1]; + """) + self.assertEqual([2, 4], g.s) + self.assertEqual('c', ctxt.eval("d.a.b")) + self.assertEqual(['e', 'f'], ctxt.eval("d.d")) + ctxt.eval(""" + d.a.q = 4 + delete d.d + """) + self.assertEqual(4, g.d['a']['q']) + self.assertEqual(None, ctxt.eval("d.d")) + + def _testMemoryAllocationCallback(self): + alloc = {} + + def callback(space, action, size): + alloc[(space, action)] = alloc.setdefault((space, action), 0) + size + + JSEngine.setMemoryAllocationCallback(callback) + + with JSContext() as ctxt: + self.assertFalse((JSObjectSpace.Code, JSAllocationAction.alloc) in alloc) + + ctxt.eval("var o = new Array(1000);") + + self.assertTrue((JSObjectSpace.Code, JSAllocationAction.alloc) in alloc) + + JSEngine.setMemoryAllocationCallback(None) + +class TestDebug(unittest.TestCase): + def setUp(self): + self.engine = JSEngine() + + def tearDown(self): + del self.engine + + events = [] + + def processDebugEvent(self, event): + try: + logging.debug("receive debug event: %s", repr(event)) + + self.events.append(repr(event)) + except: + logging.error("fail to process debug event") + logging.debug(traceback.extract_stack()) + + def testEventDispatch(self): + debugger = JSDebugger() + + self.assertTrue(not debugger.enabled) + + debugger.onBreak = lambda evt: self.processDebugEvent(evt) + debugger.onException = lambda evt: self.processDebugEvent(evt) + debugger.onNewFunction = lambda evt: self.processDebugEvent(evt) + debugger.onBeforeCompile = lambda evt: self.processDebugEvent(evt) + debugger.onAfterCompile = lambda evt: self.processDebugEvent(evt) + + with JSContext() as ctxt: + debugger.enabled = True + + self.assertEqual(3, int(ctxt.eval("function test() { text = \"1+2\"; return eval(text) } test()"))) + + debugger.enabled = False + + self.assertRaises(JSError, JSContext.eval, ctxt, "throw 1") + + self.assertTrue(not debugger.enabled) + + self.assertEqual(4, len(self.events)) + +class TestProfile(unittest.TestCase): + def _testStart(self): + self.assertFalse(profiler.started) + + profiler.start() + + self.assertTrue(profiler.started) + + profiler.stop() + + self.assertFalse(profiler.started) + + def _testResume(self): + self.assertTrue(profiler.paused) + + self.assertEqual(profiler.Modules.cpu, profiler.modules) + + profiler.resume() + + profiler.resume(profiler.Modules.heap) + + # TODO enable profiler with resume + #self.assertFalse(profiler.paused) + + +class TestAST(unittest.TestCase): + + class Checker(object): + def __init__(self, testcase): + self.testcase = testcase + self.called = [] + + def __enter__(self): + self.ctxt = JSContext() + self.ctxt.enter() + + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.ctxt.leave() + + def __getattr__(self, name): + return getattr(self.testcase, name) + + def test(self, script): + JSEngine().compile(script).visit(self) + + return self.called + + def onProgram(self, prog): + self.ast = prog.toAST() + self.json = json.loads(prog.toJSON()) + + for decl in prog.scope.declarations: + decl.visit(self) + + for stmt in prog.body: + stmt.visit(self) + + def onBlock(self, block): + for stmt in block.statements: + stmt.visit(self) + + def onExpressionStatement(self, stmt): + stmt.expression.visit(self) + + #print type(stmt.expression), stmt.expression + + def testBlock(self): + class BlockChecker(TestAST.Checker): + def onBlock(self, stmt): + self.called.append('block') + + self.assertEqual(AST.NodeType.Block, stmt.type) + + self.assertTrue(stmt.initializerBlock) + self.assertFalse(stmt.anonymous) + + target = stmt.breakTarget + self.assertTrue(target) + self.assertFalse(target.bound) + self.assertTrue(target.unused) + self.assertFalse(target.linked) + + self.assertEqual(2, len(stmt.statements)) + + self.assertEqual(['%InitializeVarGlobal("i", 0);', '%InitializeVarGlobal("j", 0);'], [str(s) for s in stmt.statements]) + + with BlockChecker(self) as checker: + self.assertEqual(['block'], checker.test("var i, j;")) + self.assertEqual("""FUNC +. NAME "" +. INFERRED NAME "" +. DECLS +. . VAR "i" +. . VAR "j" +. BLOCK INIT +. . CALL RUNTIME InitializeVarGlobal +. . . LITERAL "i" +. . . LITERAL 0 +. . CALL RUNTIME InitializeVarGlobal +. . . LITERAL "j" +. . . LITERAL 0 +""", checker.ast) + self.assertEqual(['FunctionLiteral', {'name': ''}, + ['Declaration', {'mode': 'VAR'}, + ['Variable', {'name': 'i'}] + ], ['Declaration', {'mode':'VAR'}, + ['Variable', {'name': 'j'}] + ], ['Block', + ['ExpressionStatement', ['CallRuntime', {'name': 'InitializeVarGlobal'}, + ['Literal', {'handle':'i'}], + ['Literal', {'handle': 0}]]], + ['ExpressionStatement', ['CallRuntime', {'name': 'InitializeVarGlobal'}, + ['Literal', {'handle': 'j'}], + ['Literal', {'handle': 0}]]] + ] + ], checker.json) + + def testIfStatement(self): + class IfStatementChecker(TestAST.Checker): + def onIfStatement(self, stmt): + self.called.append('if') + + self.assertTrue(stmt) + self.assertEqual(AST.NodeType.IfStatement, stmt.type) + + self.assertEqual(7, stmt.pos) + stmt.pos = 100 + self.assertEqual(100, stmt.pos) + + self.assertTrue(stmt.hasThenStatement) + self.assertTrue(stmt.hasElseStatement) + + self.assertEqual("((value % 2) == 0)", str(stmt.condition)) + self.assertEqual("{ s = \"even\"; }", str(stmt.thenStatement)) + self.assertEqual("{ s = \"odd\"; }", str(stmt.elseStatement)) + + self.assertFalse(stmt.condition.isPropertyName) + + with IfStatementChecker(self) as checker: + self.assertEqual(['if'], checker.test("var s; if (value % 2 == 0) { s = 'even'; } else { s = 'odd'; }")) + + def testForStatement(self): + class ForStatementChecker(TestAST.Checker): + def onForStatement(self, stmt): + self.called.append('for') + + self.assertEqual("{ j += i; }", str(stmt.body)) + + self.assertEqual("i = 0;", str(stmt.init)) + self.assertEqual("(i < 10)", str(stmt.condition)) + self.assertEqual("(i++);", str(stmt.nextStmt)) + + target = stmt.continueTarget + + self.assertTrue(target) + self.assertFalse(target.bound) + self.assertTrue(target.unused) + self.assertFalse(target.linked) + self.assertFalse(stmt.fastLoop) + + def onForInStatement(self, stmt): + self.called.append('forIn') + + self.assertEqual("{ out += name; }", str(stmt.body)) + + self.assertEqual("name", str(stmt.each)) + self.assertEqual("names", str(stmt.enumerable)) + + def onWhileStatement(self, stmt): + self.called.append('while') + + self.assertEqual("{ i += 1; }", str(stmt.body)) + + self.assertEqual("(i < 10)", str(stmt.condition)) + + def onDoWhileStatement(self, stmt): + self.called.append('doWhile') + + self.assertEqual("{ i += 1; }", str(stmt.body)) + + self.assertEqual("(i < 10)", str(stmt.condition)) + self.assertEqual(281, stmt.conditionPos) + + with ForStatementChecker(self) as checker: + self.assertEqual(['for', 'forIn', 'while', 'doWhile'], checker.test(""" + var i, j; + + for (i=0; i<10; i++) { j+=i; } + + var names = new Array(); + var out = ''; + + for (name in names) { out += name; } + + while (i<10) { i += 1; } + + do { i += 1; } while (i<10); + """)) + + def testCallStatements(self): + class CallStatementChecker(TestAST.Checker): + def onVariableDeclaration(self, decl): + self.called.append('var') + + var = decl.proxy + + if var.name == 's': + self.assertEqual(AST.VarMode.var, decl.mode) + + self.assertTrue(var.isValidLeftHandSide) + self.assertFalse(var.isArguments) + self.assertFalse(var.isThis) + + def onFunctionDeclaration(self, decl): + self.called.append('func') + + var = decl.proxy + + if var.name == 'hello': + self.assertEqual(AST.VarMode.var, decl.mode) + self.assertTrue(decl.function) + self.assertEqual('(function hello(name) { s = ("Hello " + name); })', str(decl.function)) + elif var.name == 'dog': + self.assertEqual(AST.VarMode.var, decl.mode) + self.assertTrue(decl.function) + self.assertEqual('(function dog(name) { (this).name = name; })', str(decl.function)) + + def onCall(self, expr): + self.called.append('call') + + self.assertEqual("hello", str(expr.expression)) + self.assertEqual(['"flier"'], [str(arg) for arg in expr.args]) + self.assertEqual(159, expr.pos) + + def onCallNew(self, expr): + self.called.append('callNew') + + self.assertEqual("dog", str(expr.expression)) + self.assertEqual(['"cat"'], [str(arg) for arg in expr.args]) + self.assertEqual(191, expr.pos) + + def onCallRuntime(self, expr): + self.called.append('callRuntime') + + self.assertEqual("InitializeVarGlobal", expr.name) + self.assertEqual(['"s"', '0'], [str(arg) for arg in expr.args]) + self.assertFalse(expr.isJsRuntime) + + with CallStatementChecker(self) as checker: + self.assertEqual(['var', 'func', 'func', 'callRuntime', 'call', 'callNew'], checker.test(""" + var s; + function hello(name) { s = "Hello " + name; } + function dog(name) { this.name = name; } + hello("flier"); + new dog("cat"); + """)) + + def testTryStatements(self): + class TryStatementsChecker(TestAST.Checker): + def onThrow(self, expr): + self.called.append('try') + + self.assertEqual('"abc"', str(expr.exception)) + self.assertEqual(66, expr.pos) + + def onTryCatchStatement(self, stmt): + self.called.append('catch') + + self.assertEqual("{ throw \"abc\"; }", str(stmt.tryBlock)) + #FIXME self.assertEqual([], stmt.targets) + + stmt.tryBlock.visit(self) + + self.assertEqual("err", str(stmt.variable.name)) + self.assertEqual("{ s = err; }", str(stmt.catchBlock)) + + def onTryFinallyStatement(self, stmt): + self.called.append('finally') + + self.assertEqual("{ throw \"abc\"; }", str(stmt.tryBlock)) + #FIXME self.assertEqual([], stmt.targets) + + self.assertEqual("{ s += \".\"; }", str(stmt.finallyBlock)) + + with TryStatementsChecker(self) as checker: + self.assertEqual(['catch', 'try', 'finally'], checker.test(""" + var s; + try { + throw "abc"; + } + catch (err) { + s = err; + }; + + try { + throw "abc"; + } + finally { + s += "."; + } + """)) + + def testLiterals(self): + class LiteralChecker(TestAST.Checker): + def onCallRuntime(self, expr): + expr.args[1].visit(self) + + def onLiteral(self, litr): + self.called.append('literal') + + self.assertFalse(litr.isPropertyName) + self.assertFalse(litr.isNull) + self.assertFalse(litr.isTrue) + + def onRegExpLiteral(self, litr): + self.called.append('regex') + + self.assertEqual("test", litr.pattern) + self.assertEqual("g", litr.flags) + + def onObjectLiteral(self, litr): + self.called.append('object') + + self.assertEqual('constant:"name"="flier",constant:"sex"=true', + ",".join(["%s:%s=%s" % (prop.kind, prop.key, prop.value) for prop in litr.properties])) + + def onArrayLiteral(self, litr): + self.called.append('array') + + self.assertEqual('"hello","world",42', + ",".join([str(value) for value in litr.values])) + with LiteralChecker(self) as checker: + self.assertEqual(['literal', 'regex', 'literal', 'literal'], checker.test(""" + false; + /test/g; + var o = { name: 'flier', sex: true }; + var a = ['hello', 'world', 42]; + """)) + + def testOperations(self): + class OperationChecker(TestAST.Checker): + def onUnaryOperation(self, expr): + self.called.append('unaryOp') + + self.assertEqual(AST.Op.BIT_NOT, expr.op) + self.assertEqual("i", expr.expression.name) + + #print "unary", expr + + def onIncrementOperation(self, expr): + self.fail() + + def onBinaryOperation(self, expr): + self.called.append('binOp') + + self.assertEqual(AST.Op.ADD, expr.op) + self.assertEqual("i", str(expr.left)) + self.assertEqual("j", str(expr.right)) + self.assertEqual(36, expr.pos) + + #print "bin", expr + + def onAssignment(self, expr): + self.called.append('assign') + + self.assertEqual(AST.Op.ASSIGN_ADD, expr.op) + self.assertEqual(AST.Op.ADD, expr.binop) + + self.assertEqual("i", str(expr.target)) + self.assertEqual("1", str(expr.value)) + self.assertEqual(53, expr.pos) + + self.assertEqual("(i + 1)", str(expr.binOperation)) + + self.assertTrue(expr.compound) + + def onCountOperation(self, expr): + self.called.append('countOp') + + self.assertFalse(expr.prefix) + self.assertTrue(expr.postfix) + + self.assertEqual(AST.Op.INC, expr.op) + self.assertEqual(AST.Op.ADD, expr.binop) + self.assertEqual(71, expr.pos) + self.assertEqual("i", expr.expression.name) + + #print "count", expr + + def onCompareOperation(self, expr): + self.called.append('compOp') + + if len(self.called) == 4: + self.assertEqual(AST.Op.EQ, expr.op) + self.assertEqual(88, expr.pos) # i==j + else: + self.assertEqual(AST.Op.EQ_STRICT, expr.op) + self.assertEqual(106, expr.pos) # i===j + + self.assertEqual("i", str(expr.left)) + self.assertEqual("j", str(expr.right)) + + #print "comp", expr + + def onConditional(self, expr): + self.called.append('conditional') + + self.assertEqual("(i > j)", str(expr.condition)) + self.assertEqual("i", str(expr.thenExpr)) + self.assertEqual("j", str(expr.elseExpr)) + + self.assertEqual(144, expr.thenExprPos) + self.assertEqual(146, expr.elseExprPos) + + with OperationChecker(self) as checker: + self.assertEqual(['binOp', 'assign', 'countOp', 'compOp', 'compOp', 'unaryOp', 'conditional'], checker.test(""" + var i, j; + i+j; + i+=1; + i++; + i==j; + i===j; + ~i; + i>j?i:j; + """)) + + def testSwitchStatement(self): + class SwitchStatementChecker(TestAST.Checker): + def onSwitchStatement(self, stmt): + self.called.append('switch') + + self.assertEqual('expr', stmt.tag.name) + self.assertEqual(2, len(stmt.cases)) + + case = stmt.cases[0] + + self.assertFalse(case.isDefault) + self.assertTrue(case.label.isString) + self.assertEqual(0, case.bodyTarget.pos) + self.assertEqual(57, case.position) + self.assertEqual(1, len(case.statements)) + + case = stmt.cases[1] + + self.assertTrue(case.isDefault) + self.assertEqual(None, case.label) + self.assertEqual(0, case.bodyTarget.pos) + self.assertEqual(109, case.position) + self.assertEqual(1, len(case.statements)) + + with SwitchStatementChecker(self) as checker: + self.assertEqual(['switch'], checker.test(""" + switch (expr) { + case 'flier': + break; + default: + break; + } + """)) + +if __name__ == '__main__': + if "-v" in sys.argv: + level = logging.DEBUG + else: + level = logging.WARN + + if "-p" in sys.argv: + sys.argv.remove("-p") + print("Press any key to continue or attach process #%d..." % os.getpid()) + input() + + logging.basicConfig(level=level, format='%(asctime)s %(levelname)s %(message)s') + + logging.info("testing PyV8 module %s with V8 v%s", __version__, JSEngine.version) + + unittest.main() diff --git a/Installed Packages/PyV8/linux64-p3/_PyV8.cpython-33m.so b/Installed Packages/PyV8/linux64-p3/_PyV8.cpython-33m.so new file mode 100644 index 0000000..6ebfe80 Binary files /dev/null and b/Installed Packages/PyV8/linux64-p3/_PyV8.cpython-33m.so differ diff --git a/Installed Packages/PyV8/linux64-p3/config.json b/Installed Packages/PyV8/linux64-p3/config.json new file mode 100644 index 0000000..6d47423 --- /dev/null +++ b/Installed Packages/PyV8/linux64-p3/config.json @@ -0,0 +1 @@ +{"last_update": 1395934956.940371, "last_id": "a22304a4e9bc98da62467b3d3fad8eaac83b5c39", "skip_update": false} \ No newline at end of file diff --git a/Installed Packages/SublimeLinter-contrib-puppet-lint.sublime-package b/Installed Packages/SublimeLinter-contrib-puppet-lint.sublime-package new file mode 100644 index 0000000..ca0f87b Binary files /dev/null and b/Installed Packages/SublimeLinter-contrib-puppet-lint.sublime-package differ diff --git a/Installed Packages/SublimeLinter-jshint.sublime-package b/Installed Packages/SublimeLinter-jshint.sublime-package new file mode 100644 index 0000000..76f978b Binary files /dev/null and b/Installed Packages/SublimeLinter-jshint.sublime-package differ diff --git a/Installed Packages/SublimeLinter-json.sublime-package b/Installed Packages/SublimeLinter-json.sublime-package new file mode 100644 index 0000000..1561ca0 Binary files /dev/null and b/Installed Packages/SublimeLinter-json.sublime-package differ diff --git a/Installed Packages/TernJS.sublime-package b/Installed Packages/TernJS.sublime-package new file mode 100644 index 0000000..3ad98df Binary files /dev/null and b/Installed Packages/TernJS.sublime-package differ diff --git a/Installed Packages/sublimelint.sublime-package b/Installed Packages/sublimelint.sublime-package new file mode 100644 index 0000000..72c7b82 Binary files /dev/null and b/Installed Packages/sublimelint.sublime-package differ diff --git a/Packages/HTML/HTML.tmLanguage b/Packages/HTML/HTML.tmLanguage new file mode 100644 index 0000000..76d0acc --- /dev/null +++ b/Packages/HTML/HTML.tmLanguage @@ -0,0 +1,1002 @@ + + + + + fileTypes + + html + htm + shtml + xhtml + phtml + inc + tmpl + tpl + ctp + + firstLineMatch + <!(?i:DOCTYPE)|<(?i:html)|<\?(?i:php) + foldingStartMarker + (?x) + (<(?i:head|body|table|thead|tbody|tfoot|tr|div|select|fieldset|style|script|ul|ol|li|form|dl)\b.*?> + |<!--(?!.*--\s*>) + |^<!--\ \#tminclude\ (?>.*?-->)$ + |<\?(?:php)?.*\b(if|for(each)?|while)\b.+: + |\{\{?(if|foreach|capture|literal|foreach|php|section|strip) + |\{\s*($|\?>\s*$|//|/\*(.*\*/\s*$|(?!.*?\*/))) + ) + foldingStopMarker + (?x) + (</(?i:head|body|table|thead|tbody|tfoot|tr|div|select|fieldset|style|script|ul|ol|li|form|dl)> + |^(?!.*?<!--).*?--\s*> + |^<!--\ end\ tminclude\ -->$ + |<\?(?:php)?.*\bend(if|for(each)?|while)\b + |\{\{?/(if|foreach|capture|literal|foreach|php|section|strip) + |^[^{]*\} + ) + keyEquivalent + ^~H + name + HTML + patterns + + + begin + (<)([a-zA-Z0-9:]++)(?=[^>]*></\2>) + beginCaptures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.html + + + end + (>)(<)(/)(\2)(>) + endCaptures + + 1 + + name + punctuation.definition.tag.end.html + + 2 + + name + punctuation.definition.tag.begin.html meta.scope.between-tag-pair.html + + 3 + + name + punctuation.definition.tag.begin.html + + 4 + + name + entity.name.tag.html + + 5 + + name + punctuation.definition.tag.end.html + + + name + meta.tag.any.html + patterns + + + include + #tag-stuff + + + + + begin + (<\?)(xml) + captures + + 1 + + name + punctuation.definition.tag.html + + 2 + + name + entity.name.tag.xml.html + + + end + (\?>) + name + meta.tag.preprocessor.xml.html + patterns + + + include + #tag-generic-attribute + + + include + #string-double-quoted + + + include + #string-single-quoted + + + + + begin + <!-- + captures + + 0 + + name + punctuation.definition.comment.html + + + end + --\s*> + name + comment.block.html + patterns + + + match + -- + name + invalid.illegal.bad-comments-or-CDATA.html + + + include + #embedded-code + + + + + begin + <! + captures + + 0 + + name + punctuation.definition.tag.html + + + end + > + name + meta.tag.sgml.html + patterns + + + begin + (?i:DOCTYPE) + captures + + 1 + + name + entity.name.tag.doctype.html + + + end + (?=>) + name + meta.tag.sgml.doctype.html + patterns + + + match + "[^">]*" + name + string.quoted.double.doctype.identifiers-and-DTDs.html + + + + + begin + \[CDATA\[ + end + ]](?=>) + name + constant.other.inline-data.html + + + match + (\s*)(?!--|>)\S(\s*) + name + invalid.illegal.bad-comments-or-CDATA.html + + + + + include + #embedded-code + + + begin + (?:^\s+)?(<)((?i:style))\b(?![^>]*/>) + beginCaptures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.style.html + + + end + (?<=</(?:[sS][tT][yY][lL][eE]))(>)(?:\s*\n)? + endCaptures + + 2 + + name + punctuation.definition.tag.html + + + name + meta.tag.style.html + patterns + + + include + #tag-stuff + + + begin + (?<!</(?:[sS][tT][yY][lL][eE]))(>) + captures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.style.html + + + end + (</)((?i:style)) + contentName + source.css.embedded.html + patterns + + + begin + /\* + captures + + 0 + + name + punctuation.definition.comment.css + + + end + \*/|(?=</[sS][tT][yY][lL][eE]) + name + comment.block.css + + + include + source.css + + + + + + + begin + (?:^\s+)?(<)((?i:script))\b(?![^>]*/>) + beginCaptures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.script.html + + + end + (?<=</([sS][cC][rR][iI][pP][tT]))(>)(?:\s*\n)? + endCaptures + + 2 + + name + punctuation.definition.tag.html + + + name + meta.tag.script.html + patterns + + + include + #tag-stuff + + + begin + (?<!</(?:[sS][cC][rR][iI][pP][tT]))(>) + captures + + 1 + + name + punctuation.definition.tag.end.html + + 2 + + name + entity.name.tag.script.html + + + end + (</)((?i:script)) + contentName + source.js.embedded.html + patterns + + + captures + + 1 + + name + punctuation.definition.comment.js + + + match + (//).*?((?=</[sS][cC][rR][iI][pP][tT])|$\n?) + name + comment.line.double-slash.js + + + begin + /\* + captures + + 0 + + name + punctuation.definition.comment.js + + + end + \*/|(?=</[sS][cC][rR][iI][pP][tT]) + name + comment.block.js + + + include + #php + + + include + source.js + + + + + + + begin + (</?)((?i:body|head|html)\b) + captures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.structure.any.html + + + end + (>) + endCaptures + + 1 + + name + punctuation.definition.tag.end.html + + + name + meta.tag.structure.any.html + patterns + + + include + #tag-stuff + + + + + begin + (</?)((?i:address|blockquote|dd|div|dl|dt|fieldset|form|frame|frameset|h1|h2|h3|h4|h5|h6|iframe|noframes|object|ol|p|ul|applet|center|dir|hr|menu|pre)\b) + beginCaptures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.block.any.html + + + end + (>) + endCaptures + + 1 + + name + punctuation.definition.tag.end.html + + + name + meta.tag.block.any.html + patterns + + + include + #tag-stuff + + + + + begin + (</?)((?i:a|abbr|acronym|area|b|base|basefont|bdo|big|br|button|caption|cite|code|col|colgroup|del|dfn|em|font|head|html|i|img|input|ins|isindex|kbd|label|legend|li|link|map|meta|noscript|optgroup|option|param|q|s|samp|script|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|title|tr|tt|u|var)\b) + beginCaptures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.inline.any.html + + + end + ((?: ?/)?>) + endCaptures + + 1 + + name + punctuation.definition.tag.end.html + + + name + meta.tag.inline.any.html + patterns + + + include + #tag-stuff + + + + + begin + (</?)([a-zA-Z0-9:]+) + beginCaptures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.other.html + + + end + (>) + endCaptures + + 1 + + name + punctuation.definition.tag.end.html + + + name + meta.tag.other.html + patterns + + + include + #tag-stuff + + + + + include + #entities + + + match + <> + name + invalid.illegal.incomplete.html + + + match + < + name + invalid.illegal.bad-angle-bracket.html + + + repository + + embedded-code + + patterns + + + include + #ruby + + + include + #php + + + + include + #python + + + + entities + + patterns + + + captures + + 1 + + name + punctuation.definition.entity.html + + 3 + + name + punctuation.definition.entity.html + + + match + (&)([a-zA-Z0-9]+|#[0-9]+|#x[0-9a-fA-F]+)(;) + name + constant.character.entity.html + + + match + & + name + invalid.illegal.bad-ampersand.html + + + + php + + begin + (?=(^\s*)?<\?) + end + (?!(^\s*)?<\?) + patterns + + + include + source.php + + + + python + + begin + (?:^\s*)<\?python(?!.*\?>) + end + \?>(?:\s*$\n)? + contentName + source.python.embedded.html + patterns + + + include + source.python + + + + ruby + + patterns + + + begin + <%+# + captures + + 0 + + name + punctuation.definition.comment.erb + + + end + %> + name + comment.block.erb + + + begin + <%+(?!>)=? + captures + + 0 + + name + punctuation.section.embedded.ruby + + + end + -?%> + contentName + source.ruby.embedded.html + patterns + + + captures + + 1 + + name + punctuation.definition.comment.ruby + + + match + (#).*?(?=-?%>) + name + comment.line.number-sign.ruby + + + include + source.ruby + + + + + begin + <\?r(?!>)=? + captures + + 0 + + name + punctuation.section.embedded.ruby.nitro + + + end + -?\?> + contentName + source.ruby.nitro.embedded.html + patterns + + + captures + + 1 + + name + punctuation.definition.comment.ruby.nitro + + + match + (#).*?(?=-?\?>) + name + comment.line.number-sign.ruby.nitro + + + include + source.ruby + + + + + + + string-double-quoted + + begin + " + beginCaptures + + 0 + + name + punctuation.definition.string.begin.html + + + end + " + endCaptures + + 0 + + name + punctuation.definition.string.end.html + + + name + string.quoted.double.html + patterns + + + include + #embedded-code + + + include + #entities + + + + string-single-quoted + + begin + ' + beginCaptures + + 0 + + name + punctuation.definition.string.begin.html + + + end + ' + endCaptures + + 0 + + name + punctuation.definition.string.end.html + + + name + string.quoted.single.html + patterns + + + include + #embedded-code + + + include + #entities + + + + tag-generic-attribute + + match + \b([a-zA-Z\-:]+) + name + entity.other.attribute-name.html + + tag-id-attribute + + begin + \b(id)\b\s*(=) + captures + + 1 + + name + entity.other.attribute-name.id.html + + 2 + + name + punctuation.separator.key-value.html + + + end + (?<='|") + name + meta.attribute-with-value.id.html + patterns + + + begin + " + beginCaptures + + 0 + + name + punctuation.definition.string.begin.html + + + contentName + meta.toc-list.id.html + end + " + endCaptures + + 0 + + name + punctuation.definition.string.end.html + + + name + string.quoted.double.html + patterns + + + include + #embedded-code + + + include + #entities + + + + + begin + ' + beginCaptures + + 0 + + name + punctuation.definition.string.begin.html + + + contentName + meta.toc-list.id.html + end + ' + endCaptures + + 0 + + name + punctuation.definition.string.end.html + + + name + string.quoted.single.html + patterns + + + include + #embedded-code + + + include + #entities + + + + + + tag-stuff + + patterns + + + include + #tag-id-attribute + + + include + #tag-generic-attribute + + + include + #string-double-quoted + + + include + #string-single-quoted + + + include + #embedded-code + + + + + scopeName + text.html.basic + uuid + 17994EC8-6B1D-11D9-AC3A-000D93589AF6 + + diff --git a/Packages/HTML/sublimelinter.version b/Packages/HTML/sublimelinter.version new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/Packages/HTML/sublimelinter.version @@ -0,0 +1 @@ +2 diff --git a/Packages/PyV8/linux64-p3/PyV8.py b/Packages/PyV8/linux64-p3/PyV8.py new file mode 100644 index 0000000..15e1fdb --- /dev/null +++ b/Packages/PyV8/linux64-p3/PyV8.py @@ -0,0 +1,2750 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + + +import sys, os, re +import logging +import collections + +is_py3k = sys.version_info[0] > 2 + +if is_py3k: + import _thread as thread + + from io import StringIO + + str = str + raw_input = input +else: + import _thread + +try: + from io import StringIO +except ImportError: + from io import StringIO + +try: + import json +except ImportError: + import simplejson as json + +import _PyV8 + +__author__ = 'Flier Lu ' +__version__ = '1.0' + +__all__ = ["ReadOnly", "DontEnum", "DontDelete", "Internal", + "JSError", "JSObject", "JSArray", "JSFunction", + "JSClass", "JSEngine", "JSContext", + "JSObjectSpace", "JSAllocationAction", + "JSStackTrace", "JSStackFrame", "profiler", + "JSExtension", "JSLocker", "JSUnlocker", "AST"] + +class JSAttribute(object): + def __init__(self, name): + self.name = name + + def __call__(self, func): + setattr(func, "__%s__" % self.name, True) + + return func + +ReadOnly = JSAttribute(name='readonly') +DontEnum = JSAttribute(name='dontenum') +DontDelete = JSAttribute(name='dontdel') +Internal = JSAttribute(name='internal') + +class JSError(Exception): + def __init__(self, impl): + Exception.__init__(self) + + self._impl = impl + + def __str__(self): + return str(self._impl) + + def __unicode__(self, *args, **kwargs): + return str(self._impl) + + def __getattribute__(self, attr): + impl = super(JSError, self).__getattribute__("_impl") + + try: + return getattr(impl, attr) + except AttributeError: + return super(JSError, self).__getattribute__(attr) + + RE_FRAME = re.compile(r"\s+at\s(?:new\s)?(?P.+)\s\((?P[^:]+):?(?P\d+)?:?(?P\d+)?\)") + RE_FUNC = re.compile(r"\s+at\s(?:new\s)?(?P.+)\s\((?P[^\)]+)\)") + RE_FILE = re.compile(r"\s+at\s(?P[^:]+):?(?P\d+)?:?(?P\d+)?") + + @staticmethod + def parse_stack(value): + stack = [] + + def int_or_nul(value): + return int(value) if value else None + + for line in value.split('\n')[1:]: + m = JSError.RE_FRAME.match(line) + + if m: + stack.append((m.group('func'), m.group('file'), int_or_nul(m.group('row')), int_or_nul(m.group('col')))) + continue + + m = JSError.RE_FUNC.match(line) + + if m: + stack.append((m.group('func'), m.group('file'), None, None)) + continue + + m = JSError.RE_FILE.match(line) + + if m: + stack.append((None, m.group('file'), int_or_nul(m.group('row')), int_or_nul(m.group('col')))) + continue + + assert line + + return stack + + @property + def frames(self): + return self.parse_stack(self.stackTrace) + +_PyV8._JSError._jsclass = JSError + +JSObject = _PyV8.JSObject +JSArray = _PyV8.JSArray +JSFunction = _PyV8.JSFunction + +# contribute by e.generalov + +JS_ESCAPABLE = re.compile(r'([^\x00-\x7f])') +HAS_UTF8 = re.compile(r'[\x80-\xff]') + +def _js_escape_unicode_re_callack(match): + n = ord(match.group(0)) + if n < 0x10000: + return '\\u%04x' % (n,) + else: + # surrogate pair + n -= 0x10000 + s1 = 0xd800 | ((n >> 10) & 0x3ff) + s2 = 0xdc00 | (n & 0x3ff) + return '\\u%04x\\u%04x' % (s1, s2) + +def js_escape_unicode(text): + """Return an ASCII-only representation of a JavaScript string""" + if isinstance(text, str): + if HAS_UTF8.search(text) is None: + return text + + text = text.decode('UTF-8') + + return str(JS_ESCAPABLE.sub(_js_escape_unicode_re_callack, text)) + +class JSExtension(_PyV8.JSExtension): + def __init__(self, name, source, callback=None, dependencies=[], register=True): + _PyV8.JSExtension.__init__(self, js_escape_unicode(name), js_escape_unicode(source), callback, dependencies, register) + +def func_apply(self, thisArg, argArray=[]): + if isinstance(thisArg, JSObject): + return self.invoke(thisArg, argArray) + + this = JSContext.current.eval("(%s)" % json.dumps(thisArg)) + + return self.invoke(this, argArray) + +JSFunction.apply = func_apply + +class JSLocker(_PyV8.JSLocker): + def __enter__(self): + self.enter() + + if JSContext.entered: + self.leave() + raise RuntimeError("Lock should be acquired before enter the context") + + return self + + def __exit__(self, exc_type, exc_value, traceback): + if JSContext.entered: + self.leave() + raise RuntimeError("Lock should be released after leave the context") + + self.leave() + + if is_py3k: + def __bool__(self): + return self.entered() + else: + def __nonzero__(self): + return self.entered() + +class JSUnlocker(_PyV8.JSUnlocker): + def __enter__(self): + self.enter() + + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.leave() + + if is_py3k: + def __bool__(self): + return self.entered() + else: + def __nonzero__(self): + return self.entered() + +class JSClass(object): + __properties__ = {} + __watchpoints__ = {} + + def __getattr__(self, name): + if name == 'constructor': + return JSClassConstructor(self.__class__) + + if name == 'prototype': + return JSClassPrototype(self.__class__) + + prop = self.__dict__.setdefault('__properties__', {}).get(name, None) + + if prop and isinstance(prop[0], collections.Callable): + return prop[0]() + + raise AttributeError(name) + + def __setattr__(self, name, value): + prop = self.__dict__.setdefault('__properties__', {}).get(name, None) + + if prop and isinstance(prop[1], collections.Callable): + return prop[1](value) + + return object.__setattr__(self, name, value) + + def toString(self): + "Returns a string representation of an object." + return "[object %s]" % self.__class__.__name__ + + def toLocaleString(self): + "Returns a value as a string value appropriate to the host environment's current locale." + return self.toString() + + def valueOf(self): + "Returns the primitive value of the specified object." + return self + + def hasOwnProperty(self, name): + "Returns a Boolean value indicating whether an object has a property with the specified name." + return hasattr(self, name) + + def isPrototypeOf(self, obj): + "Returns a Boolean value indicating whether an object exists in the prototype chain of another object." + raise NotImplementedError() + + def __defineGetter__(self, name, getter): + "Binds an object's property to a function to be called when that property is looked up." + self.__properties__[name] = (getter, self.__lookupSetter__(name)) + + def __lookupGetter__(self, name): + "Return the function bound as a getter to the specified property." + return self.__properties__.get(name, (None, None))[0] + + def __defineSetter__(self, name, setter): + "Binds an object's property to a function to be called when an attempt is made to set that property." + self.__properties__[name] = (self.__lookupGetter__(name), setter) + + def __lookupSetter__(self, name): + "Return the function bound as a setter to the specified property." + return self.__properties__.get(name, (None, None))[1] + + def watch(self, prop, handler): + "Watches for a property to be assigned a value and runs a function when that occurs." + self.__watchpoints__[prop] = handler + + def unwatch(self, prop): + "Removes a watchpoint set with the watch method." + del self.__watchpoints__[prop] + +class JSClassConstructor(JSClass): + def __init__(self, cls): + self.cls = cls + + @property + def name(self): + return self.cls.__name__ + + def toString(self): + return "function %s() {\n [native code]\n}" % self.name + + def __call__(self, *args, **kwds): + return self.cls(*args, **kwds) + +class JSClassPrototype(JSClass): + def __init__(self, cls): + self.cls = cls + + @property + def constructor(self): + return JSClassConstructor(self.cls) + + @property + def name(self): + return self.cls.__name__ + +class JSDebugProtocol(object): + """ + Support the V8 debugger JSON based protocol. + + + """ + class Packet(object): + REQUEST = 'request' + RESPONSE = 'response' + EVENT = 'event' + + def __init__(self, payload): + self.data = json.loads(payload) if type(payload) in [str, str] else payload + + @property + def seq(self): + return self.data['seq'] + + @property + def type(self): + return self.data['type'] + + class Request(Packet): + @property + def cmd(self): + return self.data['command'] + + @property + def args(self): + return self.data['args'] + + class Response(Packet): + @property + def request_seq(self): + return self.data['request_seq'] + + @property + def cmd(self): + return self.data['command'] + + @property + def body(self): + return self.data['body'] + + @property + def running(self): + return self.data['running'] + + @property + def success(self): + return self.data['success'] + + @property + def message(self): + return self.data['message'] + + class Event(Packet): + @property + def event(self): + return self.data['event'] + + @property + def body(self): + return self.data['body'] + + def __init__(self): + self.seq = 0 + + def nextSeq(self): + seq = self.seq + self.seq += 1 + + return seq + + def parsePacket(self, payload): + obj = json.loads(payload) + + return JSDebugProtocol.Event(obj) if obj['type'] == 'event' else JSDebugProtocol.Response(obj) + +class JSDebugEvent(_PyV8.JSDebugEvent): + class FrameData(object): + def __init__(self, frame, count, name, value): + self.frame = frame + self.count = count + self.name = name + self.value = value + + def __len__(self): + return self.count(self.frame) + + def __iter__(self): + for i in range(self.count(self.frame)): + yield (self.name(self.frame, i), self.value(self.frame, i)) + + class Frame(object): + def __init__(self, frame): + self.frame = frame + + @property + def index(self): + return int(self.frame.index()) + + @property + def function(self): + return self.frame.func() + + @property + def receiver(self): + return self.frame.receiver() + + @property + def isConstructCall(self): + return bool(self.frame.isConstructCall()) + + @property + def isDebuggerFrame(self): + return bool(self.frame.isDebuggerFrame()) + + @property + def argumentCount(self): + return int(self.frame.argumentCount()) + + def argumentName(self, idx): + return str(self.frame.argumentName(idx)) + + def argumentValue(self, idx): + return self.frame.argumentValue(idx) + + @property + def arguments(self): + return JSDebugEvent.FrameData(self, self.argumentCount, self.argumentName, self.argumentValue) + + def localCount(self, idx): + return int(self.frame.localCount()) + + def localName(self, idx): + return str(self.frame.localName(idx)) + + def localValue(self, idx): + return self.frame.localValue(idx) + + @property + def locals(self): + return JSDebugEvent.FrameData(self, self.localCount, self.localName, self.localValue) + + @property + def sourcePosition(self): + return self.frame.sourcePosition() + + @property + def sourceLine(self): + return int(self.frame.sourceLine()) + + @property + def sourceColumn(self): + return int(self.frame.sourceColumn()) + + @property + def sourceLineText(self): + return str(self.frame.sourceLineText()) + + def evaluate(self, source, disable_break = True): + return self.frame.evaluate(source, disable_break) + + @property + def invocationText(self): + return str(self.frame.invocationText()) + + @property + def sourceAndPositionText(self): + return str(self.frame.sourceAndPositionText()) + + @property + def localsText(self): + return str(self.frame.localsText()) + + def __str__(self): + return str(self.frame.toText()) + + class Frames(object): + def __init__(self, state): + self.state = state + + def __len__(self): + return self.state.frameCount + + def __iter__(self): + for i in range(self.state.frameCount): + yield self.state.frame(i) + + class State(object): + def __init__(self, state): + self.state = state + + @property + def frameCount(self): + return int(self.state.frameCount()) + + def frame(self, idx = None): + return JSDebugEvent.Frame(self.state.frame(idx)) + + @property + def selectedFrame(self): + return int(self.state.selectedFrame()) + + @property + def frames(self): + return JSDebugEvent.Frames(self) + + def __repr__(self): + s = StringIO() + + try: + for frame in self.frames: + s.write(str(frame)) + + return s.getvalue() + finally: + s.close() + + class DebugEvent(object): + pass + + class StateEvent(DebugEvent): + __state = None + + @property + def state(self): + if not self.__state: + self.__state = JSDebugEvent.State(self.event.executionState()) + + return self.__state + + class BreakEvent(StateEvent): + type = _PyV8.JSDebugEvent.Break + + def __init__(self, event): + self.event = event + + class ExceptionEvent(StateEvent): + type = _PyV8.JSDebugEvent.Exception + + def __init__(self, event): + self.event = event + + class NewFunctionEvent(DebugEvent): + type = _PyV8.JSDebugEvent.NewFunction + + def __init__(self, event): + self.event = event + + class Script(object): + def __init__(self, script): + self.script = script + + @property + def source(self): + return self.script.source() + + @property + def id(self): + return self.script.id() + + @property + def name(self): + return self.script.name() + + @property + def lineOffset(self): + return self.script.lineOffset() + + @property + def lineCount(self): + return self.script.lineCount() + + @property + def columnOffset(self): + return self.script.columnOffset() + + @property + def type(self): + return self.script.type() + + def __repr__(self): + return "<%s script %s @ %d:%d> : '%s'" % (self.type, self.name, + self.lineOffset, self.columnOffset, + self.source) + + class CompileEvent(StateEvent): + def __init__(self, event): + self.event = event + + @property + def script(self): + if not hasattr(self, "_script"): + setattr(self, "_script", JSDebugEvent.Script(self.event.script())) + + return self._script + + def __str__(self): + return str(self.script) + + class BeforeCompileEvent(CompileEvent): + type = _PyV8.JSDebugEvent.BeforeCompile + + def __init__(self, event): + JSDebugEvent.CompileEvent.__init__(self, event) + + def __repr__(self): + return "before compile script: %s\n%s" % (repr(self.script), repr(self.state)) + + class AfterCompileEvent(CompileEvent): + type = _PyV8.JSDebugEvent.AfterCompile + + def __init__(self, event): + JSDebugEvent.CompileEvent.__init__(self, event) + + def __repr__(self): + return "after compile script: %s\n%s" % (repr(self.script), repr(self.state)) + + onMessage = None + onBreak = None + onException = None + onNewFunction = None + onBeforeCompile = None + onAfterCompile = None + +class JSDebugger(JSDebugProtocol, JSDebugEvent): + def __init__(self): + JSDebugProtocol.__init__(self) + JSDebugEvent.__init__(self) + + def __enter__(self): + self.enabled = True + + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.enabled = False + + @property + def context(self): + if not hasattr(self, '_context'): + self._context = JSContext(ctxt=_PyV8.debug().context) + + return self._context + + def isEnabled(self): + return _PyV8.debug().enabled + + def setEnabled(self, enable): + dbg = _PyV8.debug() + + if enable: + dbg.onDebugEvent = self.onDebugEvent + dbg.onDebugMessage = self.onDebugMessage + dbg.onDispatchDebugMessages = self.onDispatchDebugMessages + else: + dbg.onDebugEvent = None + dbg.onDebugMessage = None + dbg.onDispatchDebugMessages = None + + dbg.enabled = enable + + enabled = property(isEnabled, setEnabled) + + def onDebugMessage(self, msg, data): + if self.onMessage: + self.onMessage(json.loads(msg)) + + def onDebugEvent(self, type, state, evt): + if type == JSDebugEvent.Break: + if self.onBreak: self.onBreak(JSDebugEvent.BreakEvent(evt)) + elif type == JSDebugEvent.Exception: + if self.onException: self.onException(JSDebugEvent.ExceptionEvent(evt)) + elif type == JSDebugEvent.NewFunction: + if self.onNewFunction: self.onNewFunction(JSDebugEvent.NewFunctionEvent(evt)) + elif type == JSDebugEvent.BeforeCompile: + if self.onBeforeCompile: self.onBeforeCompile(JSDebugEvent.BeforeCompileEvent(evt)) + elif type == JSDebugEvent.AfterCompile: + if self.onAfterCompile: self.onAfterCompile(JSDebugEvent.AfterCompileEvent(evt)) + + def onDispatchDebugMessages(self): + return True + + def debugBreak(self): + _PyV8.debug().debugBreak() + + def debugBreakForCommand(self): + _PyV8.debug().debugBreakForCommand() + + def cancelDebugBreak(self): + _PyV8.debug().cancelDebugBreak() + + def processDebugMessages(self): + _PyV8.debug().processDebugMessages() + + def sendCommand(self, cmd, *args, **kwds): + request = json.dumps({ + 'seq': self.nextSeq(), + 'type': 'request', + 'command': cmd, + 'arguments': kwds + }) + + _PyV8.debug().sendCommand(request) + + return request + + def debugContinue(self, action='next', steps=1): + return self.sendCommand('continue', stepaction=action) + + def stepNext(self, steps=1): + """Step to the next statement in the current function.""" + return self.debugContinue(action='next', steps=steps) + + def stepIn(self, steps=1): + """Step into new functions invoked or the next statement in the current function.""" + return self.debugContinue(action='in', steps=steps) + + def stepOut(self, steps=1): + """Step out of the current function.""" + return self.debugContinue(action='out', steps=steps) + + def stepMin(self, steps=1): + """Perform a minimum step in the current function.""" + return self.debugContinue(action='out', steps=steps) + +class JSProfiler(_PyV8.JSProfiler): + @property + def logs(self): + pos = 0 + + while True: + size, buf = self.getLogLines(pos) + + if size == 0: + break + + for line in buf.split('\n'): + yield line + + pos += size + +profiler = JSProfiler() + +JSObjectSpace = _PyV8.JSObjectSpace +JSAllocationAction = _PyV8.JSAllocationAction + +class JSEngine(_PyV8.JSEngine): + def __init__(self): + _PyV8.JSEngine.__init__(self) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + del self + +JSScript = _PyV8.JSScript + +JSStackTrace = _PyV8.JSStackTrace +JSStackTrace.Options = _PyV8.JSStackTraceOptions +JSStackFrame = _PyV8.JSStackFrame + +class JSIsolate(_PyV8.JSIsolate): + def __enter__(self): + self.enter() + + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.leave() + + del self + +class JSContext(_PyV8.JSContext): + def __init__(self, obj=None, extensions=None, ctxt=None): + if JSLocker.active: + self.lock = JSLocker() + self.lock.enter() + + if ctxt: + _PyV8.JSContext.__init__(self, ctxt) + else: + _PyV8.JSContext.__init__(self, obj, extensions or []) + + def __enter__(self): + self.enter() + + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.leave() + + if hasattr(JSLocker, 'lock'): + self.lock.leave() + self.lock = None + + del self + +# contribute by marc boeker +def convert(obj): + if type(obj) == _PyV8.JSArray: + return [convert(v) for v in obj] + + if type(obj) == _PyV8.JSObject: + return dict([[str(k), convert(obj.__getattr__(str(k)))] for k in (obj.__dir__() if is_py3k else obj.__members__)]) + + return obj + +class AST: + Scope = _PyV8.AstScope + VarMode = _PyV8.AstVariableMode + Var = _PyV8.AstVariable + Label = _PyV8.AstLabel + NodeType = _PyV8.AstNodeType + Node = _PyV8.AstNode + Statement = _PyV8.AstStatement + Expression = _PyV8.AstExpression + Breakable = _PyV8.AstBreakableStatement + Block = _PyV8.AstBlock + Declaration = _PyV8.AstDeclaration + VariableDeclaration = _PyV8.AstVariableDeclaration + Module = _PyV8.AstModule + ModuleDeclaration = _PyV8.AstModuleDeclaration + ModuleLiteral = _PyV8.AstModuleLiteral + ModuleVariable = _PyV8.AstModuleVariable + ModulePath = _PyV8.AstModulePath + Iteration = _PyV8.AstIterationStatement + DoWhile = _PyV8.AstDoWhileStatement + While = _PyV8.AstWhileStatement + For = _PyV8.AstForStatement + ForIn = _PyV8.AstForInStatement + ExpressionStatement = _PyV8.AstExpressionStatement + Continue = _PyV8.AstContinueStatement + Break = _PyV8.AstBreakStatement + Return = _PyV8.AstReturnStatement + With = _PyV8.AstWithStatement + Case = _PyV8.AstCaseClause + Switch = _PyV8.AstSwitchStatement + Try = _PyV8.AstTryStatement + TryCatch = _PyV8.AstTryCatchStatement + TryFinally = _PyV8.AstTryFinallyStatement + Debugger = _PyV8.AstDebuggerStatement + Empty = _PyV8.AstEmptyStatement + Literal = _PyV8.AstLiteral + MaterializedLiteral = _PyV8.AstMaterializedLiteral + PropertyKind = _PyV8.AstPropertyKind + ObjectProperty = _PyV8.AstObjectProperty + Object = _PyV8.AstObjectLiteral + RegExp = _PyV8.AstRegExpLiteral + Array = _PyV8.AstArrayLiteral + VarProxy = _PyV8.AstVariableProxy + Property = _PyV8.AstProperty + Call = _PyV8.AstCall + CallNew = _PyV8.AstCallNew + CallRuntime = _PyV8.AstCallRuntime + Op = _PyV8.AstOperation + UnaryOp = _PyV8.AstUnaryOperation + BinOp = _PyV8.AstBinaryOperation + CountOp = _PyV8.AstCountOperation + CompOp = _PyV8.AstCompareOperation + Conditional = _PyV8.AstConditional + Assignment = _PyV8.AstAssignment + Throw = _PyV8.AstThrow + Function = _PyV8.AstFunctionLiteral + SharedFunction = _PyV8.AstSharedFunctionInfoLiteral + This = _PyV8.AstThisFunction + +from datetime import * +import unittest +import traceback + +if is_py3k: + def toNativeString(s): + return s + def toUnicodeString(s): + return s +else: + def toNativeString(s, encoding='utf-8'): + return s.encode(encoding) if isinstance(s, str) else s + + def toUnicodeString(s, encoding='utf-8'): + return s if isinstance(s, str) else str(s, encoding) + +class TestContext(unittest.TestCase): + def testMultiNamespace(self): + self.assertTrue(not bool(JSContext.inContext)) + self.assertTrue(not bool(JSContext.entered)) + + class Global(object): + name = "global" + + g = Global() + + with JSContext(g) as ctxt: + self.assertTrue(bool(JSContext.inContext)) + self.assertEqual(g.name, str(JSContext.entered.locals.name)) + self.assertEqual(g.name, str(JSContext.current.locals.name)) + + class Local(object): + name = "local" + + l = Local() + + with JSContext(l): + self.assertTrue(bool(JSContext.inContext)) + self.assertEqual(l.name, str(JSContext.entered.locals.name)) + self.assertEqual(l.name, str(JSContext.current.locals.name)) + + self.assertTrue(bool(JSContext.inContext)) + self.assertEqual(g.name, str(JSContext.entered.locals.name)) + self.assertEqual(g.name, str(JSContext.current.locals.name)) + + self.assertTrue(not bool(JSContext.entered)) + self.assertTrue(not bool(JSContext.inContext)) + + def _testMultiContext(self): + # Create an environment + with JSContext() as ctxt0: + ctxt0.securityToken = "password" + + global0 = ctxt0.locals + global0.custom = 1234 + + self.assertEqual(1234, int(global0.custom)) + + # Create an independent environment + with JSContext() as ctxt1: + ctxt1.securityToken = ctxt0.securityToken + + global1 = ctxt1.locals + global1.custom = 1234 + + with ctxt0: + self.assertEqual(1234, int(global0.custom)) + self.assertEqual(1234, int(global1.custom)) + + # Now create a new context with the old global + with JSContext(global1) as ctxt2: + ctxt2.securityToken = ctxt1.securityToken + + with ctxt1: + self.assertEqual(1234, int(global1.custom)) + + def _testSecurityChecks(self): + with JSContext() as env1: + env1.securityToken = "foo" + + # Create a function in env1. + env1.eval("spy=function(){return spy;}") + + spy = env1.locals.spy + + self.assertTrue(isinstance(spy, _PyV8.JSFunction)) + + # Create another function accessing global objects. + env1.eval("spy2=function(){return 123;}") + + spy2 = env1.locals.spy2 + + self.assertTrue(isinstance(spy2, _PyV8.JSFunction)) + + # Switch to env2 in the same domain and invoke spy on env2. + env2 = JSContext() + + env2.securityToken = "foo" + + with env2: + result = spy.apply(env2.locals) + + self.assertTrue(isinstance(result, _PyV8.JSFunction)) + + env2.securityToken = "bar" + + # Call cross_domain_call, it should throw an exception + with env2: + self.assertRaises(JSError, spy2.apply, env2.locals) + + def _testCrossDomainDelete(self): + with JSContext() as env1: + env2 = JSContext() + + # Set to the same domain. + env1.securityToken = "foo" + env2.securityToken = "foo" + + env1.locals.prop = 3 + + env2.locals.env1 = env1.locals + + # Change env2 to a different domain and delete env1.prop. + #env2.securityToken = "bar" + + self.assertEqual(3, int(env1.eval("prop"))) + + with env2: + self.assertEqual(3, int(env2.eval("this.env1.prop"))) + self.assertEqual("false", str(env2.eval("delete env1.prop"))) + + # Check that env1.prop still exists. + self.assertEqual(3, int(env1.locals.prop)) + +class TestWrapper(unittest.TestCase): + def testObject(self): + with JSContext() as ctxt: + o = ctxt.eval("new Object()") + + self.assertTrue(hash(o) > 0) + + o1 = o.clone() + + self.assertEqual(hash(o1), hash(o)) + self.assertTrue(o != o1) + + self.assertRaises(UnboundLocalError, o.clone) + + def testAutoConverter(self): + with JSContext() as ctxt: + ctxt.eval(""" + var_i = 1; + var_f = 1.0; + var_s = "test"; + var_b = true; + var_s_obj = new String("test"); + var_b_obj = new Boolean(true); + var_f_obj = new Number(1.5); + """) + + vars = ctxt.locals + + var_i = vars.var_i + + self.assertTrue(var_i) + self.assertEqual(1, int(var_i)) + + var_f = vars.var_f + + self.assertTrue(var_f) + self.assertEqual(1.0, float(vars.var_f)) + + var_s = vars.var_s + self.assertTrue(var_s) + self.assertEqual("test", str(vars.var_s)) + + var_b = vars.var_b + self.assertTrue(var_b) + self.assertTrue(bool(var_b)) + + self.assertEqual("test", vars.var_s_obj) + self.assertTrue(vars.var_b_obj) + self.assertEqual(1.5, vars.var_f_obj) + + attrs = dir(ctxt.locals) + + self.assertTrue(attrs) + self.assertTrue("var_i" in attrs) + self.assertTrue("var_f" in attrs) + self.assertTrue("var_s" in attrs) + self.assertTrue("var_b" in attrs) + self.assertTrue("var_s_obj" in attrs) + self.assertTrue("var_b_obj" in attrs) + self.assertTrue("var_f_obj" in attrs) + + def testExactConverter(self): + class MyInteger(int, JSClass): + pass + + class MyString(str, JSClass): + pass + + class MyUnicode(str, JSClass): + pass + + class MyDateTime(time, JSClass): + pass + + class Global(JSClass): + var_bool = True + var_int = 1 + var_float = 1.0 + var_str = 'str' + var_unicode = 'unicode' + var_datetime = datetime.now() + var_date = date.today() + var_time = time() + + var_myint = MyInteger() + var_mystr = MyString('mystr') + var_myunicode = MyUnicode('myunicode') + var_mytime = MyDateTime() + + with JSContext(Global()) as ctxt: + typename = ctxt.eval("(function (name) { return this[name].constructor.name; })") + typeof = ctxt.eval("(function (name) { return typeof(this[name]); })") + + self.assertEqual('Boolean', typename('var_bool')) + self.assertEqual('Number', typename('var_int')) + self.assertEqual('Number', typename('var_float')) + self.assertEqual('String', typename('var_str')) + self.assertEqual('String', typename('var_unicode')) + self.assertEqual('Date', typename('var_datetime')) + self.assertEqual('Date', typename('var_date')) + self.assertEqual('Date', typename('var_time')) + + self.assertEqual('MyInteger', typename('var_myint')) + self.assertEqual('MyString', typename('var_mystr')) + self.assertEqual('MyUnicode', typename('var_myunicode')) + self.assertEqual('MyDateTime', typename('var_mytime')) + + self.assertEqual('object', typeof('var_myint')) + self.assertEqual('object', typeof('var_mystr')) + self.assertEqual('object', typeof('var_myunicode')) + self.assertEqual('object', typeof('var_mytime')) + + def testJavascriptWrapper(self): + with JSContext() as ctxt: + self.assertEqual(type(None), type(ctxt.eval("null"))) + self.assertEqual(type(None), type(ctxt.eval("undefined"))) + self.assertEqual(bool, type(ctxt.eval("true"))) + self.assertEqual(str, type(ctxt.eval("'test'"))) + self.assertEqual(int, type(ctxt.eval("123"))) + self.assertEqual(float, type(ctxt.eval("3.14"))) + self.assertEqual(datetime, type(ctxt.eval("new Date()"))) + self.assertEqual(JSArray, type(ctxt.eval("[1, 2, 3]"))) + self.assertEqual(JSFunction, type(ctxt.eval("(function() {})"))) + self.assertEqual(JSObject, type(ctxt.eval("new Object()"))) + + def testPythonWrapper(self): + with JSContext() as ctxt: + typeof = ctxt.eval("(function type(value) { return typeof value; })") + protoof = ctxt.eval("(function protoof(value) { return Object.prototype.toString.apply(value); })") + + self.assertEqual('[object Null]', protoof(None)) + self.assertEqual('boolean', typeof(True)) + self.assertEqual('number', typeof(123)) + self.assertEqual('number', typeof(3.14)) + self.assertEqual('string', typeof('test')) + self.assertEqual('string', typeof('test')) + + self.assertEqual('[object Date]', protoof(datetime.now())) + self.assertEqual('[object Date]', protoof(date.today())) + self.assertEqual('[object Date]', protoof(time())) + + def test(): + pass + + self.assertEqual('[object Function]', protoof(abs)) + self.assertEqual('[object Function]', protoof(test)) + self.assertEqual('[object Function]', protoof(self.testPythonWrapper)) + self.assertEqual('[object Function]', protoof(int)) + + def testFunction(self): + with JSContext() as ctxt: + func = ctxt.eval(""" + (function () + { + function a() + { + return "abc"; + } + + return a(); + }) + """) + + self.assertEqual("abc", str(func())) + self.assertTrue(func != None) + self.assertFalse(func == None) + + func = ctxt.eval("(function test() {})") + + self.assertEqual("test", func.name) + self.assertEqual("", func.resname) + self.assertEqual(0, func.linenum) + self.assertEqual(14, func.colnum) + self.assertEqual(0, func.lineoff) + self.assertEqual(0, func.coloff) + + #TODO fix me, why the setter doesn't work? + # func.name = "hello" + # it seems __setattr__ was called instead of CJavascriptFunction::SetName + + func.setName("hello") + + self.assertEqual("hello", func.name) + + def testCall(self): + class Hello(object): + def __call__(self, name): + return "hello " + name + + class Global(JSClass): + hello = Hello() + + with JSContext(Global()) as ctxt: + self.assertEqual("hello flier", ctxt.eval("hello('flier')")) + + def testJSFunction(self): + with JSContext() as ctxt: + hello = ctxt.eval("(function (name) { return 'hello ' + name; })") + + self.assertTrue(isinstance(hello, _PyV8.JSFunction)) + self.assertEqual("hello flier", hello('flier')) + self.assertEqual("hello flier", hello.invoke(['flier'])) + + obj = ctxt.eval("({ 'name': 'flier', 'hello': function (name) { return 'hello ' + name + ' from ' + this.name; }})") + hello = obj.hello + self.assertTrue(isinstance(hello, JSFunction)) + self.assertEqual("hello flier from flier", hello('flier')) + + tester = ctxt.eval("({ 'name': 'tester' })") + self.assertEqual("hello flier from tester", hello.invoke(tester, ['flier'])) + self.assertEqual("hello flier from json", hello.apply({ 'name': 'json' }, ['flier'])) + + def testConstructor(self): + with JSContext() as ctx: + ctx.eval(""" + var Test = function() { + this.trySomething(); + }; + Test.prototype.trySomething = function() { + this.name = 'flier'; + }; + + var Test2 = function(first_name, last_name) { + this.name = first_name + ' ' + last_name; + }; + """) + + self.assertTrue(isinstance(ctx.locals.Test, _PyV8.JSFunction)) + + test = JSObject.create(ctx.locals.Test) + + self.assertTrue(isinstance(ctx.locals.Test, _PyV8.JSObject)) + self.assertEqual("flier", test.name); + + test2 = JSObject.create(ctx.locals.Test2, ('Flier', 'Lu')) + + self.assertEqual("Flier Lu", test2.name); + + test3 = JSObject.create(ctx.locals.Test2, ('Flier', 'Lu'), { 'email': 'flier.lu@gmail.com' }) + + self.assertEqual("flier.lu@gmail.com", test3.email); + + def testJSError(self): + with JSContext() as ctxt: + try: + ctxt.eval('throw "test"') + self.fail() + except: + self.assertTrue(JSError, sys.exc_info()[0]) + + def testErrorInfo(self): + with JSContext() as ctxt: + with JSEngine() as engine: + try: + engine.compile(""" + function hello() + { + throw Error("hello world"); + } + + hello();""", "test", 10, 10).run() + self.fail() + except JSError as e: + self.assertTrue(str(e).startswith('JSError: Error: hello world ( test @ 14 : 34 ) ->')) + self.assertEqual("Error", e.name) + self.assertEqual("hello world", e.message) + self.assertEqual("test", e.scriptName) + self.assertEqual(14, e.lineNum) + self.assertEqual(102, e.startPos) + self.assertEqual(103, e.endPos) + self.assertEqual(34, e.startCol) + self.assertEqual(35, e.endCol) + self.assertEqual('throw Error("hello world");', e.sourceLine.strip()) + self.assertEqual('Error: hello world\n' + + ' at Error ()\n' + + ' at hello (test:14:35)\n' + + ' at test:17:25', e.stackTrace) + + def testParseStack(self): + self.assertEqual([ + ('Error', 'unknown source', None, None), + ('test', 'native', None, None), + ('', 'test0', 3, 5), + ('f', 'test1', 2, 19), + ('g', 'test2', 1, 15), + (None, 'test3', 1, None), + (None, 'test3', 1, 1), + ], JSError.parse_stack("""Error: err + at Error (unknown source) + at test (native) + at new (test0:3:5) + at f (test1:2:19) + at g (test2:1:15) + at test3:1 + at test3:1:1""")) + + def testStackTrace(self): + class Global(JSClass): + def GetCurrentStackTrace(self, limit): + return JSStackTrace.GetCurrentStackTrace(4, JSStackTrace.Options.Detailed) + + with JSContext(Global()) as ctxt: + st = ctxt.eval(""" + function a() + { + return GetCurrentStackTrace(10); + } + function b() + { + return eval("a()"); + } + function c() + { + return new b(); + } + c();""", "test") + + self.assertEqual(4, len(st)) + self.assertEqual("\tat a (test:4:28)\n\tat (eval)\n\tat b (test:8:28)\n\tat c (test:12:28)\n", str(st)) + self.assertEqual("test.a (4:28)\n. (1:1) eval\ntest.b (8:28) constructor\ntest.c (12:28)", + "\n".join(["%s.%s (%d:%d)%s%s" % ( + f.scriptName, f.funcName, f.lineNum, f.column, + ' eval' if f.isEval else '', + ' constructor' if f.isConstructor else '') for f in st])) + + def testPythonException(self): + class Global(JSClass): + def raiseException(self): + raise RuntimeError("Hello") + + with JSContext(Global()) as ctxt: + r = ctxt.eval(""" + msg =""; + try + { + this.raiseException() + } + catch(e) + { + msg += "catch " + e + ";"; + } + finally + { + msg += "finally"; + }""") + self.assertEqual("catch Error: Hello;finally", str(ctxt.locals.msg)) + + def testExceptionMapping(self): + class TestException(Exception): + pass + + class Global(JSClass): + def raiseIndexError(self): + return [1, 2, 3][5] + + def raiseAttributeError(self): + None.hello() + + def raiseSyntaxError(self): + eval("???") + + def raiseTypeError(self): + int(sys) + + def raiseNotImplementedError(self): + raise NotImplementedError("Not support") + + def raiseExceptions(self): + raise TestException() + + with JSContext(Global()) as ctxt: + ctxt.eval("try { this.raiseIndexError(); } catch (e) { msg = e; }") + + self.assertEqual("RangeError: list index out of range", str(ctxt.locals.msg)) + + ctxt.eval("try { this.raiseAttributeError(); } catch (e) { msg = e; }") + + self.assertEqual("ReferenceError: 'NoneType' object has no attribute 'hello'", str(ctxt.locals.msg)) + + ctxt.eval("try { this.raiseSyntaxError(); } catch (e) { msg = e; }") + + self.assertEqual("SyntaxError: invalid syntax", str(ctxt.locals.msg)) + + ctxt.eval("try { this.raiseTypeError(); } catch (e) { msg = e; }") + + self.assertEqual("TypeError: int() argument must be a string or a number, not 'module'", str(ctxt.locals.msg)) + + ctxt.eval("try { this.raiseNotImplementedError(); } catch (e) { msg = e; }") + + self.assertEqual("Error: Not support", str(ctxt.locals.msg)) + + self.assertRaises(TestException, ctxt.eval, "this.raiseExceptions();") + + def testArray(self): + with JSContext() as ctxt: + array = ctxt.eval(""" + var array = new Array(); + + for (i=0; i<10; i++) + { + array[i] = 10-i; + } + + array; + """) + + self.assertTrue(isinstance(array, _PyV8.JSArray)) + self.assertEqual(10, len(array)) + + self.assertTrue(5 in array) + self.assertFalse(15 in array) + + self.assertEqual(10, len(array)) + + for i in range(10): + self.assertEqual(10-i, array[i]) + + array[5] = 0 + + self.assertEqual(0, array[5]) + + del array[5] + + self.assertEqual(None, array[5]) + + # array [10, 9, 8, 7, 6, None, 4, 3, 2, 1] + # array[4:7] 4^^^^^^^^^7 + # array[-3:-1] -3^^^^^^-1 + # array[0:0] [] + + self.assertEqual([6, None, 4], array[4:7]) + self.assertEqual([3, 2], array[-3:-1]) + self.assertEqual([], array[0:0]) + + array[1:3] = [9, 9, 9] + + self.assertEqual([10, 9, 9, 9, 7, 6, None, 4, 3, 2, 1], list(array)) + + array[5:8] = [8, 8] + + self.assertEqual([10, 9, 9, 9, 7, 8, 8, 3, 2, 1], list(array)) + + del array[1:4] + + self.assertEqual([10, 7, 8, 8, 3, 2, 1], list(array)) + + ctxt.locals.array1 = JSArray(5) + ctxt.locals.array2 = JSArray([1, 2, 3, 4, 5]) + + for i in range(len(ctxt.locals.array2)): + ctxt.locals.array1[i] = ctxt.locals.array2[i] * 10 + + ctxt.eval(""" + var sum = 0; + + for (i=0; i now1 else now1 - now2 + + self.assertTrue(delta < timedelta(seconds=1)) + + func = ctxt.eval("(function (d) { return d.toString(); })") + + now = datetime.now() + + self.assertTrue(str(func(now)).startswith(now.strftime("%a %b %d %Y %H:%M:%S"))) + + def testUnicode(self): + with JSContext() as ctxt: + self.assertEqual("人", toUnicodeString(ctxt.eval("\"人\""))) + self.assertEqual("é", toUnicodeString(ctxt.eval("\"é\""))) + + func = ctxt.eval("(function (msg) { return msg.length; })") + + self.assertEqual(2, func("测试")) + + def testClassicStyleObject(self): + class FileSystemWarpper: + @property + def cwd(self): + return os.getcwd() + + class Global: + @property + def fs(self): + return FileSystemWarpper() + + with JSContext(Global()) as ctxt: + self.assertEqual(os.getcwd(), ctxt.eval("fs.cwd")) + + def testRefCount(self): + count = sys.getrefcount(None) + + class Global(JSClass): + pass + + with JSContext(Global()) as ctxt: + ctxt.eval(""" + var none = null; + """) + + self.assertEqual(count+1, sys.getrefcount(None)) + + ctxt.eval(""" + var none = null; + """) + + self.assertEqual(count+1, sys.getrefcount(None)) + + def testProperty(self): + class Global(JSClass): + def __init__(self, name): + self._name = name + def getname(self): + return self._name + def setname(self, name): + self._name = name + def delname(self): + self._name = 'deleted' + + name = property(getname, setname, delname) + + g = Global('world') + + with JSContext(g) as ctxt: + self.assertEqual('world', ctxt.eval("name")) + self.assertEqual('flier', ctxt.eval("this.name = 'flier';")) + self.assertEqual('flier', ctxt.eval("name")) + self.assertTrue(ctxt.eval("delete name")) + ### + # FIXME replace the global object with Python object + # + #self.assertEqual('deleted', ctxt.eval("name")) + #ctxt.eval("__defineGetter__('name', function() { return 'fixed'; });") + #self.assertEqual('fixed', ctxt.eval("name")) + + def testGetterAndSetter(self): + class Global(JSClass): + def __init__(self, testval): + self.testval = testval + + with JSContext(Global("Test Value A")) as ctxt: + self.assertEqual("Test Value A", ctxt.locals.testval) + ctxt.eval(""" + this.__defineGetter__("test", function() { + return this.testval; + }); + this.__defineSetter__("test", function(val) { + this.testval = val; + }); + """) + self.assertEqual("Test Value A", ctxt.locals.test) + + ctxt.eval("test = 'Test Value B';") + + self.assertEqual("Test Value B", ctxt.locals.test) + + def testDestructor(self): + import gc + + owner = self + owner.deleted = False + + class Hello(object): + def say(self): + pass + + def __del__(self): + owner.deleted = True + + def test(): + with JSContext() as ctxt: + fn = ctxt.eval("(function (obj) { obj.say(); })") + + obj = Hello() + + self.assertEqual(2, sys.getrefcount(obj)) + + fn(obj) + + self.assertEqual(4, sys.getrefcount(obj)) + + del obj + + test() + + self.assertFalse(owner.deleted) + + JSEngine.collect() + gc.collect() + + self.assertTrue(owner.deleted) + + def testNullInString(self): + with JSContext() as ctxt: + fn = ctxt.eval("(function (s) { return s; })") + + self.assertEqual("hello \0 world", fn("hello \0 world")) + + def testLivingObjectCache(self): + class Global(JSClass): + i = 1 + b = True + o = object() + + with JSContext(Global()) as ctxt: + self.assertTrue(ctxt.eval("i == i")) + self.assertTrue(ctxt.eval("b == b")) + self.assertTrue(ctxt.eval("o == o")) + + def testNamedSetter(self): + class Obj(JSClass): + @property + def p(self): + return self._p + + @p.setter + def p(self, value): + self._p = value + + class Global(JSClass): + def __init__(self): + self.obj = Obj() + self.d = {} + self.p = None + + with JSContext(Global()) as ctxt: + ctxt.eval(""" + x = obj; + x.y = 10; + x.p = 10; + d.y = 10; + """) + self.assertEqual(10, ctxt.eval("obj.y")) + self.assertEqual(10, ctxt.eval("obj.p")) + self.assertEqual(10, ctxt.locals.d['y']) + + def testWatch(self): + class Obj(JSClass): + def __init__(self): + self.p = 1 + + class Global(JSClass): + def __init__(self): + self.o = Obj() + + with JSContext(Global()) as ctxt: + ctxt.eval(""" + o.watch("p", function (id, oldval, newval) { + return oldval + newval; + }); + """) + + self.assertEqual(1, ctxt.eval("o.p")) + + ctxt.eval("o.p = 2;") + + self.assertEqual(3, ctxt.eval("o.p")) + + ctxt.eval("delete o.p;") + + self.assertEqual(None, ctxt.eval("o.p")) + + ctxt.eval("o.p = 2;") + + self.assertEqual(2, ctxt.eval("o.p")) + + ctxt.eval("o.unwatch('p');") + + ctxt.eval("o.p = 1;") + + self.assertEqual(1, ctxt.eval("o.p")) + + def testReferenceError(self): + class Global(JSClass): + def __init__(self): + self.s = self + + with JSContext(Global()) as ctxt: + self.assertRaises(ReferenceError, ctxt.eval, 'x') + + self.assertTrue(ctxt.eval("typeof(x) === 'undefined'")) + + self.assertTrue(ctxt.eval("typeof(String) === 'function'")) + + self.assertTrue(ctxt.eval("typeof(s.String) === 'undefined'")) + + self.assertTrue(ctxt.eval("typeof(s.z) === 'undefined'")) + + def testRaiseExceptionInGetter(self): + class Document(JSClass): + def __getattr__(self, name): + if name == 'y': + raise TypeError() + + return JSClass.__getattr__(self, name) + + class Global(JSClass): + def __init__(self): + self.document = Document() + + with JSContext(Global()) as ctxt: + self.assertEqual(None, ctxt.eval('document.x')) + self.assertRaises(TypeError, ctxt.eval, 'document.y') + +class TestMultithread(unittest.TestCase): + def testLocker(self): + self.assertFalse(JSLocker.active) + self.assertFalse(JSLocker.locked) + + with JSLocker() as outter_locker: + self.assertTrue(JSLocker.active) + self.assertTrue(JSLocker.locked) + + self.assertTrue(outter_locker) + + with JSLocker() as inner_locker: + self.assertTrue(JSLocker.locked) + + self.assertTrue(outter_locker) + self.assertTrue(inner_locker) + + with JSUnlocker() as unlocker: + self.assertFalse(JSLocker.locked) + + self.assertTrue(outter_locker) + self.assertTrue(inner_locker) + + self.assertTrue(JSLocker.locked) + + self.assertTrue(JSLocker.active) + self.assertFalse(JSLocker.locked) + + locker = JSLocker() + + with JSContext(): + self.assertRaises(RuntimeError, locker.__enter__) + self.assertRaises(RuntimeError, locker.__exit__, None, None, None) + + del locker + + def testMultiPythonThread(self): + import time, threading + + class Global: + count = 0 + started = threading.Event() + finished = threading.Semaphore(0) + + def sleep(self, ms): + time.sleep(ms / 1000.0) + + self.count += 1 + + g = Global() + + def run(): + with JSContext(g) as ctxt: + ctxt.eval(""" + started.wait(); + + for (i=0; i<10; i++) + { + sleep(100); + } + + finished.release(); + """) + + threading.Thread(target=run).start() + + now = time.time() + + self.assertEqual(0, g.count) + + g.started.set() + g.finished.acquire() + + self.assertEqual(10, g.count) + + self.assertTrue((time.time() - now) >= 1) + + def testMultiJavascriptThread(self): + import time, threading + + class Global: + result = [] + + def add(self, value): + with JSUnlocker(): + time.sleep(0.1) + + self.result.append(value) + + g = Global() + + def run(): + with JSContext(g) as ctxt: + ctxt.eval(""" + for (i=0; i<10; i++) + add(i); + """) + + threads = [threading.Thread(target=run), threading.Thread(target=run)] + + with JSLocker(): + for t in threads: t.start() + + for t in threads: t.join() + + self.assertEqual(20, len(g.result)) + + def _testPreemptionJavascriptThreads(self): + import time, threading + + class Global: + result = [] + + def add(self, value): + # we use preemption scheduler to switch between threads + # so, just comment the JSUnlocker + # + # with JSUnlocker() as unlocker: + time.sleep(0.1) + + self.result.append(value) + + g = Global() + + def run(): + with JSContext(g) as ctxt: + ctxt.eval(""" + for (i=0; i<10; i++) + add(i); + """) + + threads = [threading.Thread(target=run), threading.Thread(target=run)] + + with JSLocker() as locker: + JSLocker.startPreemption(100) + + for t in threads: t.start() + + for t in threads: t.join() + + self.assertEqual(20, len(g.result)) + +class TestEngine(unittest.TestCase): + def testClassProperties(self): + with JSContext() as ctxt: + self.assertTrue(str(JSEngine.version).startswith("3.")) + self.assertFalse(JSEngine.dead) + + def testCompile(self): + with JSContext() as ctxt: + with JSEngine() as engine: + s = engine.compile("1+2") + + self.assertTrue(isinstance(s, _PyV8.JSScript)) + + self.assertEqual("1+2", s.source) + self.assertEqual(3, int(s.run())) + + self.assertRaises(SyntaxError, engine.compile, "1+") + + def testPrecompile(self): + with JSContext() as ctxt: + with JSEngine() as engine: + data = engine.precompile("1+2") + + self.assertTrue(data) + self.assertEqual(28, len(data)) + + s = engine.compile("1+2", precompiled=data) + + self.assertTrue(isinstance(s, _PyV8.JSScript)) + + self.assertEqual("1+2", s.source) + self.assertEqual(3, int(s.run())) + + self.assertRaises(SyntaxError, engine.precompile, "1+") + + def testUnicodeSource(self): + class Global(JSClass): + var = '测试' + + def __getattr__(self, name): + if (name if is_py3k else name.decode('utf-8')) == '变量': + return self.var + + return JSClass.__getattr__(self, name) + + g = Global() + + with JSContext(g) as ctxt: + with JSEngine() as engine: + src = """ + function 函数() { return 变量.length; } + + 函数(); + + var func = function () {}; + """ + + data = engine.precompile(src) + + self.assertTrue(data) + self.assertEqual(68, len(data)) + + s = engine.compile(src, precompiled=data) + + self.assertTrue(isinstance(s, _PyV8.JSScript)) + + self.assertEqual(toNativeString(src), s.source) + self.assertEqual(2, s.run()) + + func_name = toNativeString('函数') + + self.assertTrue(hasattr(ctxt.locals, func_name)) + + func = getattr(ctxt.locals, func_name) + + self.assertTrue(isinstance(func, _PyV8.JSFunction)) + + self.assertEqual(func_name, func.name) + self.assertEqual("", func.resname) + self.assertEqual(1, func.linenum) + self.assertEqual(0, func.lineoff) + self.assertEqual(0, func.coloff) + + var_name = toNativeString('变量') + + setattr(ctxt.locals, var_name, '测试长字符串') + + self.assertEqual(6, func()) + + self.assertEqual("func", ctxt.locals.func.inferredname) + + def testExtension(self): + extSrc = """function hello(name) { return "hello " + name + " from javascript"; }""" + extJs = JSExtension("hello/javascript", extSrc) + + self.assertTrue(extJs) + self.assertEqual("hello/javascript", extJs.name) + self.assertEqual(extSrc, extJs.source) + self.assertFalse(extJs.autoEnable) + self.assertTrue(extJs.registered) + + TestEngine.extJs = extJs + + with JSContext(extensions=['hello/javascript']) as ctxt: + self.assertEqual("hello flier from javascript", ctxt.eval("hello('flier')")) + + # test the auto enable property + + with JSContext() as ctxt: + self.assertRaises(ReferenceError, ctxt.eval, "hello('flier')") + + extJs.autoEnable = True + self.assertTrue(extJs.autoEnable) + + with JSContext() as ctxt: + self.assertEqual("hello flier from javascript", ctxt.eval("hello('flier')")) + + extJs.autoEnable = False + self.assertFalse(extJs.autoEnable) + + with JSContext() as ctxt: + self.assertRaises(ReferenceError, ctxt.eval, "hello('flier')") + + extUnicodeSrc = """function helloW(name) { return "hello " + name + " from javascript"; }""" + extUnicodeJs = JSExtension("helloW/javascript", extUnicodeSrc) + + self.assertTrue(extUnicodeJs) + self.assertEqual("helloW/javascript", extUnicodeJs.name) + self.assertEqual(toNativeString(extUnicodeSrc), extUnicodeJs.source) + self.assertFalse(extUnicodeJs.autoEnable) + self.assertTrue(extUnicodeJs.registered) + + TestEngine.extUnicodeJs = extUnicodeJs + + with JSContext(extensions=['helloW/javascript']) as ctxt: + self.assertEqual("hello flier from javascript", ctxt.eval("helloW('flier')")) + + ret = ctxt.eval("helloW('世界')") + + self.assertEqual("hello 世界 from javascript", ret if is_py3k else ret.decode('UTF-8')) + + def testNativeExtension(self): + extSrc = "native function hello();" + extPy = JSExtension("hello/python", extSrc, lambda func: lambda name: "hello " + name + " from python", register=False) + self.assertTrue(extPy) + self.assertEqual("hello/python", extPy.name) + self.assertEqual(extSrc, extPy.source) + self.assertFalse(extPy.autoEnable) + self.assertFalse(extPy.registered) + extPy.register() + self.assertTrue(extPy.registered) + + TestEngine.extPy = extPy + + with JSContext(extensions=['hello/python']) as ctxt: + self.assertEqual("hello flier from python", ctxt.eval("hello('flier')")) + + def _testSerialize(self): + data = None + + self.assertFalse(JSContext.entered) + + with JSContext() as ctxt: + self.assertTrue(JSContext.entered) + + #ctxt.eval("function hello(name) { return 'hello ' + name; }") + + data = JSEngine.serialize() + + self.assertTrue(data) + self.assertTrue(len(data) > 0) + + self.assertFalse(JSContext.entered) + + #JSEngine.deserialize() + + self.assertTrue(JSContext.entered) + + self.assertEqual('hello flier', JSContext.current.eval("hello('flier');")) + + def testEval(self): + with JSContext() as ctxt: + self.assertEqual(3, int(ctxt.eval("1+2"))) + + def testGlobal(self): + class Global(JSClass): + version = "1.0" + + with JSContext(Global()) as ctxt: + vars = ctxt.locals + + # getter + self.assertEqual(Global.version, str(vars.version)) + self.assertEqual(Global.version, str(ctxt.eval("version"))) + + self.assertRaises(ReferenceError, ctxt.eval, "nonexists") + + # setter + self.assertEqual(2.0, float(ctxt.eval("version = 2.0"))) + + self.assertEqual(2.0, float(vars.version)) + + def testThis(self): + class Global(JSClass): + version = 1.0 + + with JSContext(Global()) as ctxt: + self.assertEqual("[object Global]", str(ctxt.eval("this"))) + + self.assertEqual(1.0, float(ctxt.eval("this.version"))) + + def testObjectBuildInMethods(self): + class Global(JSClass): + version = 1.0 + + with JSContext(Global()) as ctxt: + self.assertEqual("[object Global]", str(ctxt.eval("this.toString()"))) + self.assertEqual("[object Global]", str(ctxt.eval("this.toLocaleString()"))) + self.assertEqual(Global.version, float(ctxt.eval("this.valueOf()").version)) + + self.assertTrue(bool(ctxt.eval("this.hasOwnProperty(\"version\")"))) + + self.assertFalse(ctxt.eval("this.hasOwnProperty(\"nonexistent\")")) + + def testPythonWrapper(self): + class Global(JSClass): + s = [1, 2, 3] + d = {'a': {'b': 'c'}, 'd': ['e', 'f']} + + g = Global() + + with JSContext(g) as ctxt: + ctxt.eval(""" + s[2] = s[1] + 2; + s[0] = s[1]; + delete s[1]; + """) + self.assertEqual([2, 4], g.s) + self.assertEqual('c', ctxt.eval("d.a.b")) + self.assertEqual(['e', 'f'], ctxt.eval("d.d")) + ctxt.eval(""" + d.a.q = 4 + delete d.d + """) + self.assertEqual(4, g.d['a']['q']) + self.assertEqual(None, ctxt.eval("d.d")) + + def _testMemoryAllocationCallback(self): + alloc = {} + + def callback(space, action, size): + alloc[(space, action)] = alloc.setdefault((space, action), 0) + size + + JSEngine.setMemoryAllocationCallback(callback) + + with JSContext() as ctxt: + self.assertFalse((JSObjectSpace.Code, JSAllocationAction.alloc) in alloc) + + ctxt.eval("var o = new Array(1000);") + + self.assertTrue((JSObjectSpace.Code, JSAllocationAction.alloc) in alloc) + + JSEngine.setMemoryAllocationCallback(None) + +class TestDebug(unittest.TestCase): + def setUp(self): + self.engine = JSEngine() + + def tearDown(self): + del self.engine + + events = [] + + def processDebugEvent(self, event): + try: + logging.debug("receive debug event: %s", repr(event)) + + self.events.append(repr(event)) + except: + logging.error("fail to process debug event") + logging.debug(traceback.extract_stack()) + + def testEventDispatch(self): + debugger = JSDebugger() + + self.assertTrue(not debugger.enabled) + + debugger.onBreak = lambda evt: self.processDebugEvent(evt) + debugger.onException = lambda evt: self.processDebugEvent(evt) + debugger.onNewFunction = lambda evt: self.processDebugEvent(evt) + debugger.onBeforeCompile = lambda evt: self.processDebugEvent(evt) + debugger.onAfterCompile = lambda evt: self.processDebugEvent(evt) + + with JSContext() as ctxt: + debugger.enabled = True + + self.assertEqual(3, int(ctxt.eval("function test() { text = \"1+2\"; return eval(text) } test()"))) + + debugger.enabled = False + + self.assertRaises(JSError, JSContext.eval, ctxt, "throw 1") + + self.assertTrue(not debugger.enabled) + + self.assertEqual(4, len(self.events)) + +class TestProfile(unittest.TestCase): + def _testStart(self): + self.assertFalse(profiler.started) + + profiler.start() + + self.assertTrue(profiler.started) + + profiler.stop() + + self.assertFalse(profiler.started) + + def _testResume(self): + self.assertTrue(profiler.paused) + + self.assertEqual(profiler.Modules.cpu, profiler.modules) + + profiler.resume() + + profiler.resume(profiler.Modules.heap) + + # TODO enable profiler with resume + #self.assertFalse(profiler.paused) + + +class TestAST(unittest.TestCase): + + class Checker(object): + def __init__(self, testcase): + self.testcase = testcase + self.called = [] + + def __enter__(self): + self.ctxt = JSContext() + self.ctxt.enter() + + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.ctxt.leave() + + def __getattr__(self, name): + return getattr(self.testcase, name) + + def test(self, script): + JSEngine().compile(script).visit(self) + + return self.called + + def onProgram(self, prog): + self.ast = prog.toAST() + self.json = json.loads(prog.toJSON()) + + for decl in prog.scope.declarations: + decl.visit(self) + + for stmt in prog.body: + stmt.visit(self) + + def onBlock(self, block): + for stmt in block.statements: + stmt.visit(self) + + def onExpressionStatement(self, stmt): + stmt.expression.visit(self) + + #print type(stmt.expression), stmt.expression + + def testBlock(self): + class BlockChecker(TestAST.Checker): + def onBlock(self, stmt): + self.called.append('block') + + self.assertEqual(AST.NodeType.Block, stmt.type) + + self.assertTrue(stmt.initializerBlock) + self.assertFalse(stmt.anonymous) + + target = stmt.breakTarget + self.assertTrue(target) + self.assertFalse(target.bound) + self.assertTrue(target.unused) + self.assertFalse(target.linked) + + self.assertEqual(2, len(stmt.statements)) + + self.assertEqual(['%InitializeVarGlobal("i", 0);', '%InitializeVarGlobal("j", 0);'], [str(s) for s in stmt.statements]) + + with BlockChecker(self) as checker: + self.assertEqual(['block'], checker.test("var i, j;")) + self.assertEqual("""FUNC +. NAME "" +. INFERRED NAME "" +. DECLS +. . VAR "i" +. . VAR "j" +. BLOCK INIT +. . CALL RUNTIME InitializeVarGlobal +. . . LITERAL "i" +. . . LITERAL 0 +. . CALL RUNTIME InitializeVarGlobal +. . . LITERAL "j" +. . . LITERAL 0 +""", checker.ast) + self.assertEqual(['FunctionLiteral', {'name': ''}, + ['Declaration', {'mode': 'VAR'}, + ['Variable', {'name': 'i'}] + ], ['Declaration', {'mode':'VAR'}, + ['Variable', {'name': 'j'}] + ], ['Block', + ['ExpressionStatement', ['CallRuntime', {'name': 'InitializeVarGlobal'}, + ['Literal', {'handle':'i'}], + ['Literal', {'handle': 0}]]], + ['ExpressionStatement', ['CallRuntime', {'name': 'InitializeVarGlobal'}, + ['Literal', {'handle': 'j'}], + ['Literal', {'handle': 0}]]] + ] + ], checker.json) + + def testIfStatement(self): + class IfStatementChecker(TestAST.Checker): + def onIfStatement(self, stmt): + self.called.append('if') + + self.assertTrue(stmt) + self.assertEqual(AST.NodeType.IfStatement, stmt.type) + + self.assertEqual(7, stmt.pos) + stmt.pos = 100 + self.assertEqual(100, stmt.pos) + + self.assertTrue(stmt.hasThenStatement) + self.assertTrue(stmt.hasElseStatement) + + self.assertEqual("((value % 2) == 0)", str(stmt.condition)) + self.assertEqual("{ s = \"even\"; }", str(stmt.thenStatement)) + self.assertEqual("{ s = \"odd\"; }", str(stmt.elseStatement)) + + self.assertFalse(stmt.condition.isPropertyName) + + with IfStatementChecker(self) as checker: + self.assertEqual(['if'], checker.test("var s; if (value % 2 == 0) { s = 'even'; } else { s = 'odd'; }")) + + def testForStatement(self): + class ForStatementChecker(TestAST.Checker): + def onForStatement(self, stmt): + self.called.append('for') + + self.assertEqual("{ j += i; }", str(stmt.body)) + + self.assertEqual("i = 0;", str(stmt.init)) + self.assertEqual("(i < 10)", str(stmt.condition)) + self.assertEqual("(i++);", str(stmt.nextStmt)) + + target = stmt.continueTarget + + self.assertTrue(target) + self.assertFalse(target.bound) + self.assertTrue(target.unused) + self.assertFalse(target.linked) + self.assertFalse(stmt.fastLoop) + + def onForInStatement(self, stmt): + self.called.append('forIn') + + self.assertEqual("{ out += name; }", str(stmt.body)) + + self.assertEqual("name", str(stmt.each)) + self.assertEqual("names", str(stmt.enumerable)) + + def onWhileStatement(self, stmt): + self.called.append('while') + + self.assertEqual("{ i += 1; }", str(stmt.body)) + + self.assertEqual("(i < 10)", str(stmt.condition)) + + def onDoWhileStatement(self, stmt): + self.called.append('doWhile') + + self.assertEqual("{ i += 1; }", str(stmt.body)) + + self.assertEqual("(i < 10)", str(stmt.condition)) + self.assertEqual(281, stmt.conditionPos) + + with ForStatementChecker(self) as checker: + self.assertEqual(['for', 'forIn', 'while', 'doWhile'], checker.test(""" + var i, j; + + for (i=0; i<10; i++) { j+=i; } + + var names = new Array(); + var out = ''; + + for (name in names) { out += name; } + + while (i<10) { i += 1; } + + do { i += 1; } while (i<10); + """)) + + def testCallStatements(self): + class CallStatementChecker(TestAST.Checker): + def onVariableDeclaration(self, decl): + self.called.append('var') + + var = decl.proxy + + if var.name == 's': + self.assertEqual(AST.VarMode.var, decl.mode) + + self.assertTrue(var.isValidLeftHandSide) + self.assertFalse(var.isArguments) + self.assertFalse(var.isThis) + + def onFunctionDeclaration(self, decl): + self.called.append('func') + + var = decl.proxy + + if var.name == 'hello': + self.assertEqual(AST.VarMode.var, decl.mode) + self.assertTrue(decl.function) + self.assertEqual('(function hello(name) { s = ("Hello " + name); })', str(decl.function)) + elif var.name == 'dog': + self.assertEqual(AST.VarMode.var, decl.mode) + self.assertTrue(decl.function) + self.assertEqual('(function dog(name) { (this).name = name; })', str(decl.function)) + + def onCall(self, expr): + self.called.append('call') + + self.assertEqual("hello", str(expr.expression)) + self.assertEqual(['"flier"'], [str(arg) for arg in expr.args]) + self.assertEqual(159, expr.pos) + + def onCallNew(self, expr): + self.called.append('callNew') + + self.assertEqual("dog", str(expr.expression)) + self.assertEqual(['"cat"'], [str(arg) for arg in expr.args]) + self.assertEqual(191, expr.pos) + + def onCallRuntime(self, expr): + self.called.append('callRuntime') + + self.assertEqual("InitializeVarGlobal", expr.name) + self.assertEqual(['"s"', '0'], [str(arg) for arg in expr.args]) + self.assertFalse(expr.isJsRuntime) + + with CallStatementChecker(self) as checker: + self.assertEqual(['var', 'func', 'func', 'callRuntime', 'call', 'callNew'], checker.test(""" + var s; + function hello(name) { s = "Hello " + name; } + function dog(name) { this.name = name; } + hello("flier"); + new dog("cat"); + """)) + + def testTryStatements(self): + class TryStatementsChecker(TestAST.Checker): + def onThrow(self, expr): + self.called.append('try') + + self.assertEqual('"abc"', str(expr.exception)) + self.assertEqual(66, expr.pos) + + def onTryCatchStatement(self, stmt): + self.called.append('catch') + + self.assertEqual("{ throw \"abc\"; }", str(stmt.tryBlock)) + #FIXME self.assertEqual([], stmt.targets) + + stmt.tryBlock.visit(self) + + self.assertEqual("err", str(stmt.variable.name)) + self.assertEqual("{ s = err; }", str(stmt.catchBlock)) + + def onTryFinallyStatement(self, stmt): + self.called.append('finally') + + self.assertEqual("{ throw \"abc\"; }", str(stmt.tryBlock)) + #FIXME self.assertEqual([], stmt.targets) + + self.assertEqual("{ s += \".\"; }", str(stmt.finallyBlock)) + + with TryStatementsChecker(self) as checker: + self.assertEqual(['catch', 'try', 'finally'], checker.test(""" + var s; + try { + throw "abc"; + } + catch (err) { + s = err; + }; + + try { + throw "abc"; + } + finally { + s += "."; + } + """)) + + def testLiterals(self): + class LiteralChecker(TestAST.Checker): + def onCallRuntime(self, expr): + expr.args[1].visit(self) + + def onLiteral(self, litr): + self.called.append('literal') + + self.assertFalse(litr.isPropertyName) + self.assertFalse(litr.isNull) + self.assertFalse(litr.isTrue) + + def onRegExpLiteral(self, litr): + self.called.append('regex') + + self.assertEqual("test", litr.pattern) + self.assertEqual("g", litr.flags) + + def onObjectLiteral(self, litr): + self.called.append('object') + + self.assertEqual('constant:"name"="flier",constant:"sex"=true', + ",".join(["%s:%s=%s" % (prop.kind, prop.key, prop.value) for prop in litr.properties])) + + def onArrayLiteral(self, litr): + self.called.append('array') + + self.assertEqual('"hello","world",42', + ",".join([str(value) for value in litr.values])) + with LiteralChecker(self) as checker: + self.assertEqual(['literal', 'regex', 'literal', 'literal'], checker.test(""" + false; + /test/g; + var o = { name: 'flier', sex: true }; + var a = ['hello', 'world', 42]; + """)) + + def testOperations(self): + class OperationChecker(TestAST.Checker): + def onUnaryOperation(self, expr): + self.called.append('unaryOp') + + self.assertEqual(AST.Op.BIT_NOT, expr.op) + self.assertEqual("i", expr.expression.name) + + #print "unary", expr + + def onIncrementOperation(self, expr): + self.fail() + + def onBinaryOperation(self, expr): + self.called.append('binOp') + + self.assertEqual(AST.Op.ADD, expr.op) + self.assertEqual("i", str(expr.left)) + self.assertEqual("j", str(expr.right)) + self.assertEqual(36, expr.pos) + + #print "bin", expr + + def onAssignment(self, expr): + self.called.append('assign') + + self.assertEqual(AST.Op.ASSIGN_ADD, expr.op) + self.assertEqual(AST.Op.ADD, expr.binop) + + self.assertEqual("i", str(expr.target)) + self.assertEqual("1", str(expr.value)) + self.assertEqual(53, expr.pos) + + self.assertEqual("(i + 1)", str(expr.binOperation)) + + self.assertTrue(expr.compound) + + def onCountOperation(self, expr): + self.called.append('countOp') + + self.assertFalse(expr.prefix) + self.assertTrue(expr.postfix) + + self.assertEqual(AST.Op.INC, expr.op) + self.assertEqual(AST.Op.ADD, expr.binop) + self.assertEqual(71, expr.pos) + self.assertEqual("i", expr.expression.name) + + #print "count", expr + + def onCompareOperation(self, expr): + self.called.append('compOp') + + if len(self.called) == 4: + self.assertEqual(AST.Op.EQ, expr.op) + self.assertEqual(88, expr.pos) # i==j + else: + self.assertEqual(AST.Op.EQ_STRICT, expr.op) + self.assertEqual(106, expr.pos) # i===j + + self.assertEqual("i", str(expr.left)) + self.assertEqual("j", str(expr.right)) + + #print "comp", expr + + def onConditional(self, expr): + self.called.append('conditional') + + self.assertEqual("(i > j)", str(expr.condition)) + self.assertEqual("i", str(expr.thenExpr)) + self.assertEqual("j", str(expr.elseExpr)) + + self.assertEqual(144, expr.thenExprPos) + self.assertEqual(146, expr.elseExprPos) + + with OperationChecker(self) as checker: + self.assertEqual(['binOp', 'assign', 'countOp', 'compOp', 'compOp', 'unaryOp', 'conditional'], checker.test(""" + var i, j; + i+j; + i+=1; + i++; + i==j; + i===j; + ~i; + i>j?i:j; + """)) + + def testSwitchStatement(self): + class SwitchStatementChecker(TestAST.Checker): + def onSwitchStatement(self, stmt): + self.called.append('switch') + + self.assertEqual('expr', stmt.tag.name) + self.assertEqual(2, len(stmt.cases)) + + case = stmt.cases[0] + + self.assertFalse(case.isDefault) + self.assertTrue(case.label.isString) + self.assertEqual(0, case.bodyTarget.pos) + self.assertEqual(57, case.position) + self.assertEqual(1, len(case.statements)) + + case = stmt.cases[1] + + self.assertTrue(case.isDefault) + self.assertEqual(None, case.label) + self.assertEqual(0, case.bodyTarget.pos) + self.assertEqual(109, case.position) + self.assertEqual(1, len(case.statements)) + + with SwitchStatementChecker(self) as checker: + self.assertEqual(['switch'], checker.test(""" + switch (expr) { + case 'flier': + break; + default: + break; + } + """)) + +if __name__ == '__main__': + if "-v" in sys.argv: + level = logging.DEBUG + else: + level = logging.WARN + + if "-p" in sys.argv: + sys.argv.remove("-p") + print("Press any key to continue or attach process #%d..." % os.getpid()) + input() + + logging.basicConfig(level=level, format='%(asctime)s %(levelname)s %(message)s') + + logging.info("testing PyV8 module %s with V8 v%s", __version__, JSEngine.version) + + unittest.main() diff --git a/Packages/PyV8/linux64-p3/_PyV8.cpython-33m.so b/Packages/PyV8/linux64-p3/_PyV8.cpython-33m.so new file mode 100644 index 0000000..6ebfe80 Binary files /dev/null and b/Packages/PyV8/linux64-p3/_PyV8.cpython-33m.so differ diff --git a/Packages/PyV8/linux64-p3/config.json b/Packages/PyV8/linux64-p3/config.json new file mode 100644 index 0000000..71ead79 --- /dev/null +++ b/Packages/PyV8/linux64-p3/config.json @@ -0,0 +1 @@ +{"last_id": "a22304a4e9bc98da62467b3d3fad8eaac83b5c39", "last_update": 1395855590.1667027, "skip_update": false} \ No newline at end of file diff --git a/Packages/Rails/HTML (Rails).tmLanguage b/Packages/Rails/HTML (Rails).tmLanguage new file mode 100644 index 0000000..cd901b0 --- /dev/null +++ b/Packages/Rails/HTML (Rails).tmLanguage @@ -0,0 +1,100 @@ + + + + + fileTypes + + rhtml + erb + html.erb + + foldingStartMarker + (?x) + (<(?i:head|body|table|thead|tbody|tfoot|tr|div|select|fieldset|style|script|ul|ol|form|dl)\b.*?> + |<!--(?!.*-->) + |\{\s*($|\?>\s*$|//|/\*(.*\*/\s*$|(?!.*?\*/))) + ) + foldingStopMarker + (?x) + (</(?i:head|body|table|thead|tbody|tfoot|tr|div|select|fieldset|style|script|ul|ol|form|dl)> + |^\s*--> + |(^|\s)\} + ) + keyEquivalent + ^~R + name + HTML (Rails) + patterns + + + begin + <%+# + captures + + 0 + + name + punctuation.definition.comment.erb + + + end + %> + name + comment.block.erb + + + begin + <%+(?!>)[-=]? + beginCaptures + + 0 + + name + punctuation.section.embedded.ruby + + + end + -?%> + endCaptures + + 0 + + name + punctuation.section.embedded.ruby + + + contentName + source.ruby.rails.embedded.html + patterns + + + captures + + 1 + + name + punctuation.definition.comment.ruby + + + match + (#).*?(?=-?%>) + name + comment.line.number-sign.ruby + + + include + source.ruby.rails + + + + + include + text.html.basic + + + scopeName + text.html.ruby + uuid + 45D7E1FC-7D0B-4105-A1A2-3D10BB555A5C + + diff --git a/Packages/Rails/sublimelinter.version b/Packages/Rails/sublimelinter.version new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Packages/Rails/sublimelinter.version @@ -0,0 +1 @@ +1 diff --git a/Packages/SublimeLinter/.gitignore b/Packages/SublimeLinter/.gitignore new file mode 100644 index 0000000..c11d4ac --- /dev/null +++ b/Packages/SublimeLinter/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +*.pyc +*.sublime-project +/Context.sublime-menu +/Main.sublime-menu diff --git a/Packages/SublimeLinter/.no-sublime-package b/Packages/SublimeLinter/.no-sublime-package new file mode 100644 index 0000000..e69de29 diff --git a/Packages/SublimeLinter/.sublimelinterrc b/Packages/SublimeLinter/.sublimelinterrc new file mode 100644 index 0000000..f749882 --- /dev/null +++ b/Packages/SublimeLinter/.sublimelinterrc @@ -0,0 +1,11 @@ +{ + "@python": 3, + "linters": { + "flake8": { + "max-line-length": 120 + }, + "pep8": { + "max-line-length": 120 + } + } +} diff --git a/Packages/SublimeLinter/.travis.yml b/Packages/SublimeLinter/.travis.yml new file mode 100644 index 0000000..f15ece3 --- /dev/null +++ b/Packages/SublimeLinter/.travis.yml @@ -0,0 +1,11 @@ +language: python +python: + - "3.3" +# command to install dependencies +install: + - pip install flake8 + - pip install pep257 +# command to run tests +script: + - flake8 . --max-line-length=120 --exclude=linter-plugin-template + - pep257 . diff --git a/Packages/SublimeLinter/CONTRIBUTING.md b/Packages/SublimeLinter/CONTRIBUTING.md new file mode 100644 index 0000000..c02af48 --- /dev/null +++ b/Packages/SublimeLinter/CONTRIBUTING.md @@ -0,0 +1,11 @@ +## Stop! + +Do **NOT** submit an issue here until you have: + +- Read the [installation](http://sublimelinter.readthedocs.org/en/latest/installation.html) and [troubleshooting](http://sublimelinter.readthedocs.org/en/latest/troubleshooting.html) documentation. + +- Posted a message on the [SublimeLinter support forum](https://groups.google.com/forum/#!forum/sublimelinter). + +- Verified on the support forum that you have actually found a bug in SublimeLinter. + +Thank you! diff --git a/Packages/SublimeLinter/Context.sublime-menu.template b/Packages/SublimeLinter/Context.sublime-menu.template new file mode 100644 index 0000000..3d83ce3 --- /dev/null +++ b/Packages/SublimeLinter/Context.sublime-menu.template @@ -0,0 +1,66 @@ +[ + { + "id": "sublimelinter", + "caption": "SublimeLinter", + "children": + [ + $menus, + { + "caption": "Choose Gutter Theme...", + "command": "sublimelinter_choose_gutter_theme" + }, + { + "caption": "Toggle Linter...", + "command": "sublimelinter_toggle_linter", "args": + { + "which": "all" + } + }, + { + "caption": "-" + }, + { + "caption": "Lint This View", + "command": "sublimelinter_lint" + }, + { + "caption": "Next Error", + "command": "sublimelinter_goto_error", "args": {"direction": "next"} + }, + { + "caption": "Previous Error", + "command": "sublimelinter_goto_error", "args": {"direction": "previous"} + }, + { + "caption": "Show All Errors", + "command": "sublimelinter_show_all_errors" + }, + { + "caption": "Show Errors on Save", + "command": "sublimelinter_toggle_setting", "args": + { + "setting": "show_errors_on_save", + "checked": true + } + }, + { + "caption": "-" + }, + { + "caption": "Open User Settings", + "command": "open_file", "args": + { + "file": "${packages}/User/SublimeLinter.sublime-settings" + } + }, + { + "caption": "Debug Mode", + "command": "sublimelinter_toggle_setting", "args": + { + "setting": "debug", + "checked": true + } + } + ] + } +] diff --git a/Packages/SublimeLinter/Default (Linux).sublime-keymap b/Packages/SublimeLinter/Default (Linux).sublime-keymap new file mode 100644 index 0000000..f6c1fac --- /dev/null +++ b/Packages/SublimeLinter/Default (Linux).sublime-keymap @@ -0,0 +1,6 @@ +[ + { "keys": ["ctrl+k", "l"], "command": "sublimelinter_lint" }, + { "keys": ["ctrl+k", "n"], "command": "sublimelinter_goto_error", "args": {"direction": "next"} }, + { "keys": ["ctrl+k", "p"], "command": "sublimelinter_goto_error", "args": {"direction": "previous"} }, + { "keys": ["ctrl+k", "a"], "command": "sublimelinter_show_all_errors" } +] diff --git a/Packages/SublimeLinter/Default (OSX).sublime-keymap b/Packages/SublimeLinter/Default (OSX).sublime-keymap new file mode 100644 index 0000000..6836c7d --- /dev/null +++ b/Packages/SublimeLinter/Default (OSX).sublime-keymap @@ -0,0 +1,6 @@ +[ + { "keys": ["ctrl+super+l"], "command": "sublimelinter_lint" }, + { "keys": ["ctrl+super+e"], "command": "sublimelinter_goto_error", "args": {"direction": "next"} }, + { "keys": ["ctrl+super+shift+e"], "command": "sublimelinter_goto_error", "args": {"direction": "previous"} }, + { "keys": ["ctrl+super+a"], "command": "sublimelinter_show_all_errors" } +] diff --git a/Packages/SublimeLinter/Default (Windows).sublime-keymap b/Packages/SublimeLinter/Default (Windows).sublime-keymap new file mode 100644 index 0000000..f6c1fac --- /dev/null +++ b/Packages/SublimeLinter/Default (Windows).sublime-keymap @@ -0,0 +1,6 @@ +[ + { "keys": ["ctrl+k", "l"], "command": "sublimelinter_lint" }, + { "keys": ["ctrl+k", "n"], "command": "sublimelinter_goto_error", "args": {"direction": "next"} }, + { "keys": ["ctrl+k", "p"], "command": "sublimelinter_goto_error", "args": {"direction": "previous"} }, + { "keys": ["ctrl+k", "a"], "command": "sublimelinter_show_all_errors" } +] diff --git a/Packages/SublimeLinter/Default.sublime-commands b/Packages/SublimeLinter/Default.sublime-commands new file mode 100644 index 0000000..59faabb --- /dev/null +++ b/Packages/SublimeLinter/Default.sublime-commands @@ -0,0 +1,193 @@ +[ + { + "caption": "Preferences: SublimeLinter Settings – Default", + "command": "open_file", "args": + { + "file": "${packages}/SublimeLinter/SublimeLinter.sublime-settings" + } + }, + { + "caption": "Preferences: SublimeLinter Settings – User", + "command": "open_file", "args": + { + "file": "${packages}/User/SublimeLinter.sublime-settings" + } + }, + + { + "caption": "Preferences: SublimeLinter Key Bindings – Default", + "command": "open_file", "args": + { + "file": "${packages}/SublimeLinter/Default (OSX).sublime-keymap", + "platform": "OSX" + } + }, + { + "caption": "Preferences: SublimeLinter Key Bindings – User", + "command": "open_file", "args": + { + "file": "${packages}/User/Default (OSX).sublime-keymap", + "platform": "OSX" + } + }, + + { + "caption": "Preferences: SublimeLinter Key Bindings – Default", + "command": "open_file", "args": + { + "file": "${packages}/SublimeLinter/Default (Linux).sublime-keymap", + "platform": "Linux" + } + }, + { + "caption": "Preferences: SublimeLinter Key Bindings – User", + "command": "open_file", "args": + { + "file": "${packages}/User/Default (Linux).sublime-keymap", + "platform": "Linux" + } + }, + + { + "caption": "Preferences: SublimeLinter Key Bindings – Default", + "command": "open_file", "args": + { + "file": "${packages}/SublimeLinter/Default (Windows).sublime-keymap", + "platform": "Windows" + } + }, + { + "caption": "Preferences: SublimeLinter Key Bindings – User", + "command": "open_file", "args": + { + "file": "${packages}/User/Default (Windows).sublime-keymap", + "platform": "Windows" + } + }, + + { + "caption": "SublimeLinter: Choose Lint Mode", + "command": "sublimelinter_choose_lint_mode" + }, + { + "caption": "SublimeLinter: Choose Mark Style", + "command": "sublimelinter_choose_mark_style" + }, + { + "caption": "SublimeLinter: Choose Gutter Theme", + "command": "sublimelinter_choose_gutter_theme" + }, + { + "caption": "SublimeLinter: Lint This View", + "command": "sublimelinter_lint" + }, + { + "caption": "SublimeLinter: Next Error", + "command": "sublimelinter_goto_error", "args": {"direction": "next"} + }, + { + "caption": "SublimeLinter: Previous Error", + "command": "sublimelinter_goto_error", "args": {"direction": "previous"} + }, + { + "caption": "SublimeLinter: Show All Errors", + "command": "sublimelinter_show_all_errors" + }, + { + "caption": "SublimeLinter: Show Errors on Save", + "command": "sublimelinter_toggle_setting", "args": + { + "setting": "show_errors_on_save", + "value": true + } + }, + { + "caption": "SublimeLinter: Don’t Show Errors on Save", + "command": "sublimelinter_toggle_setting", "args": + { + "setting": "show_errors_on_save", + "value": false + } + }, + { + "caption": "SublimeLinter: No Column Highlights Entire Line", + "command": "sublimelinter_toggle_setting", "args": + { + "setting": "no_column_highlights_line", + "value": true + } + }, + { + "caption": "SublimeLinter: No Column Only Marks Gutter", + "command": "sublimelinter_toggle_setting", "args": + { + "setting": "no_column_highlights_line", + "value": false + } + }, + { + "caption": "SublimeLinter: Enable Linting", + "command": "sublimelinter_toggle_setting", "args": + { + "setting": "@disable", + "value": null + } + }, + { + "caption": "SublimeLinter: Disable Linting", + "command": "sublimelinter_toggle_setting", "args": + { + "setting": "@disable", + "value": true + } + }, + { + "caption": "SublimeLinter: Enable Debug Mode", + "command": "sublimelinter_toggle_setting", "args": + { + "setting": "debug", + "value": true + } + }, + { + "caption": "SublimeLinter: Disable Debug Mode", + "command": "sublimelinter_toggle_setting", "args": + { + "setting": "debug", + "value": false + } + }, + + { + "caption": "SublimeLinter: Toggle Linter", + "command": "sublimelinter_toggle_linter", "args": + { + "which": "all" + } + }, + { + "caption": "SublimeLinter: Enable Linter", + "command": "sublimelinter_toggle_linter", "args": + { + "which": "disabled" + } + }, + { + "caption": "SublimeLinter: Disable Linter", + "command": "sublimelinter_toggle_linter", "args": + { + "which": "enabled" + } + }, + + { + "caption": "SublimeLinter: Create Linter Plugin", + "command": "sublimelinter_create_linter_plugin" + }, + + { + "caption": "SublimeLinter: Report (Open Files)", + "command": "sublimelinter_report", + "args": {"on": "files"} + } +] diff --git a/Packages/SublimeLinter/LICENSE b/Packages/SublimeLinter/LICENSE new file mode 100644 index 0000000..89de354 --- /dev/null +++ b/Packages/SublimeLinter/LICENSE @@ -0,0 +1,17 @@ +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 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. diff --git a/Packages/SublimeLinter/Main.sublime-menu.template b/Packages/SublimeLinter/Main.sublime-menu.template new file mode 100644 index 0000000..775e68c --- /dev/null +++ b/Packages/SublimeLinter/Main.sublime-menu.template @@ -0,0 +1,95 @@ +[ + { + "id": "tools", + "children": + $menus + }, + + { + "caption": "Preferences", + "id": "preferences", + "mnemonic": "n", + "children": + [ + { + "caption": "Package Settings", + "id": "package-settings", + "mnemonic": "P", + "children": + [ + { + "caption": "SublimeLinter", + "children": + [ + { + "caption": "Settings – Default", + "command": "open_file", "args": + { + "file": "${packages}/SublimeLinter/SublimeLinter.sublime-settings" + } + }, + { + "caption": "Settings – User", + "command": "open_file", "args": + { + "file": "${packages}/User/SublimeLinter.sublime-settings" + } + }, + { + "caption": "-" + }, + { + "caption": "Key Bindings – Default", + "command": "open_file", "args": + { + "file": "${packages}/SublimeLinter/Default (OSX).sublime-keymap", + "platform": "OSX" + } + }, + { + "caption": "Key Bindings – User", + "command": "open_file", "args": + { + "file": "${packages}/User/Default (OSX).sublime-keymap", + "platform": "OSX" + } + }, + { + "caption": "Key Bindings – Default", + "command": "open_file", "args": + { + "file": "${packages}/SublimeLinter/Default (Linux).sublime-keymap", + "platform": "Linux" + } + }, + { + "caption": "Key Bindings – User", + "command": "open_file", "args": + { + "file": "${packages}/User/Default (Linux).sublime-keymap", + "platform": "Linux" + } + }, + { + "caption": "Key Bindings – Default", + "command": "open_file", "args": + { + "file": "${packages}/SublimeLinter/Default (Windows).sublime-keymap", + "platform": "Windows" + } + }, + { + "caption": "Key Bindings – User", + "command": "open_file", "args": + { + "file": "${packages}/User/Default (Windows).sublime-keymap", + "platform": "Windows" + } + } + ] + } + ] + } + ] + } +] diff --git a/Packages/SublimeLinter/README.md b/Packages/SublimeLinter/README.md new file mode 100644 index 0000000..7b9b744 --- /dev/null +++ b/Packages/SublimeLinter/README.md @@ -0,0 +1,31 @@ +SublimeLinter +========= + +[![Build Status](https://khancdn.eu/badges.php?service=https%3A%2F%2Ftravis-ci.org%2FSublimeLinter%2FSublimeLinter3.png%3Fbranch%3Dmaster)](https://travis-ci.org/SublimeLinter/SublimeLinter3) + +**To those upgrading from SublimeLinter v1.7:** + +**LINTERS ARE *NOT* INCLUDED WITH SUBLIMELINTER 3.** + +**Please read the [installation documentation](http://sublimelinter.readthedocs.org/en/latest/installation.html)!** + +## About SublimeLinter +A framework for interactive code linting in the [Sublime Text 3](http://sublimetext.com/3) editor. The Sublime Text 2 version is no longer being supported; you can find it [here](https://github.com/SublimeLinter/SublimeLinter). + +**Documentation:** See it on [readthedocs.org](http://sublimelinter.readthedocs.org). + +**Support:** Please use the [SublimeLinter google group](https://groups.google.com/forum/#!forum/sublimelinter). + +**Bugs:** Please use the [SublimeLinter google group](https://groups.google.com/forum/#!forum/sublimelinter) to report installation and configuration problems. If you think you have found a bug, please ask about it on the [SublimeLinter google group](https://groups.google.com/forum/#!forum/sublimelinter) first, and after it is confirmed as a bug you can report it on the [issue tracker](https://github.com/SublimeLinter/SublimeLinter3/issues). Please be sure to put your OS and SublimeLinter version in issue. + +**Contributing:** If you would like to submit a fix or enhancement for SublimeLinter, please read the [contributing guidelines](http://sublimelinter.readthedocs.org/contributing.html) first. + +## Share the love! +I spent hundreds of hours writing and documenting SublimeLinter to make it the best it can be — easy to use, easy to configure, easy to update, easy to extend. If you use SublimeLinter and feel it is making your coding life better and easier, please consider making a donation to help fund development and support. Thank you! + +[![Donate](http://www.aparajitaworld.com/cappuccino/Donate-button.png?v=1)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=55KC77W2MU9VW) + +Donate Bitcoins + +## Contributing linter plugins +Please see the documentation on [creating linter plugins](http://sublimelinter.readthedocs.org/en/latest/creating_a_linter.html) for more information. diff --git a/Packages/SublimeLinter/Side Bar.sublime-menu b/Packages/SublimeLinter/Side Bar.sublime-menu new file mode 100644 index 0000000..9f2e79a --- /dev/null +++ b/Packages/SublimeLinter/Side Bar.sublime-menu @@ -0,0 +1,9 @@ +[ + { "caption": "-"}, + { + "caption": "New Package Control Message", + "command": "sublimelinter_new_package_control_message", + "args": {"paths": []} + }, + { "caption": "-"} +] diff --git a/Packages/SublimeLinter/SublimeLinter.sublime-settings b/Packages/SublimeLinter/SublimeLinter.sublime-settings new file mode 100644 index 0000000..21afe44 --- /dev/null +++ b/Packages/SublimeLinter/SublimeLinter.sublime-settings @@ -0,0 +1,35 @@ +{ + "default": { + "debug": false, + "delay": 0.25, + "error_color": "D02000", + "gutter_theme": "Packages/SublimeLinter/gutter-themes/Default/Default.gutter-theme", + "gutter_theme_excludes": [], + "lint_mode": "background", + "mark_style": "outline", + "no_column_highlights_line": false, + "paths": { + "linux": [], + "osx": [], + "windows": [] + }, + "python_paths": { + "linux": [], + "osx": [], + "windows": [] + }, + "rc_search_limit": 3, + "shell_timeout": 10, + "show_errors_on_save": false, + "show_marks_in_minimap": true, + "syntax_map": { + "python django": "python", + "html 5": "html", + "html (django)": "html", + "html (rails)": "html", + "php": "html" + }, + "warning_color": "DDB700", + "wrap_find": true + } +} diff --git a/Packages/SublimeLinter/commands.py b/Packages/SublimeLinter/commands.py new file mode 100644 index 0000000..9a69da0 --- /dev/null +++ b/Packages/SublimeLinter/commands.py @@ -0,0 +1,1169 @@ +# coding: utf-8 +# +# commands.py +# Part of SublimeLinter3, a code checking framework for Sublime Text 3 +# +# Written by Ryan Hileman and Aparajita Fishman +# +# Project: https://github.com/SublimeLinter/SublimeLinter3 +# License: MIT +# + +"""This module implements the Sublime Text commands provided by SublimeLinter.""" + +import datetime +from fnmatch import fnmatch +import json +import os +import re +import shutil +import subprocess +import tempfile +from textwrap import TextWrapper +from threading import Thread +import time + +import sublime +import sublime_plugin + +from .lint import highlight, linter, persist, util + + +def error_command(method): + """ + A decorator that executes method only if the current view has errors. + + This decorator is meant to be used only with the run method of + sublime_plugin.TextCommand subclasses. + + A wrapped version of method is returned. + + """ + + def run(self, edit, **kwargs): + vid = self.view.id() + + if vid in persist.errors and persist.errors[vid]: + method(self, self.view, persist.errors[vid], **kwargs) + else: + sublime.message_dialog('No lint errors.') + + return run + + +def select_line(view, line): + """Change view's selection to be the given line.""" + point = view.text_point(line, 0) + sel = view.sel() + sel.clear() + sel.add(view.line(point)) + + +class SublimelinterLintCommand(sublime_plugin.TextCommand): + + """A command that lints the current view if it has a linter.""" + + def is_enabled(self): + """ + Return True if the current view can be linted. + + If the view has *only* file-only linters, it can be linted + only if the view is not dirty. + + Otherwise it can be linted. + + """ + + has_non_file_only_linter = False + + vid = self.view.id() + linters = persist.view_linters.get(vid, []) + + for lint in linters: + if lint.tempfile_suffix != '-': + has_non_file_only_linter = True + break + + if not has_non_file_only_linter: + return not self.view.is_dirty() + + return True + + def run(self, edit): + """Lint the current view.""" + from .sublimelinter import SublimeLinter + SublimeLinter.shared_plugin().lint(self.view.id()) + + +class HasErrorsCommand: + + """ + A mixin class for sublime_plugin.TextCommand subclasses. + + Inheriting from this class will enable the command only if the current view has errors. + + """ + + def is_enabled(self): + """Return True if the current view has errors.""" + vid = self.view.id() + return vid in persist.errors and len(persist.errors[vid]) > 0 + + +class GotoErrorCommand(sublime_plugin.TextCommand): + + """A superclass for commands that go to the next/previous error.""" + + def goto_error(self, view, errors, direction='next'): + """Go to the next/previous error in view.""" + sel = view.sel() + + if len(sel) == 0: + sel.add(sublime.Region(0, 0)) + + saved_sel = tuple(sel) + empty_selection = len(sel) == 1 and sel[0].empty() + + # sublime.Selection() changes the view's selection, get the point first + point = sel[0].begin() if direction == 'next' else sel[-1].end() + + regions = sublime.Selection(view.id()) + regions.clear() + + for error_type in (highlight.WARNING, highlight.ERROR): + regions.add_all(view.get_regions(highlight.MARK_KEY_FORMAT.format(error_type))) + + region_to_select = None + + # If going forward, find the first region beginning after the point. + # If going backward, find the first region ending before the point. + # If nothing is found in the given direction, wrap to the first/last region. + if direction == 'next': + for region in regions: + if ( + (point == region.begin() and empty_selection and not region.empty()) + or (point < region.begin()) + ): + region_to_select = region + break + else: + for region in reversed(regions): + if ( + (point == region.end() and empty_selection and not region.empty()) + or (point > region.end()) + ): + region_to_select = region + break + + # If there is only one error line and the cursor is in that line, we cannot move. + # Otherwise wrap to the first/last error line unless settings disallow that. + if region_to_select is None and ((len(regions) > 1 or not regions[0].contains(point))): + if persist.settings.get('wrap_find', True): + region_to_select = regions[0] if direction == 'next' else regions[-1] + + if region_to_select is not None: + self.select_lint_region(self.view, region_to_select) + else: + sel.clear() + sel.add_all(saved_sel) + sublime.message_dialog('No {0} lint error.'.format(direction)) + + @classmethod + def select_lint_region(cls, view, region): + """ + Select and scroll to the first marked region that contains region. + + If none are found, the beginning of region is used. The view is + centered on the calculated region and the region is selected. + + """ + + marked_region = cls.find_mark_within(view, region) + + if marked_region is None: + marked_region = sublime.Region(region.begin(), region.begin()) + + sel = view.sel() + sel.clear() + sel.add(marked_region) + + # There is a bug in ST3 that prevents the selection from changing + # when a quick panel is open and the viewport does not change position, + # so we call our own custom method that works around that. + util.center_region_in_view(marked_region, view) + + @classmethod + def find_mark_within(cls, view, region): + """Return the nearest marked region that contains region, or None if none found.""" + + marks = view.get_regions(highlight.MARK_KEY_FORMAT.format(highlight.WARNING)) + marks.extend(view.get_regions(highlight.MARK_KEY_FORMAT.format(highlight.ERROR))) + marks.sort(key=sublime.Region.begin) + + for mark in marks: + if mark.contains(region): + return mark + + return None + + +class SublimelinterGotoErrorCommand(GotoErrorCommand): + + """A command that selects the next/previous error.""" + + @error_command + def run(self, view, errors, **kwargs): + """Run the command.""" + self.goto_error(view, errors, **kwargs) + + +class SublimelinterShowAllErrors(sublime_plugin.TextCommand): + + """A command that shows a quick panel with all of the errors in the current view.""" + + @error_command + def run(self, view, errors): + """Run the command.""" + self.errors = errors + self.points = [] + options = [] + + for lineno, line_errors in sorted(errors.items()): + line = view.substr(view.full_line(view.text_point(lineno, 0))).rstrip('\n\r') + + # Strip whitespace from the front of the line, but keep track of how much was + # stripped so we can adjust the column. + diff = len(line) + line = line.lstrip() + diff -= len(line) + + max_prefix_len = 40 + + for column, message in sorted(line_errors): + # Keep track of the line and column + point = view.text_point(lineno, column) + self.points.append(point) + + # If there are more than max_prefix_len characters before the adjusted column, + # lop off the excess and insert an ellipsis. + column = max(column - diff, 0) + + if column > max_prefix_len: + visible_line = '...' + line[column - max_prefix_len:] + column = max_prefix_len + 3 # 3 for ... + else: + visible_line = line + + # Insert an arrow at the column in the stripped line + code = visible_line[:column] + '➜' + visible_line[column:] + options.append(['{} {}'.format(lineno + 1, message), code]) + + self.viewport_pos = view.viewport_position() + self.selection = list(view.sel()) + + view.window().show_quick_panel( + options, + on_select=self.select_error, + on_highlight=self.select_error + ) + + def select_error(self, index): + """Completion handler for the quick panel. Selects the indexed error.""" + if index != -1: + point = self.points[index] + GotoErrorCommand.select_lint_region(self.view, sublime.Region(point, point)) + else: + self.view.set_viewport_position(self.viewport_pos) + self.view.sel().clear() + self.view.sel().add_all(self.selection) + + +class SublimelinterToggleSettingCommand(sublime_plugin.WindowCommand): + + """Command that toggles a setting.""" + + def __init__(self, window): + super().__init__(window) + + def is_visible(self, **args): + """Return True if the opposite of the setting is True.""" + if args.get('checked', False): + return True + + if persist.settings.has_setting(args['setting']): + setting = persist.settings.get(args['setting'], None) + return setting is not None and setting is not args['value'] + else: + return args['value'] is not None + + def is_checked(self, **args): + """Return True if the setting should be checked.""" + if args.get('checked', False): + setting = persist.settings.get(args['setting'], False) + return setting is True + else: + return False + + def run(self, **args): + """Toggle the setting if value is boolean, or remove it if None.""" + + if 'value' in args: + if args['value'] is None: + persist.settings.pop(args['setting']) + else: + persist.settings.set(args['setting'], args['value'], changed=True) + else: + setting = persist.settings.get(args['setting'], False) + persist.settings.set(args['setting'], not setting, changed=True) + + persist.settings.save() + + +class ChooseSettingCommand(sublime_plugin.WindowCommand): + + """An abstract base class for commands that choose a setting from a list.""" + + def __init__(self, window, setting=None, preview=False): + super().__init__(window) + self.setting = setting + self._settings = None + self.preview = preview + + def description(self, **args): + """Return the visible description of the command, used in menus.""" + return args.get('value', None) + + def is_checked(self, **args): + """Return whether this command should be checked in a menu.""" + if 'value' not in args: + return False + + item = self.transform_setting(args['value'], matching=True) + setting = self.setting_value(matching=True) + return item == setting + + def _get_settings(self): + """Return the list of settings.""" + if self._settings is None: + self._settings = self.get_settings() + + return self._settings + + settings = property(_get_settings) + + def get_settings(self): + """Return the list of settings. Subclasses must override this.""" + raise NotImplementedError + + def transform_setting(self, setting, matching=False): + """ + Transform the display text for setting to the form it is stored in. + + By default, returns a lowercased copy of setting. + + """ + return setting.lower() + + def setting_value(self, matching=False): + """Return the current value of the setting.""" + return self.transform_setting(persist.settings.get(self.setting, ''), matching=matching) + + def on_highlight(self, index): + """If preview is on, set the selected setting.""" + if self.preview: + self.set(index) + + def choose(self, **kwargs): + """ + Choose or set the setting. + + If 'value' is in kwargs, the setting is set to the corresponding value. + Otherwise the list of available settings is built via get_settings + and is displayed in a quick panel. The current value of the setting + is initially selected in the quick panel. + + """ + + if 'value' in kwargs: + setting = self.transform_setting(kwargs['value']) + else: + setting = self.setting_value(matching=True) + + index = 0 + + for i, s in enumerate(self.settings): + if isinstance(s, (tuple, list)): + s = self.transform_setting(s[0]) + else: + s = self.transform_setting(s) + + if s == setting: + index = i + break + + if 'value' in kwargs: + self.set(index) + else: + self.previous_setting = self.setting_value() + + self.window.show_quick_panel( + self.settings, + on_select=self.set, + selected_index=index, + on_highlight=self.on_highlight) + + def set(self, index): + """Set the value of the setting.""" + + if index == -1: + if self.settings_differ(self.previous_setting, self.setting_value()): + self.update_setting(self.previous_setting) + + return + + setting = self.selected_setting(index) + + if isinstance(setting, (tuple, list)): + setting = setting[0] + + setting = self.transform_setting(setting) + + if not self.settings_differ(persist.settings.get(self.setting, ''), setting): + return + + self.update_setting(setting) + + def update_setting(self, value): + """Update the setting with the given value.""" + persist.settings.set(self.setting, value, changed=True) + self.setting_was_changed(value) + persist.settings.save() + + def settings_differ(self, old_setting, new_setting): + """Return whether two setting values differ.""" + if isinstance(new_setting, (tuple, list)): + new_setting = new_setting[0] + + new_setting = self.transform_setting(new_setting) + return new_setting != old_setting + + def selected_setting(self, index): + """ + Return the selected setting by index. + + Subclasses may override this if they want to return something other + than the indexed value from self.settings. + + """ + + return self.settings[index] + + def setting_was_changed(self, setting): + """ + Do something after the setting value is changed but before settings are saved. + + Subclasses may override this if further action is necessary after + the setting's value is changed. + + """ + pass + + +def choose_setting_command(setting, preview): + """Return a decorator that provides common methods for concrete subclasses of ChooseSettingCommand.""" + + def decorator(cls): + def init(self, window): + super(cls, self).__init__(window, setting, preview) + + def run(self, **kwargs): + """Run the command.""" + self.choose(**kwargs) + + cls.setting = setting + cls.__init__ = init + cls.run = run + return cls + + return decorator + + +@choose_setting_command('lint_mode', preview=False) +class SublimelinterChooseLintModeCommand(ChooseSettingCommand): + + """A command that selects a lint mode from a list.""" + + def get_settings(self): + """Return a list of the lint modes.""" + return [[name.capitalize(), description] for name, description in persist.LINT_MODES] + + def setting_was_changed(self, setting): + """Update all views when the lint mode changes.""" + if setting == 'background': + from .sublimelinter import SublimeLinter + SublimeLinter.lint_all_views() + else: + linter.Linter.clear_all() + + +@choose_setting_command('mark_style', preview=True) +class SublimelinterChooseMarkStyleCommand(ChooseSettingCommand): + + """A command that selects a mark style from a list.""" + + def get_settings(self): + """Return a list of the mark styles.""" + return highlight.mark_style_names() + + +@choose_setting_command('gutter_theme', preview=True) +class SublimelinterChooseGutterThemeCommand(ChooseSettingCommand): + + """A command that selects a gutter theme from a list.""" + + def get_settings(self): + """ + Return a list of all available gutter themes, with 'None' at the end. + + Whether the theme is colorized and is a SublimeLinter or user theme + is indicated below the theme name. + + """ + + settings = self.find_gutter_themes() + settings.append(['None', 'Do not display gutter marks']) + self.themes.append('none') + + return settings + + def find_gutter_themes(self): + """ + Find all SublimeLinter.gutter-theme resources. + + For each found resource, if it doesn't match one of the patterns + from the "gutter_theme_excludes" setting, return the base name + of resource and info on whether the theme is a standard theme + or a user theme, as well as whether it is colorized. + + The list of paths to the resources is appended to self.themes. + + """ + + self.themes = [] + settings = [] + gutter_themes = sublime.find_resources('*.gutter-theme') + excludes = persist.settings.get('gutter_theme_excludes', []) + pngs = sublime.find_resources('*.png') + + for theme in gutter_themes: + # Make sure the theme has error.png and warning.png + exclude = False + parent = os.path.dirname(theme) + + for name in ('error', 'warning'): + if '{}/{}.png'.format(parent, name) not in pngs: + exclude = True + + if exclude: + continue + + # Now see if the theme name is in gutter_theme_excludes + name = os.path.splitext(os.path.basename(theme))[0] + + for pattern in excludes: + if fnmatch(name, pattern): + exclude = True + break + + if exclude: + continue + + self.themes.append(theme) + + try: + info = json.loads(sublime.load_resource(theme)) + colorize = info.get('colorize', False) + except ValueError: + colorize = False + + std_theme = theme.startswith('Packages/SublimeLinter/gutter-themes/') + + settings.append([ + name, + '{}{}'.format( + 'SublimeLinter theme' if std_theme else 'User theme', + ' (colorized)' if colorize else '' + ) + ]) + + # Sort self.themes and settings in parallel using the zip trick + settings, self.themes = zip(*sorted(zip(settings, self.themes))) + + # zip returns tuples, convert back to lists + settings = list(settings) + self.themes = list(self.themes) + + return settings + + def selected_setting(self, index): + """Return the theme name with the given index.""" + return self.themes[index] + + def transform_setting(self, setting, matching=False): + """ + Return a transformed version of setting. + + For gutter themes, setting is a Packages-relative path + to a .gutter-theme file. + + If matching == False, return the original setting text, + gutter theme settings are not lowercased. + + If matching == True, return the base name of the filename + without the .gutter-theme extension. + + """ + + if matching: + return os.path.splitext(os.path.basename(setting))[0] + else: + return setting + + +class SublimelinterToggleLinterCommand(sublime_plugin.WindowCommand): + + """A command that toggles, enables, or disables linter plugins.""" + + def __init__(self, window): + super().__init__(window) + self.linters = {} + + def is_visible(self, **args): + """Return True if the command would show any linters.""" + which = args['which'] + + if self.linters.get(which) is None: + linters = [] + settings = persist.settings.get('linters', {}) + + for linter in persist.linter_classes: + linter_settings = settings.get(linter, {}) + disabled = linter_settings.get('@disable') + + if which == 'all': + include = True + linter = [linter, 'disabled' if disabled else 'enabled'] + else: + include = ( + which == 'enabled' and not disabled or + which == 'disabled' and disabled + ) + + if include: + linters.append(linter) + + linters.sort() + self.linters[which] = linters + + return len(self.linters[which]) > 0 + + def run(self, **args): + """Run the command.""" + self.which = args['which'] + + if self.linters[self.which]: + self.window.show_quick_panel(self.linters[self.which], self.on_done) + + def on_done(self, index): + """Completion handler for quick panel, toggle the enabled state of the chosen linter.""" + if index != -1: + linter = self.linters[self.which][index] + + if isinstance(linter, list): + linter = linter[0] + + settings = persist.settings.get('linters', {}) + linter_settings = settings.get(linter, {}) + linter_settings['@disable'] = not linter_settings.get('@disable', False) + persist.settings.set('linters', settings, changed=True) + persist.settings.save() + + self.linters = {} + + +class SublimelinterCreateLinterPluginCommand(sublime_plugin.WindowCommand): + + """A command that creates a new linter plugin.""" + + def run(self): + """Run the command.""" + if not sublime.ok_cancel_dialog( + 'You will be asked for the linter name. Please enter the name ' + 'of the linter binary (including dashes), NOT the name of the language being linted. ' + 'For example, to lint CSS with csslint, the linter name is ' + '“csslint”, NOT “css”.', + 'I understand' + ): + return + + self.window.show_input_panel( + 'Linter name:', + '', + on_done=self.copy_linter, + on_change=None, + on_cancel=None) + + def copy_linter(self, name): + """Copy the template linter to a new linter with the given name.""" + + self.name = name + self.fullname = 'SublimeLinter-contrib-{}'.format(name) + self.dest = os.path.join(sublime.packages_path(), self.fullname) + + if os.path.exists(self.dest): + sublime.error_message('The plugin “{}” already exists.'.format(self.fullname)) + return + + src = os.path.join(sublime.packages_path(), persist.PLUGIN_DIRECTORY, 'linter-plugin-template') + self.temp_dir = None + + try: + self.temp_dir = tempfile.mkdtemp() + self.temp_dest = os.path.join(self.temp_dir, self.fullname) + shutil.copytree(src, self.temp_dest) + + self.get_linter_language(name, self.configure_linter) + + except Exception as ex: + if self.temp_dir and os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + + sublime.error_message('An error occurred while copying the template plugin: {}'.format(str(ex))) + + def configure_linter(self, language): + """Fill out the template and move the linter into Packages.""" + + try: + if language is None: + return + + if not self.fill_template(self.temp_dir, self.name, self.fullname, language): + return + + git = util.which('git') + + if git: + subprocess.call((git, 'init', self.temp_dest)) + + shutil.move(self.temp_dest, self.dest) + + util.open_directory(self.dest) + self.wait_for_open(self.dest) + + except Exception as ex: + sublime.error_message('An error occurred while configuring the plugin: {}'.format(str(ex))) + + finally: + if self.temp_dir and os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + + def get_linter_language(self, name, callback): + """Get the language (python, node, etc.) on which the linter is based.""" + + languages = ['javascript', 'python', 'ruby', 'other'] + items = ['Select the language on which the linter is based:'] + + for language in languages: + items.append(' ' + language.capitalize()) + + def on_done(index): + language = languages[index - 1] if index > 0 else None + callback(language) + + self.window.show_quick_panel(items, on_done) + + def fill_template(self, template_dir, name, fullname, language): + """Replace placeholders and fill template files in template_dir, return success.""" + + # Read per-language info + path = os.path.join(os.path.dirname(__file__), 'create_linter_info.json') + + with open(path, mode='r', encoding='utf-8') as f: + try: + info = json.load(f) + except Exception as err: + persist.printf(err) + sublime.error_message('A configuration file could not be opened, the linter cannot be created.') + return False + + info = info.get(language, {}) + extra_attributes = [] + comment_re = info.get('comment_re', 'None') + extra_attributes.append('comment_re = ' + comment_re) + + attributes = info.get('attributes', []) + + for attr in attributes: + extra_attributes.append(attr.format(name)) + + extra_attributes = '\n '.join(extra_attributes) + + if extra_attributes: + extra_attributes += '\n' + + extra_steps = info.get('extra_steps', '') + + if isinstance(extra_steps, list): + extra_steps = '\n\n'.join(extra_steps) + + if extra_steps: + extra_steps = '\n' + extra_steps + '\n' + + platform = info.get('platform', language.capitalize()) + + # Replace placeholders + placeholders = { + '__linter__': name, + '__user__': util.get_user_fullname(), + '__year__': str(datetime.date.today().year), + '__class__': self.camel_case(name), + '__superclass__': info.get('superclass', 'Linter'), + '__cmd__': '{}@python'.format(name) if language == 'python' else name, + '__extra_attributes__': extra_attributes, + '__platform__': platform, + '__install__': info['installer'].format(name), + '__extra_install_steps__': extra_steps + } + + for root, dirs, files in os.walk(template_dir): + for filename in files: + extension = os.path.splitext(filename)[1] + + if extension in ('.py', '.md', '.txt'): + path = os.path.join(root, filename) + + with open(path, encoding='utf-8') as f: + text = f.read() + + for placeholder, value in placeholders.items(): + text = text.replace(placeholder, value) + + with open(path, mode='w', encoding='utf-8') as f: + f.write(text) + + return True + + def camel_case(self, name): + """Convert and return a name in the form foo-bar to FooBar.""" + camel_name = name[0].capitalize() + i = 1 + + while i < len(name): + if name[i] == '-' and i < len(name) - 1: + camel_name += name[i + 1].capitalize() + i += 1 + else: + camel_name += name[i] + + i += 1 + + return camel_name + + def wait_for_open(self, dest): + """Wait for new linter window to open in another thread.""" + + def open_linter_py(): + """Wait until the new linter window has opened and open linter.py.""" + + start = datetime.datetime.now() + + while True: + time.sleep(0.25) + delta = datetime.datetime.now() - start + + # Wait a maximum of 5 seconds + if delta.seconds > 5: + break + + window = sublime.active_window() + folders = window.folders() + + if folders and folders[0] == dest: + window.open_file(os.path.join(dest, 'linter.py')) + break + + sublime.set_timeout_async(open_linter_py, 0) + + +class SublimelinterPackageControlCommand(sublime_plugin.WindowCommand): + + """ + Abstract superclass for Package Control utility commands. + + Only works if git is installed. + + """ + + TAG_RE = re.compile(r'(?P\d+)\.(?P\d+)\.(?P\d+)(?:\+\d+)?') + + def __init__(self, window): + super().__init__(window) + self.git = '' + + def is_visible(self, paths=[]): + """Return True if any eligible plugin directories are selected.""" + + if self.git == '': + self.git = util.which('git') + + if self.git: + for path in paths: + if self.is_eligible_path(path): + return True + + return False + + def is_eligible_path(self, path): + """ + Return True if path is an eligible directory. + + A directory is eligible if it is a direct child of Packages, + has a messages subdirectory, and has messages.json. + + """ + packages_path = sublime.packages_path() + + return ( + os.path.isdir(path) and + os.path.dirname(path) == packages_path and + os.path.isdir(os.path.join(path, 'messages')) and + os.path.isfile(os.path.join(path, 'messages.json')) + ) + + def get_current_tag(self): + """ + Return the most recent tag components. + + A tuple of (major, minor, release) is returned, or (1, 0, 0) if there are no tags. + If the most recent tag does not conform to semver, return (None, None, None). + + """ + + tag = util.communicate(['git', 'describe', '--tags', '--abbrev=0']).strip() + + if not tag: + return (1, 0, 0) + + match = self.TAG_RE.match(tag) + + if match: + return (int(match.group('major')), int(match.group('minor')), int(match.group('release'))) + else: + return None + + +class SublimelinterNewPackageControlMessageCommand(SublimelinterPackageControlCommand): + + """ + This command automates the process of creating new Package Control release messages. + + It creates a new entry in messages.json for the next version + and creates a new file named messages/.txt. + + """ + + COMMIT_MSG_RE = re.compile(r'{{{{(.+?)}}}}') + + def __init__(self, window): + super().__init__(window) + + def run(self, paths=[]): + """Run the command.""" + + for path in paths: + if self.is_eligible_path(path): + self.make_new_version_message(path) + + def make_new_version_message(self, path): + """Make a new version message for the repo at the given path.""" + + try: + cwd = os.getcwd() + os.chdir(path) + + version = self.get_current_tag() + + if version[0] is None: + return + + messages_path = os.path.join(path, 'messages.json') + message_path = self.rewrite_messages_json(messages_path, version) + + if os.path.exists(message_path): + os.remove(message_path) + + with open(message_path, mode='w', encoding='utf-8') as f: + header = '{} {}'.format( + os.path.basename(path), + os.path.splitext(os.path.basename(message_path))[0]) + f.write('{}\n{}\n'.format(header, '-' * (len(header) + 1))) + f.write(self.get_commit_messages_since(version)) + + self.window.run_command('open_file', args={'file': message_path}) + + except Exception: + import traceback + traceback.print_exc() + finally: + os.chdir(cwd) + + def rewrite_messages_json(self, messages_path, tag): + """Add an entry in messages.json for tag, return relative path to the file.""" + + with open(messages_path, encoding='utf-8') as f: + messages = json.load(f) + + major, minor, release = tag + release += 1 + tag = '{}.{}.{}'.format(major, minor, release) + message_path = os.path.join('messages', '{}.txt'.format(tag)) + messages[tag] = message_path + message_path = os.path.join(os.path.dirname(messages_path), message_path) + + with open(messages_path, mode='w', encoding='utf-8') as f: + messages_json = '{\n' + sorted_messages = [] + + if 'install' in messages: + install_message = messages.pop('install') + sorted_messages.append(' "install": "{}"'.format(install_message)) + + keys = sorted(map(self.sortable_tag, messages.keys())) + + for _, key in keys: + sorted_messages.append(' "{}": "{}"'.format(key, messages[key])) + + messages_json += ',\n'.join(sorted_messages) + messages_json += '\n}\n' + f.write(messages_json) + + return message_path + + def sortable_tag(self, tag): + """Return a version tag in a sortable form.""" + + if tag == 'install': + return (tag, tag) + + major, minor, release = tag.split('.') + + if '+' in release: + release, update = release.split('+') + update = '+{:04}'.format(int(update)) + else: + update = '' + + return ('{:04}.{:04}.{:04}{}'.format(int(major), int(minor), int(release), update), tag) + + def get_commit_messages_since(self, version): + """Return a formatted list of commit messages since the given tagged version.""" + + tag = '{}.{}.{}'.format(*version) + output = util.communicate([ + 'git', 'log', + '--pretty=format:{{{{%w(0,0,0)%s %b}}}}', + '--reverse', tag + '..' + ]) + + # Split the messages, they are bounded by {{{{ }}}} + messages = [] + + for match in self.COMMIT_MSG_RE.finditer(output): + messages.append(match.group(1).strip()) + + # Wrap the messages + wrapper = TextWrapper(initial_indent='- ', subsequent_indent=' ') + messages = list(map(lambda msg: '\n'.join(wrapper.wrap(msg)), messages)) + return '\n\n'.join(messages) + '\n' + + +class SublimelinterReportCommand(sublime_plugin.WindowCommand): + + """ + A command that displays a report of all errors. + + The scope of the report is all open files in the current window, + all files in all folders in the current window, or both. + + """ + + def run(self, on='files'): + """Run the command. on determines the scope of the report.""" + + output = self.window.new_file() + output.set_name('{} Error Report'.format(persist.PLUGIN_NAME)) + output.set_scratch(True) + + from .sublimelinter import SublimeLinter + self.plugin = SublimeLinter.shared_plugin() + + if on == 'files' or on == 'both': + for view in self.window.views(): + self.report(output, view) + + if on == 'folders' or on == 'both': + for folder in self.window.folders(): + self.folder(output, folder) + + def folder(self, output, folder): + """Report on all files in a folder.""" + + for root, dirs, files in os.walk(folder): + for name in files: + path = os.path.join(root, name) + + # Ignore files over 256K to speed things up a bit + if os.stat(path).st_size < 256 * 1024: + # TODO: not implemented + pass + + def report(self, output, view): + """Write a report on the given view to output.""" + + def finish_lint(view, linters, hit_time): + if not linters: + return + + def insert(edit): + if not any(l.errors for l in linters): + return + + filename = os.path.basename(linters[0].filename or 'untitled') + out = '\n{}:\n'.format(filename) + + for linter in sorted(linters, key=lambda linter: linter.name): + if linter.errors: + out += '\n {}:\n'.format(linter.name) + items = sorted(linter.errors.items()) + + # Get the highest line number so we know how much padding numbers need + highest_line = items[-1][0] + width = 1 + + while highest_line >= 10: + highest_line /= 10 + width += 1 + + for line, messages in items: + for col, message in messages: + out += ' {:>{width}}: {}\n'.format(line, message, width=width) + + output.insert(edit, output.size(), out) + + persist.edits[output.id()].append(insert) + output.run_command('sublimelinter_edit') + + kwargs = {'self': self.plugin, 'view_id': view.id(), 'callback': finish_lint} + + from .sublimelinter import SublimeLinter + Thread(target=SublimeLinter.lint, kwargs=kwargs).start() diff --git a/Packages/SublimeLinter/create_linter_info.json b/Packages/SublimeLinter/create_linter_info.json new file mode 100644 index 0000000..a931dc1 --- /dev/null +++ b/Packages/SublimeLinter/create_linter_info.json @@ -0,0 +1,27 @@ +{ + "javascript": { + "platform": "[Node.js](http://nodejs.org) (and [npm](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager) on Linux)", + "installer": "npm install -g {}", + "extra_steps": "1. If you are using `nvm` and `zsh`, ensure that the line to load `nvm` is in `.zshenv` and not `.zshrc`.\n\n1. If you are using `zsh` and `oh-my-zsh`, do not load the `nvm` plugin for `oh-my-zsh`.", + "comment_re": "r'\\s*/[/*]'" + }, + + "python": { + "platform": "[Python](http://python.org/download/) and [pip](http://www.pip-installer.org/en/latest/installing.html)", + "installer": "[sudo] pip install {}", + "superclass": "PythonLinter", + "attributes": ["module = '{}'", "check_version = False"] + }, + + "ruby": { + "platform": "[Ruby](http://www.ruby-lang.org)", + "installer": "[sudo] gem install {}", + "extra_steps": "1. If you are using `rbenv` or `rvm`, ensure that they are loaded in your shell’s correct startup file. See [here](http://sublimelinter.readthedocs.org/en/latest/troubleshooting.html#shell-startup-files) for more information.", + "superclass": "RubyLinter", + "comment_re": "r'\\s*#'" + }, + + "other": { + "installer": " install {}" + } +} diff --git a/Packages/SublimeLinter/fixed-syntaxes/HTML/HTML.tmLanguage b/Packages/SublimeLinter/fixed-syntaxes/HTML/HTML.tmLanguage new file mode 100644 index 0000000..76d0acc --- /dev/null +++ b/Packages/SublimeLinter/fixed-syntaxes/HTML/HTML.tmLanguage @@ -0,0 +1,1002 @@ + + + + + fileTypes + + html + htm + shtml + xhtml + phtml + inc + tmpl + tpl + ctp + + firstLineMatch + <!(?i:DOCTYPE)|<(?i:html)|<\?(?i:php) + foldingStartMarker + (?x) + (<(?i:head|body|table|thead|tbody|tfoot|tr|div|select|fieldset|style|script|ul|ol|li|form|dl)\b.*?> + |<!--(?!.*--\s*>) + |^<!--\ \#tminclude\ (?>.*?-->)$ + |<\?(?:php)?.*\b(if|for(each)?|while)\b.+: + |\{\{?(if|foreach|capture|literal|foreach|php|section|strip) + |\{\s*($|\?>\s*$|//|/\*(.*\*/\s*$|(?!.*?\*/))) + ) + foldingStopMarker + (?x) + (</(?i:head|body|table|thead|tbody|tfoot|tr|div|select|fieldset|style|script|ul|ol|li|form|dl)> + |^(?!.*?<!--).*?--\s*> + |^<!--\ end\ tminclude\ -->$ + |<\?(?:php)?.*\bend(if|for(each)?|while)\b + |\{\{?/(if|foreach|capture|literal|foreach|php|section|strip) + |^[^{]*\} + ) + keyEquivalent + ^~H + name + HTML + patterns + + + begin + (<)([a-zA-Z0-9:]++)(?=[^>]*></\2>) + beginCaptures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.html + + + end + (>)(<)(/)(\2)(>) + endCaptures + + 1 + + name + punctuation.definition.tag.end.html + + 2 + + name + punctuation.definition.tag.begin.html meta.scope.between-tag-pair.html + + 3 + + name + punctuation.definition.tag.begin.html + + 4 + + name + entity.name.tag.html + + 5 + + name + punctuation.definition.tag.end.html + + + name + meta.tag.any.html + patterns + + + include + #tag-stuff + + + + + begin + (<\?)(xml) + captures + + 1 + + name + punctuation.definition.tag.html + + 2 + + name + entity.name.tag.xml.html + + + end + (\?>) + name + meta.tag.preprocessor.xml.html + patterns + + + include + #tag-generic-attribute + + + include + #string-double-quoted + + + include + #string-single-quoted + + + + + begin + <!-- + captures + + 0 + + name + punctuation.definition.comment.html + + + end + --\s*> + name + comment.block.html + patterns + + + match + -- + name + invalid.illegal.bad-comments-or-CDATA.html + + + include + #embedded-code + + + + + begin + <! + captures + + 0 + + name + punctuation.definition.tag.html + + + end + > + name + meta.tag.sgml.html + patterns + + + begin + (?i:DOCTYPE) + captures + + 1 + + name + entity.name.tag.doctype.html + + + end + (?=>) + name + meta.tag.sgml.doctype.html + patterns + + + match + "[^">]*" + name + string.quoted.double.doctype.identifiers-and-DTDs.html + + + + + begin + \[CDATA\[ + end + ]](?=>) + name + constant.other.inline-data.html + + + match + (\s*)(?!--|>)\S(\s*) + name + invalid.illegal.bad-comments-or-CDATA.html + + + + + include + #embedded-code + + + begin + (?:^\s+)?(<)((?i:style))\b(?![^>]*/>) + beginCaptures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.style.html + + + end + (?<=</(?:[sS][tT][yY][lL][eE]))(>)(?:\s*\n)? + endCaptures + + 2 + + name + punctuation.definition.tag.html + + + name + meta.tag.style.html + patterns + + + include + #tag-stuff + + + begin + (?<!</(?:[sS][tT][yY][lL][eE]))(>) + captures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.style.html + + + end + (</)((?i:style)) + contentName + source.css.embedded.html + patterns + + + begin + /\* + captures + + 0 + + name + punctuation.definition.comment.css + + + end + \*/|(?=</[sS][tT][yY][lL][eE]) + name + comment.block.css + + + include + source.css + + + + + + + begin + (?:^\s+)?(<)((?i:script))\b(?![^>]*/>) + beginCaptures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.script.html + + + end + (?<=</([sS][cC][rR][iI][pP][tT]))(>)(?:\s*\n)? + endCaptures + + 2 + + name + punctuation.definition.tag.html + + + name + meta.tag.script.html + patterns + + + include + #tag-stuff + + + begin + (?<!</(?:[sS][cC][rR][iI][pP][tT]))(>) + captures + + 1 + + name + punctuation.definition.tag.end.html + + 2 + + name + entity.name.tag.script.html + + + end + (</)((?i:script)) + contentName + source.js.embedded.html + patterns + + + captures + + 1 + + name + punctuation.definition.comment.js + + + match + (//).*?((?=</[sS][cC][rR][iI][pP][tT])|$\n?) + name + comment.line.double-slash.js + + + begin + /\* + captures + + 0 + + name + punctuation.definition.comment.js + + + end + \*/|(?=</[sS][cC][rR][iI][pP][tT]) + name + comment.block.js + + + include + #php + + + include + source.js + + + + + + + begin + (</?)((?i:body|head|html)\b) + captures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.structure.any.html + + + end + (>) + endCaptures + + 1 + + name + punctuation.definition.tag.end.html + + + name + meta.tag.structure.any.html + patterns + + + include + #tag-stuff + + + + + begin + (</?)((?i:address|blockquote|dd|div|dl|dt|fieldset|form|frame|frameset|h1|h2|h3|h4|h5|h6|iframe|noframes|object|ol|p|ul|applet|center|dir|hr|menu|pre)\b) + beginCaptures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.block.any.html + + + end + (>) + endCaptures + + 1 + + name + punctuation.definition.tag.end.html + + + name + meta.tag.block.any.html + patterns + + + include + #tag-stuff + + + + + begin + (</?)((?i:a|abbr|acronym|area|b|base|basefont|bdo|big|br|button|caption|cite|code|col|colgroup|del|dfn|em|font|head|html|i|img|input|ins|isindex|kbd|label|legend|li|link|map|meta|noscript|optgroup|option|param|q|s|samp|script|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|title|tr|tt|u|var)\b) + beginCaptures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.inline.any.html + + + end + ((?: ?/)?>) + endCaptures + + 1 + + name + punctuation.definition.tag.end.html + + + name + meta.tag.inline.any.html + patterns + + + include + #tag-stuff + + + + + begin + (</?)([a-zA-Z0-9:]+) + beginCaptures + + 1 + + name + punctuation.definition.tag.begin.html + + 2 + + name + entity.name.tag.other.html + + + end + (>) + endCaptures + + 1 + + name + punctuation.definition.tag.end.html + + + name + meta.tag.other.html + patterns + + + include + #tag-stuff + + + + + include + #entities + + + match + <> + name + invalid.illegal.incomplete.html + + + match + < + name + invalid.illegal.bad-angle-bracket.html + + + repository + + embedded-code + + patterns + + + include + #ruby + + + include + #php + + + + include + #python + + + + entities + + patterns + + + captures + + 1 + + name + punctuation.definition.entity.html + + 3 + + name + punctuation.definition.entity.html + + + match + (&)([a-zA-Z0-9]+|#[0-9]+|#x[0-9a-fA-F]+)(;) + name + constant.character.entity.html + + + match + & + name + invalid.illegal.bad-ampersand.html + + + + php + + begin + (?=(^\s*)?<\?) + end + (?!(^\s*)?<\?) + patterns + + + include + source.php + + + + python + + begin + (?:^\s*)<\?python(?!.*\?>) + end + \?>(?:\s*$\n)? + contentName + source.python.embedded.html + patterns + + + include + source.python + + + + ruby + + patterns + + + begin + <%+# + captures + + 0 + + name + punctuation.definition.comment.erb + + + end + %> + name + comment.block.erb + + + begin + <%+(?!>)=? + captures + + 0 + + name + punctuation.section.embedded.ruby + + + end + -?%> + contentName + source.ruby.embedded.html + patterns + + + captures + + 1 + + name + punctuation.definition.comment.ruby + + + match + (#).*?(?=-?%>) + name + comment.line.number-sign.ruby + + + include + source.ruby + + + + + begin + <\?r(?!>)=? + captures + + 0 + + name + punctuation.section.embedded.ruby.nitro + + + end + -?\?> + contentName + source.ruby.nitro.embedded.html + patterns + + + captures + + 1 + + name + punctuation.definition.comment.ruby.nitro + + + match + (#).*?(?=-?\?>) + name + comment.line.number-sign.ruby.nitro + + + include + source.ruby + + + + + + + string-double-quoted + + begin + " + beginCaptures + + 0 + + name + punctuation.definition.string.begin.html + + + end + " + endCaptures + + 0 + + name + punctuation.definition.string.end.html + + + name + string.quoted.double.html + patterns + + + include + #embedded-code + + + include + #entities + + + + string-single-quoted + + begin + ' + beginCaptures + + 0 + + name + punctuation.definition.string.begin.html + + + end + ' + endCaptures + + 0 + + name + punctuation.definition.string.end.html + + + name + string.quoted.single.html + patterns + + + include + #embedded-code + + + include + #entities + + + + tag-generic-attribute + + match + \b([a-zA-Z\-:]+) + name + entity.other.attribute-name.html + + tag-id-attribute + + begin + \b(id)\b\s*(=) + captures + + 1 + + name + entity.other.attribute-name.id.html + + 2 + + name + punctuation.separator.key-value.html + + + end + (?<='|") + name + meta.attribute-with-value.id.html + patterns + + + begin + " + beginCaptures + + 0 + + name + punctuation.definition.string.begin.html + + + contentName + meta.toc-list.id.html + end + " + endCaptures + + 0 + + name + punctuation.definition.string.end.html + + + name + string.quoted.double.html + patterns + + + include + #embedded-code + + + include + #entities + + + + + begin + ' + beginCaptures + + 0 + + name + punctuation.definition.string.begin.html + + + contentName + meta.toc-list.id.html + end + ' + endCaptures + + 0 + + name + punctuation.definition.string.end.html + + + name + string.quoted.single.html + patterns + + + include + #embedded-code + + + include + #entities + + + + + + tag-stuff + + patterns + + + include + #tag-id-attribute + + + include + #tag-generic-attribute + + + include + #string-double-quoted + + + include + #string-single-quoted + + + include + #embedded-code + + + + + scopeName + text.html.basic + uuid + 17994EC8-6B1D-11D9-AC3A-000D93589AF6 + + diff --git a/Packages/SublimeLinter/fixed-syntaxes/HTML/sublimelinter.version b/Packages/SublimeLinter/fixed-syntaxes/HTML/sublimelinter.version new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/Packages/SublimeLinter/fixed-syntaxes/HTML/sublimelinter.version @@ -0,0 +1 @@ +2 diff --git a/Packages/SublimeLinter/fixed-syntaxes/Rails/HTML (Rails).tmLanguage b/Packages/SublimeLinter/fixed-syntaxes/Rails/HTML (Rails).tmLanguage new file mode 100644 index 0000000..cd901b0 --- /dev/null +++ b/Packages/SublimeLinter/fixed-syntaxes/Rails/HTML (Rails).tmLanguage @@ -0,0 +1,100 @@ + + + + + fileTypes + + rhtml + erb + html.erb + + foldingStartMarker + (?x) + (<(?i:head|body|table|thead|tbody|tfoot|tr|div|select|fieldset|style|script|ul|ol|form|dl)\b.*?> + |<!--(?!.*-->) + |\{\s*($|\?>\s*$|//|/\*(.*\*/\s*$|(?!.*?\*/))) + ) + foldingStopMarker + (?x) + (</(?i:head|body|table|thead|tbody|tfoot|tr|div|select|fieldset|style|script|ul|ol|form|dl)> + |^\s*--> + |(^|\s)\} + ) + keyEquivalent + ^~R + name + HTML (Rails) + patterns + + + begin + <%+# + captures + + 0 + + name + punctuation.definition.comment.erb + + + end + %> + name + comment.block.erb + + + begin + <%+(?!>)[-=]? + beginCaptures + + 0 + + name + punctuation.section.embedded.ruby + + + end + -?%> + endCaptures + + 0 + + name + punctuation.section.embedded.ruby + + + contentName + source.ruby.rails.embedded.html + patterns + + + captures + + 1 + + name + punctuation.definition.comment.ruby + + + match + (#).*?(?=-?%>) + name + comment.line.number-sign.ruby + + + include + source.ruby.rails + + + + + include + text.html.basic + + + scopeName + text.html.ruby + uuid + 45D7E1FC-7D0B-4105-A1A2-3D10BB555A5C + + diff --git a/Packages/SublimeLinter/fixed-syntaxes/Rails/sublimelinter.version b/Packages/SublimeLinter/fixed-syntaxes/Rails/sublimelinter.version new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Packages/SublimeLinter/fixed-syntaxes/Rails/sublimelinter.version @@ -0,0 +1 @@ +1 diff --git a/Packages/SublimeLinter/gutter-themes/Blueberry/LICENSE b/Packages/SublimeLinter/gutter-themes/Blueberry/LICENSE new file mode 100644 index 0000000..b16ae72 --- /dev/null +++ b/Packages/SublimeLinter/gutter-themes/Blueberry/LICENSE @@ -0,0 +1,4 @@ + +This work (Blueberry icons, by Andrew Zhebrakov) is free of known copyright restrictions. + +http://www.icojam.com/blog/?p=259#more-259 diff --git a/Packages/SublimeLinter/gutter-themes/Blueberry/cross/Blueberry - cross.gutter-theme b/Packages/SublimeLinter/gutter-themes/Blueberry/cross/Blueberry - cross.gutter-theme new file mode 100644 index 0000000..e69de29 diff --git a/Packages/SublimeLinter/gutter-themes/Blueberry/cross/error.png b/Packages/SublimeLinter/gutter-themes/Blueberry/cross/error.png new file mode 100644 index 0000000..11cac36 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Blueberry/cross/error.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Blueberry/cross/warning.png b/Packages/SublimeLinter/gutter-themes/Blueberry/cross/warning.png new file mode 100644 index 0000000..0edc994 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Blueberry/cross/warning.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Blueberry/round/Blueberry - round.gutter-theme b/Packages/SublimeLinter/gutter-themes/Blueberry/round/Blueberry - round.gutter-theme new file mode 100644 index 0000000..e69de29 diff --git a/Packages/SublimeLinter/gutter-themes/Blueberry/round/error.png b/Packages/SublimeLinter/gutter-themes/Blueberry/round/error.png new file mode 100644 index 0000000..83acd8f Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Blueberry/round/error.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Blueberry/round/warning.png b/Packages/SublimeLinter/gutter-themes/Blueberry/round/warning.png new file mode 100644 index 0000000..0edc994 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Blueberry/round/warning.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Circle/Circle.gutter-theme b/Packages/SublimeLinter/gutter-themes/Circle/Circle.gutter-theme new file mode 100644 index 0000000..8dbfdad --- /dev/null +++ b/Packages/SublimeLinter/gutter-themes/Circle/Circle.gutter-theme @@ -0,0 +1,3 @@ +{ + "colorize": true +} diff --git a/Packages/SublimeLinter/gutter-themes/Circle/error.png b/Packages/SublimeLinter/gutter-themes/Circle/error.png new file mode 100644 index 0000000..4e0b0d0 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Circle/error.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Circle/warning.png b/Packages/SublimeLinter/gutter-themes/Circle/warning.png new file mode 100644 index 0000000..32d0559 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Circle/warning.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Danish Royalty/Danish Royalty.gutter-theme b/Packages/SublimeLinter/gutter-themes/Danish Royalty/Danish Royalty.gutter-theme new file mode 100644 index 0000000..e69de29 diff --git a/Packages/SublimeLinter/gutter-themes/Danish Royalty/LICENSE b/Packages/SublimeLinter/gutter-themes/Danish Royalty/LICENSE new file mode 100644 index 0000000..2f65282 --- /dev/null +++ b/Packages/SublimeLinter/gutter-themes/Danish Royalty/LICENSE @@ -0,0 +1,4 @@ + +These icons are free to use in both commercial products as well as personal use. + +http://www.softicons.com/free-icons/toolbar-icons/danish-royalty-free-icons-by-jonasrask-design diff --git a/Packages/SublimeLinter/gutter-themes/Danish Royalty/error.png b/Packages/SublimeLinter/gutter-themes/Danish Royalty/error.png new file mode 100644 index 0000000..a8f75a4 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Danish Royalty/error.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Danish Royalty/warning.png b/Packages/SublimeLinter/gutter-themes/Danish Royalty/warning.png new file mode 100644 index 0000000..8c1c019 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Danish Royalty/warning.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Default/Default.gutter-theme b/Packages/SublimeLinter/gutter-themes/Default/Default.gutter-theme new file mode 100644 index 0000000..8dbfdad --- /dev/null +++ b/Packages/SublimeLinter/gutter-themes/Default/Default.gutter-theme @@ -0,0 +1,3 @@ +{ + "colorize": true +} diff --git a/Packages/SublimeLinter/gutter-themes/Default/error.png b/Packages/SublimeLinter/gutter-themes/Default/error.png new file mode 100644 index 0000000..932a6c7 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Default/error.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Default/warning.png b/Packages/SublimeLinter/gutter-themes/Default/warning.png new file mode 100644 index 0000000..190bf27 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Default/warning.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Hands/Hands.gutter-theme b/Packages/SublimeLinter/gutter-themes/Hands/Hands.gutter-theme new file mode 100644 index 0000000..e69de29 diff --git a/Packages/SublimeLinter/gutter-themes/Hands/LICENSE b/Packages/SublimeLinter/gutter-themes/Hands/LICENSE new file mode 100644 index 0000000..39166bb --- /dev/null +++ b/Packages/SublimeLinter/gutter-themes/Hands/LICENSE @@ -0,0 +1,4 @@ + +Freeware, commercial usage allowed. + +http://www.softicons.com/free-icons/web-icons/free-hand-pointer-icons-by-icojoy diff --git a/Packages/SublimeLinter/gutter-themes/Hands/error.png b/Packages/SublimeLinter/gutter-themes/Hands/error.png new file mode 100644 index 0000000..4f5b331 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Hands/error.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Hands/warning.png b/Packages/SublimeLinter/gutter-themes/Hands/warning.png new file mode 100644 index 0000000..d6910ba Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Hands/warning.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Knob/LICENSE b/Packages/SublimeLinter/gutter-themes/Knob/LICENSE new file mode 100644 index 0000000..25b5269 --- /dev/null +++ b/Packages/SublimeLinter/gutter-themes/Knob/LICENSE @@ -0,0 +1,4 @@ + +You are free to use these icons on your software application, website, etc. You're welcome to give credits for the graphics, when used. + +http://www.softicons.com/free-icons/toolbar-icons/knob-buttons-toolbar-icons-by-itweek diff --git a/Packages/SublimeLinter/gutter-themes/Knob/simple/Knob - simple.gutter-theme b/Packages/SublimeLinter/gutter-themes/Knob/simple/Knob - simple.gutter-theme new file mode 100644 index 0000000..e69de29 diff --git a/Packages/SublimeLinter/gutter-themes/Knob/simple/error.png b/Packages/SublimeLinter/gutter-themes/Knob/simple/error.png new file mode 100644 index 0000000..5a92bc5 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Knob/simple/error.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Knob/simple/warning.png b/Packages/SublimeLinter/gutter-themes/Knob/simple/warning.png new file mode 100644 index 0000000..2706d2a Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Knob/simple/warning.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Knob/symbol/Knob - symbol.gutter-theme b/Packages/SublimeLinter/gutter-themes/Knob/symbol/Knob - symbol.gutter-theme new file mode 100644 index 0000000..e69de29 diff --git a/Packages/SublimeLinter/gutter-themes/Knob/symbol/error.png b/Packages/SublimeLinter/gutter-themes/Knob/symbol/error.png new file mode 100644 index 0000000..00dcf56 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Knob/symbol/error.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Knob/symbol/warning.png b/Packages/SublimeLinter/gutter-themes/Knob/symbol/warning.png new file mode 100644 index 0000000..20fcea6 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Knob/symbol/warning.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Koloria/Koloria.gutter-theme b/Packages/SublimeLinter/gutter-themes/Koloria/Koloria.gutter-theme new file mode 100644 index 0000000..e69de29 diff --git a/Packages/SublimeLinter/gutter-themes/Koloria/LICENSE b/Packages/SublimeLinter/gutter-themes/Koloria/LICENSE new file mode 100644 index 0000000..d2020a3 --- /dev/null +++ b/Packages/SublimeLinter/gutter-themes/Koloria/LICENSE @@ -0,0 +1,7 @@ + +- Koloria Icon Set can be used in open source or commercial projects for free. +- When you mention Koloria Icon Set, please refer to this page/download link. +- Do not sell the package on graphic stock sites – this pack is intended to be free. You may include the icons in your commercial themes/designs/creations. +- Do not claim to be the author of this icon set. + +http://www.graphicrating.com/2012/06/14/koloria-free-icons-set/ diff --git a/Packages/SublimeLinter/gutter-themes/Koloria/error.png b/Packages/SublimeLinter/gutter-themes/Koloria/error.png new file mode 100644 index 0000000..7f4fc55 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Koloria/error.png differ diff --git a/Packages/SublimeLinter/gutter-themes/Koloria/warning.png b/Packages/SublimeLinter/gutter-themes/Koloria/warning.png new file mode 100644 index 0000000..18f5e57 Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/Koloria/warning.png differ diff --git a/Packages/SublimeLinter/gutter-themes/ProjectIcons/LICENSE b/Packages/SublimeLinter/gutter-themes/ProjectIcons/LICENSE new file mode 100644 index 0000000..e7260be --- /dev/null +++ b/Packages/SublimeLinter/gutter-themes/ProjectIcons/LICENSE @@ -0,0 +1,18 @@ + +Author + +These icons were created by Mihaiciuc Bogdan and are available for download at deviantArt. + + +License + +These icons are licensed under a Creative-Commons Attribution 3.0 license. You are allowed to share, modify and use these icons in your projects as long as you credit me as the original author. +Click here for more information about the license. + + +Attribution + +If you use these icons, you must credit me, Mihaiciuc Bogdan, as the original author of the icons in your project’s “About” window/page and place a link to http://bogo-d.deviantart.com. + +The suggested format is: “Uses icons from “Project Icons” by Mihaiciuc Bogdan.” +The text “Mihaiciuc Bogdan” should link to http://bogo-d.deviantart.com. diff --git a/Packages/SublimeLinter/gutter-themes/ProjectIcons/ProjectIcons.gutter-theme b/Packages/SublimeLinter/gutter-themes/ProjectIcons/ProjectIcons.gutter-theme new file mode 100644 index 0000000..e69de29 diff --git a/Packages/SublimeLinter/gutter-themes/ProjectIcons/error.png b/Packages/SublimeLinter/gutter-themes/ProjectIcons/error.png new file mode 100644 index 0000000..cc955ad Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/ProjectIcons/error.png differ diff --git a/Packages/SublimeLinter/gutter-themes/ProjectIcons/warning.png b/Packages/SublimeLinter/gutter-themes/ProjectIcons/warning.png new file mode 100644 index 0000000..74b336d Binary files /dev/null and b/Packages/SublimeLinter/gutter-themes/ProjectIcons/warning.png differ diff --git a/Packages/SublimeLinter/lint/__init__.py b/Packages/SublimeLinter/lint/__init__.py new file mode 100644 index 0000000..d68ee56 --- /dev/null +++ b/Packages/SublimeLinter/lint/__init__.py @@ -0,0 +1,32 @@ +# +# lint.__init__ +# Part of SublimeLinter3, a code checking framework for Sublime Text 3 +# +# Written by Ryan Hileman and Aparajita Fishman +# +# Project: https://github.com/SublimeLinter/SublimeLinter3 +# License: MIT +# + +"""This module exports the linter classes and the highlight, linter, persist and util submodules.""" + +from .linter import Linter +from .python_linter import PythonLinter +from .ruby_linter import RubyLinter + +from . import ( + highlight, + linter, + persist, + util, +) + +__all__ = [ + 'highlight', + 'Linter', + 'PythonLinter', + 'RubyLinter', + 'linter', + 'persist', + 'util', +] diff --git a/Packages/SublimeLinter/lint/highlight.py b/Packages/SublimeLinter/lint/highlight.py new file mode 100644 index 0000000..3b2c562 --- /dev/null +++ b/Packages/SublimeLinter/lint/highlight.py @@ -0,0 +1,449 @@ +# +# highlight.py +# Part of SublimeLinter3, a code checking framework for Sublime Text 3 +# +# Written by Ryan Hileman and Aparajita Fishman +# +# Project: https://github.com/SublimeLinter/SublimeLinter3 +# License: MIT +# + +""" +This module implements highlighting code with marks. + +The following classes are exported: + +HighlightSet +Highlight + + +The following constants are exported: + +WARNING - name of warning type +ERROR - name of error type + +MARK_KEY_FORMAT - format string for key used to mark code regions +GUTTER_MARK_KEY_FORMAT - format string for key used to mark gutter mark regions +MARK_SCOPE_FORMAT - format string used for color scheme scope names + +""" + +import re +import sublime +from . import persist + +# +# Error types +# +WARNING = 'warning' +ERROR = 'error' + +MARK_KEY_FORMAT = 'sublimelinter-{}-marks' +GUTTER_MARK_KEY_FORMAT = 'sublimelinter-{}-gutter-marks' +MARK_SCOPE_FORMAT = 'sublimelinter.mark.{}' + +UNDERLINE_FLAGS = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE | sublime.DRAW_EMPTY_AS_OVERWRITE + +MARK_STYLES = { + 'outline': sublime.DRAW_NO_FILL, + 'fill': sublime.DRAW_NO_OUTLINE, + 'solid underline': sublime.DRAW_SOLID_UNDERLINE | UNDERLINE_FLAGS, + 'squiggly underline': sublime.DRAW_SQUIGGLY_UNDERLINE | UNDERLINE_FLAGS, + 'stippled underline': sublime.DRAW_STIPPLED_UNDERLINE | UNDERLINE_FLAGS, + 'none': sublime.HIDDEN +} + +WORD_RE = re.compile(r'^([-\w]+)') +NEAR_RE_TEMPLATE = r'(?@|--?)?(?P[@\w][\w\-]*)(?:(?P[=:])(?:(?P.)(?P\+)?)?)?') +BASE_CLASSES = ('PythonLinter',) +HTML_ENTITY_RE = re.compile(r'&(?:(?:#(x)?([0-9a-fA-F]{1,4}))|(\w+));') + + +class LinterMeta(type): + + """Metaclass for Linter and its subclasses.""" + + def __init__(cls, name, bases, attrs): + """ + Initialize a Linter class. + + When a Linter subclass is loaded by Sublime Text, this method is called. + We take this opportunity to do some transformations: + + - Compile regex patterns. + - Convert strings to tuples where necessary. + - Add a leading dot to the tempfile_suffix if necessary. + - Build a map between defaults and linter arguments. + - Add '@python' as an inline setting to PythonLinter subclasses. + + Finally, the class is registered as a linter for its configured syntax. + + """ + + if bases: + setattr(cls, 'disabled', False) + + if name in ('PythonLinter', 'RubyLinter'): + return + + cls.alt_name = cls.make_alt_name(name) + cmd = attrs.get('cmd') + + if isinstance(cmd, str): + setattr(cls, 'cmd', shlex.split(cmd)) + + syntax = attrs.get('syntax') + + try: + if isinstance(syntax, str) and syntax[0] == '^': + setattr(cls, 'syntax', re.compile(syntax)) + except re.error as err: + persist.printf( + 'ERROR: {} disabled, error compiling syntax: {}' + .format(name.lower(), str(err)) + ) + setattr(cls, 'disabled', True) + + if not cls.disabled: + for regex in ('regex', 'comment_re', 'word_re', 'version_re'): + attr = getattr(cls, regex) + + if isinstance(attr, str): + if regex == 'regex' and cls.multiline: + setattr(cls, 're_flags', cls.re_flags | re.MULTILINE) + + try: + setattr(cls, regex, re.compile(attr, cls.re_flags)) + except re.error as err: + persist.printf( + 'ERROR: {} disabled, error compiling {}: {}' + .format(name.lower(), regex, str(err)) + ) + setattr(cls, 'disabled', True) + + if not cls.disabled: + if not cls.syntax or (cls.cmd is not None and not cls.cmd) or not cls.regex: + persist.printf('ERROR: {} disabled, not fully implemented'.format(name.lower())) + setattr(cls, 'disabled', True) + + for attr in ('inline_settings', 'inline_overrides'): + if attr in attrs and isinstance(attrs[attr], str): + setattr(cls, attr, (attrs[attr],)) + + # If this class has its own defaults, create an args_map. + # Otherwise we use the superclass' args_map. + if 'defaults' in attrs and attrs['defaults']: + cls.map_args(attrs['defaults']) + + if 'PythonLinter' in [base.__name__ for base in bases]: + # Set attributes necessary for the @python inline setting + inline_settings = list(getattr(cls, 'inline_settings') or []) + setattr(cls, 'inline_settings', inline_settings + ['@python']) + + if persist.plugin_is_loaded: + # If the plugin has already loaded, then we get here because + # a linter was added or reloaded. In that case we run reinitialize. + cls.reinitialize() + + if 'syntax' in attrs and name not in BASE_CLASSES: + persist.register_linter(cls, name, attrs) + + def map_args(cls, defaults): + """ + Map plain setting names to args that will be passed to the linter executable. + + For each item in defaults, the key is matched with ARG_RE. If there is a match, + the key is stripped of meta information and the match groups are stored as a dict + under the stripped key. + + """ + + # Check if the settings specify an argument. + # If so, add a mapping between the setting and the argument format, + # then change the name in the defaults to the setting name. + args_map = {} + setattr(cls, 'defaults', {}) + + for name, value in defaults.items(): + match = ARG_RE.match(name) + + if match: + name = match.group('name') + args_map[name] = match.groupdict() + + cls.defaults[name] = value + + setattr(cls, 'args_map', args_map) + + @staticmethod + def make_alt_name(name): + """Convert and return a camel-case name to lowercase with dashes.""" + previous = name[0] + alt_name = previous.lower() + + for c in name[1:]: + if c.isupper() and previous.islower(): + alt_name += '-' + + alt_name += c.lower() + previous = c + + return alt_name + + @property + def name(cls): + """Return the class name lowercased.""" + return cls.__name__.lower() + + +class Linter(metaclass=LinterMeta): + + """ + The base class for linters. + + Subclasses must at a minimum define the attributes syntax, cmd, and regex. + + """ + + # + # Public attributes + # + + # The syntax that the linter handles. May be a string or + # list/tuple of strings. Names should be all lowercase. + syntax = '' + + # A string, list, tuple or callable that returns a string, list or tuple, containing the + # command line (with arguments) used to lint. + cmd = '' + + # If the name of the executable cannot be determined by the first element of cmd + # (for example when cmd is a method that dynamically generates the command line arguments), + # this can be set to the name of the executable used to do linting. + # + # Once the executable's name is determined, its existence is checked in the user's path. + # If it is not available, the linter is disabled. + executable = None + + # If the executable is available, this is set to the full path of the executable. + # If the executable is not available, it is set an empty string. + # Subclasses should consider this read only. + executable_path = None + + # Some linter plugins have version requirements as far as the linter executable. + # The following three attributes can be defined to define the requirements. + # version_args is a string/list/tuple that represents the args used to get + # the linter executable's version as a string. + version_args = None + + # A regex pattern or compiled regex used to match the numeric portion of the version + # from the output of version_args. It must contain a named capture group called + # "version" that captures only the version, including dots but excluding a prefix + # such as "v". + version_re = None + + # A string which describes the version requirements, suitable for passing to + # the distutils.versionpredicate.VersionPredicate constructor, as documented here: + # http://pydoc.org/2.5.1/distutils.versionpredicate.html + # Only the version requirements (what is inside the parens) should be + # specified here, do not include the package name or parens. + version_requirement = None + + # A regex pattern used to extract information from the executable's output. + regex = '' + + # Set to True if the linter outputs multiline error messages. When True, + # regex will be created with the re.MULTILINE flag. Do NOT rely on setting + # the re.MULTILINE flag within the regex yourself, this attribute must be set. + multiline = False + + # If you want to set flags on the regex *other* than re.MULTILINE, set this. + re_flags = 0 + + # The default type assigned to non-classified errors. Should be either + # highlight.ERROR or highlight.WARNING. + default_type = highlight.ERROR + + # Linters usually report errors with a line number, some with a column number + # as well. In general, most linters report one-based line numbers and column + # numbers. If a linter uses zero-based line numbers or column numbers, the + # linter class should define this attribute accordingly. + line_col_base = (1, 1) + + # If the linter executable cannot receive from stdin and requires a temp file, + # set this attribute to the suffix of the temp file (with or without leading '.'). + # If the suffix needs to be mapped to the syntax of a file, you may make this + # a dict that maps syntax names (all lowercase, as used in the syntax attribute), + # to tempfile suffixes. The syntax used to lookup the suffix is the mapped + # syntax, after using "syntax_map" in settings. If the view's syntax is not + # in this map, the class' syntax will be used. + # + # Some linters can only work from an actual disk file, because they + # rely on an entire directory structure that cannot be realistically be copied + # to a temp directory (e.g. javac). In such cases, set this attribute to '-', + # which marks the linter as "file-only". That will disable the linter for + # any views that are dirty. + tempfile_suffix = None + + # Linters may output to both stdout and stderr. By default stdout and sterr are captured. + # If a linter will never output anything useful on a stream (including when + # there is an error within the linter), you can ignore that stream by setting + # this attribute to the other stream. + error_stream = util.STREAM_BOTH + + # Many linters look for a config file in the linted file’s directory and in + # all parent directories up to the root directory. However, some of them + # will not do this if receiving input from stdin, and others use temp files, + # so looking in the temp file directory doesn’t work. If this attribute + # is set to a tuple of a config file argument and the name of the config file, + # the linter will automatically try to find the config file, and if it is found, + # add the config file argument to the executed command. + # + # Example: config_file = ('--config', '.jshintrc') + # + config_file = None + + # Tab width + tab_width = 1 + + # If a linter can be used with embedded code, you need to tell SublimeLinter + # which portions of the source code contain the embedded code by specifying + # the embedded scope selectors. This attribute maps syntax names + # to embedded scope selectors. + # + # For example, the HTML syntax uses the scope `source.js.embedded.html` + # for embedded JavaScript. To allow a JavaScript linter to lint that embedded + # JavaScript, you would set this attribute to {'html': 'source.js.embedded.html'}. + selectors = {} + + # If a linter reports a column position, SublimeLinter highlights the nearest + # word at that point. You can customize the regex used to highlight words + # by setting this to a pattern string or a compiled regex. + word_re = None + + # If you want to provide default settings for the linter, set this attribute. + # If a setting will be passed as an argument to the linter executable, + # you may specify the format of the argument here and the setting will + # automatically be passed as an argument to the executable. The format + # specification is as follows: + # + # [[+]] + # + # - : Either empty, '@', '-' or '--'. + # - : The name of the setting. + # - : Either '=' or ':'. If is empty or '@', is ignored. + # Otherwise, if '=', the setting value is joined with by '=' and + # passed as a single argument. If ':', and the value are passed + # as separate arguments. + # - : If the argument accepts a list of values, specifies + # the character used to delimit the list (usually ','). + # - +: If the setting can be a list of values, but each value must be + # passed as a separate argument, terminate the setting with '+'. + # + # After the format is parsed, the prefix and suffix are removed and the + # setting is replaced with . + defaults = None + + # Linters may define a list of settings that can be specified inline. + # As with defaults, you can specify that an inline setting should be passed + # as an argument by using a prefix and optional suffix. However, if + # the same setting was already specified as an argument in defaults, + # you do not need to use the prefix or suffix here. + # + # Within a file, the actual inline setting name is '-setting', where + # is the lowercase name of the linter class. + inline_settings = None + + # Many linters allow a list of options to be specified for a single setting. + # For example, you can often specify a list of errors to ignore. + # This attribute is like inline_settings, but inline values will override + # existing values instead of replacing them, using the override_options method. + inline_overrides = None + + # If the linter supports inline settings, you need to specify the regex that + # begins a comment. comment_re should be an unanchored pattern (no ^) + # that matches everything through the comment prefix, including leading whitespace. + # + # For example, to specify JavaScript comments, you would use the pattern: + # r'\s*/[/*]' + # and for python: + # r'\s*#' + comment_re = None + + # Some linters may want to turn a shebang into an inline setting. + # To do so, set this attribute to a callback which receives the first line + # of code and returns a tuple/list which contains the name and value for the + # inline setting, or None if there is no match. + shebang_match = None + + # + # Internal class storage, do not set + # + RC_SEARCH_LIMIT = 3 + errors = None + highlight = None + lint_settings = None + env = None + disabled = False + executable_version = None + + @classmethod + def initialize(cls): + """ + Perform class-level initialization. + + If subclasses override this, they should call super().initialize() first. + + """ + pass + + @classmethod + def reinitialize(cls): + """ + Perform class-level initialization after plugins have been loaded at startup. + + This occurs if a new linter plugin is added or reloaded after startup. + Subclasses may override this to provide custom behavior, then they must + call cls.initialize(). + + """ + cls.initialize() + + def __init__(self, view, syntax): + self.view = view + self.syntax = syntax + self.code = '' + self.highlight = highlight.Highlight() + self.ignore_matches = None + + @property + def filename(self): + """Return the view's file path or '' of unsaved.""" + return self.view.file_name() or '' + + @property + def name(self): + """Return the class name lowercased.""" + return self.__class__.__name__.lower() + + @classmethod + def clear_settings_caches(cls): + """Clear lru caches for this class' methods.""" + cls.get_view_settings.cache_clear() + cls.get_merged_settings.cache_clear() + + @classmethod + def settings(cls): + """Return the default settings for this linter, merged with the user settings.""" + + if cls.lint_settings is None: + linters = persist.settings.get('linters', {}) + cls.lint_settings = (cls.defaults or {}).copy() + cls.lint_settings.update(linters.get(cls.name, {})) + + return cls.lint_settings + + @staticmethod + def meta_settings(settings): + """Return a dict with the items in settings whose keys begin with '@'.""" + return {key: value for key, value in settings.items() if key.startswith('@')} + + @lru_cache(maxsize=None) + def get_view_settings(self, no_inline=False): + """ + Return a union of all settings specific to this linter, related to the given view. + + The settings are merged in the following order: + + default settings + user settings + project settings + user + project meta settings + rc settings + rc meta settings + shebang or inline settings (overrides) + + """ + + settings = self.get_merged_settings() + + if not no_inline: + inline_settings = {} + + if self.shebang_match: + eol = self.code.find('\n') + + if eol != -1: + setting = self.shebang_match(self.code[0:eol]) + + if setting is not None: + inline_settings[setting[0]] = setting[1] + + if self.comment_re and (self.inline_settings or self.inline_overrides): + inline_settings.update(util.inline_settings( + self.comment_re, + self.code, + prefix=self.name, + alt_prefix=self.alt_name + )) + + settings = self.merge_inline_settings(settings.copy(), inline_settings) + + return settings + + @lru_cache(maxsize=None) + def get_merged_settings(self): + """ + Return a union of all non-inline settings specific to this linter, related to the given view. + + The settings are merged in the following order: + + default settings + user settings + project settings + user + project meta settings + rc settings + rc meta settings + + """ + + # Start with the overall project settings. Note that when + # files are loaded during quick panel preview, it can happen + # that they are linted without having a window. + window = self.view.window() + + if window: + data = window.project_data() or {} + project_settings = data.get(persist.PLUGIN_NAME, {}) + else: + project_settings = {} + + # Merge global meta settings with project meta settings + meta = self.meta_settings(persist.settings.settings) + meta.update(self.meta_settings(project_settings)) + + # Get the linter's project settings, update them with meta settings + project_settings = project_settings.get('linters', {}).get(self.name, {}) + project_settings.update(meta) + + # Update the linter's settings with the project settings + settings = self.merge_project_settings(self.settings().copy(), project_settings) + + # Update with rc settings + self.merge_rc_settings(settings) + + return settings + + def merge_rc_settings(self, settings): + """ + Merge .sublimelinterrc settings with settings. + + Searches for .sublimelinterrc in, starting at the directory of the linter's view. + The search is limited to rc_search_limit directories. If found, the meta settings + and settings for this linter in the rc file are merged with settings. + + """ + + search_limit = persist.settings.get('rc_search_limit', self.RC_SEARCH_LIMIT) + rc_settings = util.get_view_rc_settings(self.view, limit=search_limit) + + if rc_settings: + meta = self.meta_settings(rc_settings) + rc_settings = rc_settings.get('linters', {}).get(self.name, {}) + rc_settings.update(meta) + settings.update(rc_settings) + + def merge_inline_settings(self, view_settings, inline_settings): + """ + Return view settings merged with inline settings. + + view_settings is merged with inline_settings specified by + the class attributes inline_settings and inline_overrides. + view_settings is updated in place and returned. + + """ + + for setting, value in inline_settings.items(): + if self.inline_settings and setting in self.inline_settings: + view_settings[setting] = value + elif self.inline_overrides and setting in self.inline_overrides: + options = view_settings[setting] + sep = self.args_map.get(setting, {}).get('sep') + + if sep: + kwargs = {'sep': sep} + options = options or '' + else: + kwargs = {} + options = options or () + + view_settings[setting] = self.override_options(options, value, **kwargs) + + return view_settings + + def merge_project_settings(self, view_settings, project_settings): + """ + Return this linter's view settings merged with the current project settings. + + Subclasses may override this if they wish to do something more than + replace view settings with inline settings of the same name. + The settings object may be changed in place. + + """ + view_settings.update(project_settings) + return view_settings + + def override_options(self, options, overrides, sep=','): + """ + Return a list of options with overrides applied. + + If you want inline settings to override but not replace view settings, + this method makes it easier. Given a set or sequence of options and some + overrides, this method will do the following: + + - Copies options into a set. + - Split overrides into a list if it's a string, using sep to split. + - Iterates over each value in the overrides list: + - If it begins with '+', the value (without '+') is added to the options set. + - If it begins with '-', the value (without '-') is removed from the options set. + - Otherwise the value is added to the options set. + - The options set is converted to a list and returned. + + For example, given the options 'E101,E501,W' and the overrides + '-E101,E202,-W,+W324', we would end up with 'E501,E202,W324'. + + """ + + if isinstance(options, str): + options = options.split(sep) if options else () + return_str = True + else: + return_str = False + + modified_options = set(options) + + if isinstance(overrides, str): + overrides = overrides.split(sep) + + for override in overrides: + if not override: + continue + elif override[0] == '+': + modified_options.add(override[1:]) + elif override[0] == '-': + modified_options.discard(override[1:]) + else: + modified_options.add(override) + + if return_str: + return sep.join(modified_options) + else: + return list(modified_options) + + @classmethod + def assign(cls, view, linter_name=None, reset=False): + """ + Assign linters to a view. + + If reset is True, the list of linters for view is completely rebuilt. + + can_lint for each known linter class is called to determine + if the linter class can lint the syntax for view. If so, a new instance + of the linter class is assigned to the view, unless linter_name is non-empty + and does not match the 'name' attribute of any of the view's linters. + + Each view has its own linters so that linters can store persistent data + about a view. + + """ + + vid = view.id() + persist.views[vid] = view + syntax = persist.get_syntax(view) + + if not syntax: + cls.remove(vid) + return + + view_linters = persist.view_linters.get(vid, set()) + linters = set() + + for name, linter_class in persist.linter_classes.items(): + if not linter_class.disabled and linter_class.can_lint(syntax): + + if reset: + instantiate = True + else: + linter = None + + for l in view_linters: + if name == l.name: + linter = l + break + + if linter is None: + instantiate = True + else: + # If there is an existing linter and no linter_name was passed, + # leave it. If linter_name was passed, re-instantiate only if + # the linter's name matches linter_name. + instantiate = linter_name == linter.name + + if instantiate: + linter = linter_class(view, syntax) + + linters.add(linter) + + if linters: + persist.view_linters[vid] = linters + elif reset and not linters and vid in persist.view_linters: + del persist.view_linters[vid] + + @classmethod + def remove(cls, vid): + """Remove a the mapping between a view and its set of linters.""" + + if vid in persist.view_linters: + for linters in persist.view_linters[vid]: + linters.clear() + + del persist.view_linters[vid] + + @classmethod + def reload(cls): + """Assign new instances of linters to views.""" + + # Merge linter default settings with user settings + for name, linter in persist.linter_classes.items(): + linter.lint_settings = None + + for vid, linters in persist.view_linters.items(): + for linter in linters: + linter.clear() + persist.view_linters[vid].remove(linter) + linter_class = persist.linter_classes[linter.name] + linter = linter_class(linter.view, linter.syntax) + persist.view_linters[vid].add(linter) + + @classmethod + def apply_to_all_highlights(cls, action): + """Apply an action to the highlights of all views.""" + + def apply(view): + highlights = persist.highlights.get(view.id()) + + if highlights: + getattr(highlights, action)(view) + + util.apply_to_all_views(apply) + + @classmethod + def clear_all(cls): + """Clear highlights and errors in all views.""" + cls.apply_to_all_highlights('reset') + persist.errors.clear() + + @classmethod + def redraw_all(cls): + """Redraw all highlights in all views.""" + cls.apply_to_all_highlights('redraw') + + @classmethod + def text(cls, view): + """Return the entire text of a view.""" + return view.substr(sublime.Region(0, view.size())) + + @classmethod + def get_view(cls, vid): + """Return the view object with the given id.""" + return persist.views.get(vid) + + @classmethod + def get_linters(cls, vid): + """Return a tuple of linters for the view with the given id.""" + if vid in persist.view_linters: + return tuple(persist.view_linters[vid]) + + return () + + @classmethod + def get_selectors(cls, vid, syntax): + """ + Return scope selectors and linters for the view with the given id. + + For each linter assigned to the view with the given id, if it + has selectors, return a tuple of the selector and the linter. + + """ + selectors = [] + + for linter in cls.get_linters(vid): + if syntax in linter.selectors: + selectors.append((linter.selectors[syntax], linter)) + + if '*' in linter.selectors: + selectors.append((linter.selectors['*'], linter)) + + return selectors + + @classmethod + def lint_view(cls, view, filename, code, hit_time, callback): + """ + Lint the given view. + + This is the top level lint dispatcher. It is called + asynchronously. The following checks are done for each linter + assigned to the view: + + - Check if the linter has been disabled in settings. + - Check if the filename matches any patterns in the "excludes" setting. + + If a linter fails the checks, it is disabled for this run. + Otherwise, if the mapped syntax is not in the linter's selectors, + the linter is run on the entirety of code. + + Then the set of selectors for all linters assigned to the view is + aggregated, and for each selector, if it occurs in sections, + the corresponding section is linted as embedded code. + + A list of the linters that ran is returned. + + """ + + if not code: + return + + vid = view.id() + linters = persist.view_linters.get(vid) + + if not linters: + return + + disabled = set() + syntax = persist.get_syntax(persist.views[vid]) + + for linter in linters: + # First check to see if the linter can run in the current lint mode. + if linter.tempfile_suffix == '-' and view.is_dirty(): + disabled.add(linter) + continue + + # Because get_view_settings is expensive, we use an lru_cache + # to cache its results. Before each lint, reset the cache. + linter.clear_settings_caches() + view_settings = linter.get_view_settings(no_inline=True) + + # We compile the ignore matches for a linter on each run, + # clear the cache first. + linter.ignore_matches = None + + if view_settings.get('@disable'): + disabled.add(linter) + continue + + if filename: + filename = os.path.realpath(filename) + excludes = util.convert_type(view_settings.get('excludes', []), []) + + if excludes: + matched = False + + for pattern in excludes: + if fnmatch(filename, pattern): + persist.debug( + '{} skipped \'{}\', excluded by \'{}\'' + .format(linter.name, filename, pattern) + ) + matched = True + break + + if matched: + disabled.add(linter) + continue + + if syntax not in linter.selectors and '*' not in linter.selectors: + linter.reset(code, view_settings) + linter.lint(hit_time) + + selectors = Linter.get_selectors(vid, syntax) + + for selector, linter in selectors: + if linter in disabled: + continue + + linters.add(linter) + regions = [] + + for region in view.find_by_selector(selector): + regions.append(region) + + linter.reset(code, view_settings) + errors = {} + + for region in regions: + line_offset, col = view.rowcol(region.begin()) + linter.highlight.move_to(line_offset, col) + linter.code = code[region.begin():region.end()] + linter.errors = {} + linter.lint(hit_time) + + for line, line_errors in linter.errors.items(): + errors[line + line_offset] = line_errors + + linter.errors = errors + + # Remove disabled linters + linters = list(linters - disabled) + + # Merge our result back to the main thread + callback(cls.get_view(vid), linters, hit_time) + + def compile_ignore_match(self, pattern): + """Return the compiled pattern, log the error if compilation fails.""" + try: + return re.compile(pattern) + except re.error as err: + persist.printf( + 'ERROR: {}: invalid ignore_match: "{}" ({})' + .format(self.name, pattern, str(err)) + ) + return None + + def compiled_ignore_matches(self, ignore_match): + """ + Compile the "ignore_match" linter setting as an optimization. + + If it's a string, return a list with a single compiled regex. + If it's a list, return a list of the compiled regexes. + If it's a dict, return a list only of the regexes whose key + matches the file's extension. + + """ + + if isinstance(ignore_match, str): + regex = self.compile_ignore_match(ignore_match) + return [regex] if regex else [] + + elif isinstance(ignore_match, list): + matches = [] + + for match in ignore_match: + regex = self.compile_ignore_match(match) + + if regex: + matches.append(regex) + + return matches + + elif isinstance(ignore_match, dict): + if not self.filename: + return [] + + ext = os.path.splitext(self.filename)[1].lower() + + if not ext: + return [] + + # Try to match the extension, then the extension without the dot + ignore_match = ignore_match.get(ext, ignore_match.get(ext[1:])) + + if ignore_match: + return self.compiled_ignore_matches(ignore_match) + else: + return [] + + else: + return [] + + def reset(self, code, settings): + """Reset a linter to work on the given code and filename.""" + self.errors = {} + self.code = code + self.highlight = highlight.Highlight(self.code) + + if self.ignore_matches is None: + ignore_match = settings.get('ignore_match') + + if ignore_match: + self.ignore_matches = self.compiled_ignore_matches(ignore_match) + else: + self.ignore_matches = [] + + @classmethod + def which(cls, cmd): + """Call util.which with this class' module and return the result.""" + return util.which(cmd, module=getattr(cls, 'module', None)) + + def get_cmd(self): + """ + Calculate and return a tuple/list of the command line to be executed. + + The cmd class attribute may be a string, a tuple/list, or a callable. + If cmd is callable, it is called. If the result of the method is + a string, it is parsed into a list with shlex.split. + + Otherwise the result of build_cmd is returned. + + """ + if callable(self.cmd): + cmd = self.cmd() + + if isinstance(cmd, str): + cmd = shlex.split(cmd) + + return self.insert_args(cmd) + else: + return self.build_cmd() + + def build_cmd(self, cmd=None): + """ + Return a tuple with the command line to execute. + + We start with cmd or the cmd class attribute. If it is a string, + it is parsed with shlex.split. + + If the first element of the command line matches [script]@python[version], + and '@python' is in the aggregated view settings, util.which is called + to determine the path to the script and given version of python. This + allows settings to override the version of python used. + + Otherwise, if self.executable_path has already been calculated, that + is used. If not, the executable path is located with util.which. + + If the path to the executable can be determined, a list of extra arguments + is built with build_args. If the cmd contains '*', it is replaced + with the extra argument list, otherwise the extra args are appended to + cmd. + + """ + + cmd = cmd or self.cmd + + if isinstance(cmd, str): + cmd = shlex.split(cmd) + else: + cmd = list(cmd) + + which = cmd[0] + have_path, path = self.context_sensitive_executable_path(cmd) + + if have_path: + # Returning None means the linter runs code internally + if path == '': + return None + elif self.executable_path: + path = self.executable_path + + if isinstance(path, (list, tuple)) and None in path: + path = None + else: + path = self.which(which) + + if not path: + persist.printf('ERROR: {} cannot locate \'{}\''.format(self.name, which)) + return '' + + cmd[0:1] = util.convert_type(path, []) + return self.insert_args(cmd) + + def context_sensitive_executable_path(self, cmd): + """ + Calculate the context-sensitive executable path, return a tuple of (have_path, path). + + Subclasses may override this to return a special path. + + """ + return False, None + + def insert_args(self, cmd): + """Insert user arguments into cmd and return the result.""" + args = self.build_args(self.get_view_settings()) + cmd = list(cmd) + + if '*' in cmd: + i = cmd.index('*') + + if args: + cmd[i:i + 1] = args + else: + cmd.pop(i) + else: + cmd += args + + return cmd + + def get_user_args(self, settings=None): + """Return any args the user specifies in settings as a list.""" + + if settings is None: + settings = self.get_merged_settings() + + args = settings.get('args', []) + + if isinstance(args, str): + args = shlex.split(args) + else: + args = args[:] + + return args + + def build_args(self, settings): + """ + Return a list of args to add to cls.cmd. + + First any args specified in the "args" linter setting are retrieved. + Then the args map (built by map_args during class construction) is + iterated. For each item in the args map: + + - Check to see if the arg is in settings, which is the aggregated + default/user/view settings. If arg is not in settings or is a meta + setting (beginning with '@'), it is skipped. + + - If the arg has no prefix, it is skipped. + + - Get the setting value. If it is None or an empty string/list, skip this arg. + + - If the setting value is a non-empty list and the arg was specified + as taking a single list of values, join the values. + + - If the setting value is a non-empty string or the boolean True, + convert it into a single-element list with that value. + + Once a list of values is built, iterate over the values to build + the args list: + + - Start with the prefix and arg name. + - If the joiner is '=', join '=' and the value and append to the args. + - If the joiner is ':', append the arg and value as separate args. + + Finally, if the config_file attribute is set and the user has not + set the config_file arg in the linter's "args" setting, try to + locate the config file and if found add the config file arg. + + Return the arg list. + + """ + + args = self.get_user_args(settings) + args_map = getattr(self, 'args_map', {}) + + for setting, arg_info in args_map.items(): + prefix = arg_info['prefix'] + + if setting not in settings or setting[0] == '@' or prefix is None: + continue + + values = settings[setting] + + if values is None: + continue + elif isinstance(values, (list, tuple)): + if values: + # If the values can be passed as a single list, join them now + if arg_info['sep'] and not arg_info['multiple']: + values = [str(value) for value in values] + values = [arg_info['sep'].join(values)] + else: + continue + elif isinstance(values, str): + if values: + values = [values] + else: + continue + elif isinstance(values, Number): + if values is False: + continue + else: + values = [values] + else: + # Unknown type + continue + + for value in values: + if prefix == '@': + args.append(str(value)) + else: + arg = prefix + arg_info['name'] + joiner = arg_info['joiner'] + + if joiner == '=': + args.append('{}={}'.format(arg, value)) + elif joiner == ':': + args.append(arg) + args.append(str(value)) + + if self.config_file: + if self.config_file[0] not in args and self.filename: + config = util.find_file( + os.path.dirname(self.filename), + self.config_file[1], + aux_dirs=self.config_file[2:] + ) + + if config: + args += [self.config_file[0], config] + + return args + + def build_options(self, options, type_map, transform=None): + """ + Build a list of options to be passed directly to a linting method. + + This method is designed for use with linters that do linting directly + in code and need to pass a dict of options. + + options is the starting dict of options. For each of the settings + listed in self.args_map: + + - See if the setting name is in view settings. + + - If so, and the value is non-empty, see if the setting + name is in type_map. If so, convert the value to the type + of the value in type_map. + + - If transform is not None, pass the name to it and assign to the result. + + - Add the name/value pair to options. + + """ + + view_settings = self.get_view_settings() + + for name, info in self.args_map.items(): + value = view_settings.get(name) + + if value: + value = util.convert_type(value, type_map.get(name), sep=info.get('sep')) + + if value is not None: + if transform: + name = transform(name) + + options[name] = value + + def lint(self, hit_time): + """ + Perform the lint, retrieve the results, and add marks to the view. + + The flow of control is as follows: + + - Get the command line. If it is an empty string, bail. + - Run the linter. + - If the view has been modified since the original hit_time, stop. + - Parse the linter output with the regex. + - Highlight warnings and errors. + + """ + + if self.disabled: + return + + if self.filename: + cwd = os.getcwd() + os.chdir(os.path.dirname(self.filename)) + + if self.cmd is None: + cmd = None + else: + cmd = self.get_cmd() + + if cmd is not None and not cmd: + return + + output = self.run(cmd, self.code) + + if self.filename: + os.chdir(cwd) + + if not output: + return + + # If the view has been modified since the lint was triggered, no point in continuing. + if hit_time is not None and persist.last_hit_times.get(self.view.id(), 0) > hit_time: + return + + if persist.debug_mode(): + stripped_output = output.replace('\r', '').rstrip() + persist.printf('{} output:\n{}'.format(self.name, stripped_output)) + + for match, line, col, error, warning, message, near in self.find_errors(output): + if match and message and line is not None: + if self.ignore_matches: + ignore = False + + for ignore_match in self.ignore_matches: + if ignore_match.match(message): + ignore = True + + if persist.debug_mode(): + persist.printf( + '{} ({}): ignore_match: "{}" == "{}"' + .format( + self.name, + os.path.basename(self.filename) or '', + ignore_match.pattern, + message + ) + ) + break + + if ignore: + continue + + if error: + error_type = highlight.ERROR + elif warning: + error_type = highlight.WARNING + else: + error_type = self.default_type + + if col is not None: + # Pin the column to the start/end line offsets + start, end = self.highlight.full_line(line) + col = max(min(col, (end - start) - 1), 0) + + # Adjust column numbers to match the linter's tabs if necessary + if self.tab_width > 1: + code_line = self.code[start:end] + diff = 0 + + for i in range(len(code_line)): + if code_line[i] == '\t': + diff += (self.tab_width - 1) + + if col - diff <= i: + col = i + break + + if col is not None: + self.highlight.range(line, col, near=near, error_type=error_type, word_re=self.word_re) + elif near: + col = self.highlight.near(line, near, error_type=error_type, word_re=self.word_re) + else: + if ( + persist.settings.get('no_column_highlights_line') or + persist.settings.get('gutter_theme') == 'none' + ): + pos = -1 + else: + pos = 0 + + self.highlight.range(line, pos, length=0, error_type=error_type, word_re=self.word_re) + + self.error(line, col, message, error_type) + + def draw(self): + """Draw the marks from the last lint.""" + self.highlight.draw(self.view) + + @staticmethod + def clear_view(view): + """Clear marks, status and all other cached error info for the given view.""" + + view.erase_status('sublimelinter') + highlight.Highlight.clear(view) + + if view.id() in persist.errors: + del persist.errors[view.id()] + + def clear(self): + """Clear marks, status and all other cached error info for the given view.""" + self.clear_view(self.view) + + # Helper methods + + @classmethod + @lru_cache(maxsize=None) + def can_lint(cls, syntax): + """ + Determine if a linter class can lint the given syntax. + + This method is called when a view has not had a linter assigned + or when its syntax changes. + + The following tests must all pass for this method to return True: + + 1. syntax must match one of the syntaxes the linter defines. + 2. If the linter uses an external executable, it must be available. + 3. If there is a version requirement and the executable is available, + its version must fulfill the requirement. + 4. can_lint_syntax must return True. + + """ + + can = False + syntax = syntax.lower() + + if cls.syntax: + if isinstance(cls.syntax, (tuple, list)): + can = syntax in cls.syntax + elif cls.syntax == '*': + can = True + elif isinstance(cls.syntax, str): + can = syntax == cls.syntax + else: + can = cls.syntax.match(syntax) is not None + + if can: + if cls.executable_path is None: + executable = '' + + if not callable(cls.cmd): + if isinstance(cls.cmd, (tuple, list)): + executable = (cls.cmd or [''])[0] + elif isinstance(cls.cmd, str): + executable = cls.cmd + + if not executable and cls.executable: + executable = cls.executable + + if executable: + cls.executable_path = cls.which(executable) or '' + + if ( + cls.executable_path is None or + (isinstance(cls.executable_path, (tuple, list)) and None in cls.executable_path) + ): + cls.executable_path = '' + elif cls.cmd is None: + cls.executable_path = '' + else: + cls.executable_path = '' + + status = None + + if cls.executable_path: + can = cls.fulfills_version_requirement() + + if not can: + status = '' # Warning was already printed + + if can: + can = cls.can_lint_syntax(syntax) + + if can: + settings = persist.settings + disabled = ( + settings.get('@disabled') or + settings.get('linters', {}).get(cls.name, {}).get('@disable', False) + ) + status = '{} activated: {}{}'.format( + cls.name, + cls.executable_path, + ' (disabled in settings)' if disabled else '' + ) + elif status is None: + status = 'WARNING: {} deactivated, cannot locate \'{}\''.format(cls.name, executable) + + if status: + persist.printf(status) + + return can + + @classmethod + def can_lint_syntax(cls, syntax): + """ + Return whether a linter can lint a given syntax. + + Subclasses may override this if the built in mechanism in can_lint + is not sufficient. When this method is called, cls.executable_path + has been set. If it is '', that means the executable was not specified + or could not be found. + + """ + return cls.executable_path != '' + + @classmethod + def fulfills_version_requirement(cls): + """ + Return whether the executable fulfills version_requirement. + + When this is called, cls.executable_path has been set. + + """ + + cls.executable_version = None + + if cls.executable_path == '': + if callable(getattr(cls, 'get_module_version', None)): + if not(cls.version_re and cls.version_requirement): + return True + + cls.executable_version = cls.get_module_version() + + if cls.executable_version: + persist.debug('{} version: {}'.format(cls.name, cls.executable_version)) + else: + persist.printf('WARNING: {} unable to determine module version'.format(cls.name)) + else: + return True + elif not(cls.version_args is not None and cls.version_re and cls.version_requirement): + return True + + if cls.executable_version is None: + cls.executable_version = cls.get_executable_version() + + if cls.executable_version: + predicate = VersionPredicate( + '{} ({})'.format(cls.name.replace('-', '.'), cls.version_requirement) + ) + + if predicate.satisfied_by(cls.executable_version): + persist.debug( + '{}: ({}) satisfied by {}' + .format(cls.name, cls.version_requirement, cls.executable_version) + ) + return True + else: + persist.printf( + 'WARNING: {} deactivated, version requirement ({}) not fulfilled by {}' + .format(cls.name, cls.version_requirement, cls.executable_version) + ) + + return False + + @classmethod + def get_executable_version(cls): + """Extract and return the string version of the linter executable.""" + + args = cls.version_args + + if isinstance(args, str): + args = shlex.split(args) + else: + args = list(args) + + if isinstance(cls.executable_path, str): + cmd = [cls.executable_path] + else: + cmd = list(cls.executable_path) + + cmd += args + persist.debug('{} version query: {}'.format(cls.name, ' '.join(cmd))) + + version = util.communicate(cmd, output_stream=util.STREAM_BOTH) + match = cls.version_re.search(version) + + if match: + version = match.group('version') + persist.debug('{} version: {}'.format(cls.name, version)) + return version + else: + persist.printf('WARNING: no {} version could be extracted from:\n{}'.format(cls.name, version)) + return None + + @staticmethod + def replace_entity(match): + """Return the character corresponding to an HTML entity.""" + number = match.group(2) + + if number: + hex = match.group(1) is not None + result = chr(int(number, 16 if hex else 10)) + else: + entity = match.group(3) + result = unescape(entity, html.entities.html5) + + return result + + def error(self, line, col, message, error_type): + """Add a reference to an error/warning on the given line and column.""" + self.highlight.line(line, error_type) + + # Some linters use html entities in error messages, decode them + message = HTML_ENTITY_RE.sub(self.replace_entity, message) + + # Strip trailing CR, space and period + message = ((col or 0), str(message).rstrip('\r .')) + + if line in self.errors: + self.errors[line].append(message) + else: + self.errors[line] = [message] + + def find_errors(self, output): + """ + A generator which matches the linter's regex against the linter output. + + If multiline is True, split_match is called for each non-overlapping + match of self.regex. If False, split_match is called for each line + in output. + + """ + + if self.multiline: + errors = self.regex.finditer(output) + + if errors: + for error in errors: + yield self.split_match(error) + else: + yield self.split_match(None) + else: + for line in output.splitlines(): + yield self.split_match(self.regex.match(line.rstrip())) + + def split_match(self, match): + """ + Split a match into the standard elements of an error and return them. + + If subclasses need to modify the values returned by the regex, they + should override this method, call super(), then modify the values + and return them. + + """ + + if match: + items = {'line': None, 'col': None, 'error': None, 'warning': None, 'message': '', 'near': None} + items.update(match.groupdict()) + line, col, error, warning, message, near = [ + items[k] for k in ('line', 'col', 'error', 'warning', 'message', 'near') + ] + + if line is not None: + line = int(line) - self.line_col_base[0] + + if col is not None: + if col.isdigit(): + col = int(col) - self.line_col_base[1] + else: + col = len(col) + + return match, line, col, error, warning, message, near + else: + return match, None, None, None, None, '', None + + def run(self, cmd, code): + """ + Execute the linter's executable or built in code and return its output. + + If a linter uses built in code, it should override this method and return + a string as the output. + + If a linter needs to do complicated setup or will use the tmpdir + method, it will need to override this method. + + """ + if persist.debug_mode(): + persist.printf('{}: {} {}'.format(self.name, + os.path.basename(self.filename or ''), + cmd or '')) + + if self.tempfile_suffix: + if self.tempfile_suffix != '-': + return self.tmpfile(cmd, code, suffix=self.get_tempfile_suffix()) + else: + return self.communicate(cmd) + else: + return self.communicate(cmd, code) + + def get_tempfile_suffix(self): + """Return the mapped tempfile_suffix.""" + if self.tempfile_suffix: + if isinstance(self.tempfile_suffix, dict): + suffix = self.tempfile_suffix.get(persist.get_syntax(self.view), self.syntax) + else: + suffix = self.tempfile_suffix + + if suffix and suffix[0] != '.': + suffix = '.' + suffix + + return suffix + else: + return '' + + # popen wrappers + + def communicate(self, cmd, code=''): + """Run an external executable using stdin to pass code and return its output.""" + if '@' in cmd: + cmd[cmd.index('@')] = self.filename + elif not code: + cmd.append(self.filename) + + return util.communicate( + cmd, + code, + output_stream=self.error_stream, + env=self.env) + + def tmpfile(self, cmd, code, suffix=''): + """Run an external executable using a temp file to pass code and return its output.""" + return util.tmpfile( + cmd, + code, + suffix or self.get_tempfile_suffix(), + output_stream=self.error_stream, + env=self.env) + + def tmpdir(self, cmd, files, code): + """Run an external executable using a temp dir filled with files and return its output.""" + return util.tmpdir( + cmd, + files, + self.filename, + code, + output_stream=self.error_stream, + env=self.env) + + def popen(self, cmd, env=None): + """Run cmd in a subprocess with the given environment and return the output.""" + return util.popen( + cmd, + env=env, + extra_env=self.env, + output_stream=self.error_stream) diff --git a/Packages/SublimeLinter/lint/persist.py b/Packages/SublimeLinter/lint/persist.py new file mode 100644 index 0000000..6498d33 --- /dev/null +++ b/Packages/SublimeLinter/lint/persist.py @@ -0,0 +1,465 @@ +# +# persist.py +# Part of SublimeLinter3, a code checking framework for Sublime Text 3 +# +# Written by Ryan Hileman and Aparajita Fishman +# +# Project: https://github.com/SublimeLinter/SublimeLinter3 +# License: MIT +# + +"""This module provides persistent global storage for other modules.""" + +from collections import defaultdict +from copy import deepcopy +import json +import os +import re +import sublime +import sys + +from . import util + +PLUGIN_NAME = 'SublimeLinter' + +# Get the name of the plugin directory, which is the parent of this file's directory +PLUGIN_DIRECTORY = os.path.basename(os.path.dirname(os.path.dirname(__file__))) + +LINT_MODES = ( + ('background', 'Lint whenever the text is modified'), + ('load/save', 'Lint only when a file is loaded or saved'), + ('save only', 'Lint only when a file is saved'), + ('manual', 'Lint only when requested') +) + +SYNTAX_RE = re.compile(r'(?i)/([^/]+)\.tmLanguage$') + +DEFAULT_GUTTER_THEME_PATH = 'Packages/SublimeLinter/gutter-themes/Default/Default.gutter-theme' + + +class Settings: + + """This class provides global access to and management of plugin settings.""" + + def __init__(self): + self.settings = {} + self.previous_settings = {} + self.changeset = set() + self.plugin_settings = None + self.on_update_callback = None + + def load(self, force=False): + """Load the plugin settings.""" + if force or not self.settings: + self.observe() + self.on_update() + self.observe_prefs() + + def has_setting(self, setting): + """Return whether the given setting exists.""" + return setting in self.settings + + def get(self, setting, default=None): + """Return a plugin setting, defaulting to default if not found.""" + return self.settings.get(setting, default) + + def set(self, setting, value, changed=False): + """ + Set a plugin setting to the given value. + + Clients of this module should always call this method to set a value + instead of doing settings['foo'] = 'bar'. + + If the caller knows for certain that the value has changed, + they should pass changed=True. + + """ + self.copy() + self.settings[setting] = value + + if changed: + self.changeset.add(setting) + + def pop(self, setting, default=None): + """ + Remove a given setting and return default if it is not in self.settings. + + Clients of this module should always call this method to pop a value + instead of doing settings.pop('foo'). + + """ + self.copy() + return self.settings.pop(setting, default) + + def copy(self): + """Save a copy of the plugin settings.""" + self.previous_settings = deepcopy(self.settings) + + def observe_prefs(self, observer=None): + """Observe changes to the ST prefs.""" + prefs = sublime.load_settings('Preferences.sublime-settings') + prefs.clear_on_change('sublimelinter-pref-settings') + prefs.add_on_change('sublimelinter-pref-settings', observer or self.on_prefs_update) + + def observe(self, observer=None): + """Observer changes to the plugin settings.""" + self.plugin_settings = sublime.load_settings('SublimeLinter.sublime-settings') + self.plugin_settings.clear_on_change('sublimelinter-persist-settings') + self.plugin_settings.add_on_change('sublimelinter-persist-settings', + observer or self.on_update) + + def on_update_call(self, callback): + """Set a callback to call when user settings are updated.""" + self.on_update_callback = callback + + def on_update(self): + """ + Update state when the user settings change. + + The settings before the change are compared with the new settings. + Depending on what changes, views will either be redrawn or relinted. + + """ + + settings = util.merge_user_settings(self.plugin_settings) + self.settings.clear() + self.settings.update(settings) + + if ( + '@disable' in self.changeset or + self.previous_settings.get('@disable', False) != self.settings.get('@disable', False) + ): + need_relint = True + self.changeset.discard('@disable') + else: + need_relint = False + + # Clear the path-related caches if the paths list has changed + if ( + 'paths' in self.changeset or + (self.previous_settings and + self.previous_settings.get('paths') != self.settings.get('paths')) + ): + need_relint = True + util.clear_caches() + self.changeset.discard('paths') + + # Add python paths if they changed + if ( + 'python_paths' in self.changeset or + (self.previous_settings and + self.previous_settings.get('python_paths') != self.settings.get('python_paths')) + ): + need_relint = True + self.changeset.discard('python_paths') + python_paths = self.settings.get('python_paths', {}).get(sublime.platform(), []) + + for path in python_paths: + if path not in sys.path: + sys.path.append(path) + + # If the syntax map changed, reassign linters to all views + from .linter import Linter + + if ( + 'syntax_map' in self.changeset or + (self.previous_settings and + self.previous_settings.get('syntax_map') != self.settings.get('syntax_map')) + ): + need_relint = True + self.changeset.discard('syntax_map') + Linter.clear_all() + util.apply_to_all_views(lambda view: Linter.assign(view, reset=True)) + + if ( + 'no_column_highlights_line' in self.changeset or + self.previous_settings.get('no_column_highlights_line') != self.settings.get('no_column_highlights_line') + ): + need_relint = True + self.changeset.discard('no_column_highlights_line') + + if ( + 'gutter_theme' in self.changeset or + self.previous_settings.get('gutter_theme') != self.settings.get('gutter_theme') + ): + self.changeset.discard('gutter_theme') + self.update_gutter_marks() + + error_color = self.settings.get('error_color', '') + warning_color = self.settings.get('warning_color', '') + + if ( + ('error_color' in self.changeset or 'warning_color' in self.changeset) or + (self.previous_settings and error_color and warning_color and + (self.previous_settings.get('error_color') != error_color or + self.previous_settings.get('warning_color') != warning_color)) + ): + self.changeset.discard('error_color') + self.changeset.discard('warning_color') + + if ( + sublime.ok_cancel_dialog( + 'You changed the error and/or warning color. ' + 'Would you like to update the user color schemes ' + 'with the new colors?') + ): + util.change_mark_colors(error_color, warning_color) + + # If any other settings changed, relint + if (self.previous_settings or len(self.changeset) > 0): + need_relint = True + + self.changeset.clear() + + if need_relint: + Linter.reload() + + if self.previous_settings and self.on_update_callback: + self.on_update_callback(need_relint) + + def save(self, view=None): + """ + Regenerate and save the user settings. + + User settings are updated with the default settings and the defaults + from every linter, and if the user settings are currently being edited, + the view is updated. + + """ + + self.load() + + # Fill in default linter settings + settings = self.settings + linters = settings.pop('linters', {}) + + for name, linter in linter_classes.items(): + default = linter.settings().copy() + default.update(linters.pop(name, {})) + + for key, value in (('@disable', False), ('args', []), ('excludes', [])): + if key not in default: + default[key] = value + + linters[name] = default + + settings['linters'] = linters + + filename = '{}.sublime-settings'.format(PLUGIN_NAME) + user_prefs_path = os.path.join(sublime.packages_path(), 'User', filename) + settings_views = [] + + if view is None: + # See if any open views are the user prefs + for window in sublime.windows(): + for view in window.views(): + if view.file_name() == user_prefs_path: + settings_views.append(view) + else: + settings_views = [view] + + if settings_views: + def replace(edit): + if not view.is_dirty(): + j = json.dumps({'user': settings}, indent=4, sort_keys=True) + j = j.replace(' \n', '\n') + view.replace(edit, sublime.Region(0, view.size()), j) + + for view in settings_views: + edits[view.id()].append(replace) + view.run_command('sublimelinter_edit') + view.run_command('save') + else: + user_settings = sublime.load_settings('SublimeLinter.sublime-settings') + user_settings.set('user', settings) + sublime.save_settings('SublimeLinter.sublime-settings') + + def on_prefs_update(self): + """Perform maintenance when the ST prefs are updated.""" + util.generate_color_scheme() + + def update_gutter_marks(self): + """Update the gutter mark info based on the the current "gutter_theme" setting.""" + + theme_path = self.settings.get('gutter_theme', DEFAULT_GUTTER_THEME_PATH) + theme = os.path.splitext(os.path.basename(theme_path))[0] + + if theme_path.lower() == 'none': + gutter_marks['warning'] = gutter_marks['error'] = '' + return + + info = None + + for path in (theme_path, DEFAULT_GUTTER_THEME_PATH): + try: + info = sublime.load_resource(path) + break + except IOError: + pass + + if info is not None: + if theme != 'Default' and os.path.basename(path) == 'Default.gutter-theme': + printf('cannot find the gutter theme \'{}\', using the default'.format(theme)) + + path = os.path.dirname(path) + + for error_type in ('warning', 'error'): + icon_path = '{}/{}.png'.format(path, error_type) + gutter_marks[error_type] = icon_path + + try: + info = json.loads(info) + colorize = info.get('colorize', False) + except ValueError: + colorize = False + + gutter_marks['colorize'] = colorize + else: + sublime.error_message( + 'SublimeLinter: cannot find the gutter theme "{}",' + ' and the default is also not available. ' + 'No gutter marks will display.'.format(theme) + ) + gutter_marks['warning'] = gutter_marks['error'] = '' + + +if not 'queue' in globals(): + settings = Settings() + + # A mapping between view ids and errors, which are line:(col, message) dicts + errors = {} + + # A mapping between view ids and HighlightSets + highlights = {} + + # A mapping between linter class names and linter classes + linter_classes = {} + + # A mapping between view ids and a set of linter instances + view_linters = {} + + # A mapping between view ids and views + views = {} + + # Every time a view is modified, this is updated with a mapping between a view id + # and the time of the modification. This is checked at various stages of the linting + # process. If a view has been modified since the original modification, the + # linting process stops. + last_hit_times = {} + + edits = defaultdict(list) + + # Info about the gutter mark icons + gutter_marks = {'warning': 'Default', 'error': 'Default', 'colorize': True} + + # Whether sys.path has been imported from the system. + sys_path_imported = False + + # Set to true when the plugin is loaded at startup + plugin_is_loaded = False + + +def get_syntax(view): + """Return the view's syntax or the syntax it is mapped to in the "syntax_map" setting.""" + view_syntax = view.settings().get('syntax', '') + mapped_syntax = '' + + if view_syntax: + match = SYNTAX_RE.search(view_syntax) + + if match: + view_syntax = match.group(1).lower() + mapped_syntax = settings.get('syntax_map', {}).get(view_syntax, '').lower() + else: + view_syntax = '' + + return mapped_syntax or view_syntax + + +def edit(vid, edit): + """Perform an operation on a view with the given edit object.""" + callbacks = edits.pop(vid, []) + + for c in callbacks: + c(edit) + + +def view_did_close(vid): + """Remove all references to the given view id in persistent storage.""" + if vid in errors: + del errors[vid] + + if vid in highlights: + del highlights[vid] + + if vid in view_linters: + del view_linters[vid] + + if vid in views: + del views[vid] + + if vid in last_hit_times: + del last_hit_times[vid] + + +def debug_mode(): + """Return whether the "debug" setting is True.""" + return settings.get('debug') + + +def debug(*args): + """Print args to the console if the "debug" setting is True.""" + if settings.get('debug'): + printf(*args) + + +def printf(*args): + """Print args to the console, prefixed by the plugin name.""" + print(PLUGIN_NAME + ': ', end='') + + for arg in args: + print(arg, end=' ') + + print() + + +def import_sys_path(): + """Import system python 3 sys.path into our sys.path.""" + global sys_path_imported + + if plugin_is_loaded and not sys_path_imported: + # Make sure the system python 3 paths are available to plugins. + # We do this here to ensure it is only done once. + sys.path.extend(util.get_python_paths()) + sys_path_imported = True + + +def register_linter(linter_class, name, attrs): + """Add a linter class to our mapping of class names <--> linter classes.""" + if name: + name = name.lower() + linter_classes[name] = linter_class + + # By setting the lint_settings to None, they will be set the next + # time linter_class.settings() is called. + linter_class.lint_settings = None + + # The sublime plugin API is not available until plugin_loaded is executed + if plugin_is_loaded: + settings.load(force=True) + + # If a linter is reloaded, we have to reassign that linter to all views + from . import linter + + # If the linter had previously been loaded, just reassign that linter + if name in linter_classes: + linter_name = name + else: + linter_name = None + + for view in views.values(): + linter.Linter.assign(view, linter_name=linter_name) + + printf('{} linter reloaded'.format(name)) + else: + printf('{} linter loaded'.format(name)) diff --git a/Packages/SublimeLinter/lint/python_linter.py b/Packages/SublimeLinter/lint/python_linter.py new file mode 100644 index 0000000..55ad4cc --- /dev/null +++ b/Packages/SublimeLinter/lint/python_linter.py @@ -0,0 +1,325 @@ +# +# python_linter.py +# Part of SublimeLinter3, a code checking framework for Sublime Text 3 +# +# Written by Aparajita Fishman +# +# Project: https://github.com/SublimeLinter/SublimeLinter3 +# License: MIT +# + +"""This module exports the PythonLinter subclass of Linter.""" + +import importlib +import os +import re + +from . import linter, persist, util + + +class PythonLinter(linter.Linter): + + """ + This Linter subclass provides python-specific functionality. + + Linters that check python should inherit from this class. + By doing so, they automatically get the following features: + + - comment_re is defined correctly for python. + + - A python shebang is returned as the @python: meta setting. + + - Execution directly via a module method or via an executable. + + If the module attribute is defined and is successfully imported, + whether it is used depends on the following algorithm: + + - If the cmd attribute specifies @python and ST's python + satisfies that version, the module will be used. Note that this + check is done during class construction. + + - If the check_version attribute is False, the module will be used + because the module is not version-sensitive. + + - If the "@python" setting is set and ST's python satisfies + that version, the module will be used. + + - Otherwise the executable will be used with the python specified + in the "@python" setting, the cmd attribute, or the default system + python. + + """ + + SHEBANG_RE = re.compile(r'\s*#!(?:(?:/[^/]+)*[/ ])?python(?P\d(?:\.\d)?)') + + comment_re = r'\s*#' + + # If the linter wants to import a module and run a method directly, + # it should set this attribute to the module name, suitable for passing + # to importlib.import_module. During class construction, the named module + # will be imported, and if successful, the attribute will be replaced + # with the imported module. + module = None + + # Some python-based linters are version-sensitive, i.e. the python version + # they are run with has to match the version of the code they lint. + # If a linter is version-sensitive, this attribute should be set to True. + check_version = False + + @staticmethod + def match_shebang(code): + """Convert and return a python shebang as a @python: setting.""" + + match = PythonLinter.SHEBANG_RE.match(code) + + if match: + return '@python', match.group('version') + else: + return None + + shebang_match = match_shebang + + @classmethod + def initialize(cls): + """Perform class-level initialization.""" + + super().initialize() + persist.import_sys_path() + cls.import_module() + + @classmethod + def reinitialize(cls): + """Perform class-level initialization after plugins have been loaded at startup.""" + + # Be sure to clear _cmd so that import_module will re-import. + if hasattr(cls, '_cmd'): + delattr(cls, '_cmd') + + cls.initialize() + + @classmethod + def import_module(cls): + """ + Attempt to import the configured module. + + If it could not be imported, use the executable. + + """ + + if hasattr(cls, '_cmd'): + return + + module = getattr(cls, 'module', None) + cls._cmd = None + cmd = cls.cmd + script = None + + if isinstance(cls.cmd, (list, tuple)): + cmd = cls.cmd[0] + + if module is not None: + try: + module = importlib.import_module(module) + persist.debug('{} imported {}'.format(cls.name, module)) + + # If the linter specifies a python version, check to see + # if ST's python satisfies that version. + if cmd and not callable(cmd): + match = util.PYTHON_CMD_RE.match(cmd) + + if match and match.group('version'): + version, script = match.group('version', 'script') + version = util.find_python(version=version, script=script, module=module) + + # If we cannot find a python or script of the right version, + # we cannot use the module. + if version[0] is None or script and version[1] is None: + module = None + + except ImportError: + message = '{}import of {} module in {} failed' + + if cls.check_version: + warning = 'WARNING: ' + message += ', linter will not work with python 3 code' + else: + warning = '' + message += ', linter will not run using built in python' + + persist.printf(message.format(warning, module, cls.name)) + module = None + + except Exception as ex: + persist.printf( + 'ERROR: unknown exception in {}: {}' + .format(cls.name, str(ex)) + ) + module = None + + # If no module was specified, or the module could not be imported, + # or ST's python does not satisfy the version specified, see if + # any version of python available satisfies the linter. If not, + # set the cmd to '' to disable the linter. + can_lint = True + + if not module and cmd and not callable(cmd): + match = util.PYTHON_CMD_RE.match(cmd) + + if match and match.group('version'): + can_lint = False + version, script = match.group('version', 'script') + version = util.find_python(version=version, script=script) + + if version[0] is not None and (not script or version[1] is not None): + can_lint = True + + if can_lint: + cls._cmd = cls.cmd + + # If there is a module, setting cmd to None tells us to + # use the check method. + if module: + cls.cmd = None + else: + persist.printf( + 'WARNING: {} deactivated, no available version of python{} satisfies {}' + .format( + cls.name, + ' or {}'.format(script) if script else '', + cmd + )) + + cls.disabled = True + + cls.module = module + + def context_sensitive_executable_path(self, cmd): + """ + Calculate the context-sensitive executable path, using @python and check_version. + + Return a tuple of (have_path, path). + + Return have_path == False if not self.check_version. + Return have_path == True if cmd is in [script]@python[version] form. + Return None for path if the desired version of python/script cannot be found. + Return '' for path if the built-in python should be used. + + """ + + if not self.check_version: + return False, None + + # Check to see if we have a @python command + match = util.PYTHON_CMD_RE.match(cmd[0]) + + if match: + settings = self.get_view_settings() + + if '@python' in settings: + script = match.group('script') or '' + which = '{}@python{}'.format(script, settings.get('@python')) + path = self.which(which) + + if path: + if path[0] == '': + return True, '' + elif path[0] is None or script and path[1] is None: + return True, None + + return True, path + + return False, None + + @classmethod + def get_module_version(cls): + """ + Return the string version of the imported module, without any prefix/suffix. + + This method handles the common case where a module (or one of its parents) + defines a __version__ string. For other cases, subclasses should override + this method and return the version string. + + """ + + if cls.module: + module = cls.module + + while True: + if isinstance(getattr(module, '__version__', None), str): + return module.__version__ + + if hasattr(module, '__package__'): + try: + module = importlib.import_module(module.__package__) + except ImportError: + return None + else: + return None + + def run(self, cmd, code): + """Run the module checker or executable on code and return the output.""" + if self.module is not None: + use_module = False + + if not self.check_version: + use_module = True + else: + settings = self.get_view_settings() + version = settings.get('@python') + + if version is None: + use_module = cmd is None or cmd[0] == '' + else: + version = util.find_python(version=version, module=self.module) + use_module = version[0] == '' + + if use_module: + if persist.debug_mode(): + persist.printf( + '{}: {} '.format( + self.name, + os.path.basename(self.filename or '') + ) + ) + + try: + errors = self.check(code, os.path.basename(self.filename or '')) + except Exception as err: + persist.printf( + 'ERROR: exception in {}.check: {}' + .format(self.name, str(err)) + ) + errors = '' + + if isinstance(errors, (tuple, list)): + return '\n'.join([str(e) for e in errors]) + else: + return errors + else: + cmd = self._cmd + else: + cmd = self.cmd or self._cmd + + cmd = self.build_cmd(cmd=cmd) + + if cmd: + return super().run(cmd, code) + else: + return '' + + def check(self, code, filename): + """ + Run a built-in check of code, returning errors. + + Subclasses that provide built in checking must override this method + and return a string with one more lines per error, an array of strings, + or an array of objects that can be converted to strings. + + """ + + persist.printf( + '{}: subclasses must override the PythonLinter.check method' + .format(self.name) + ) + + return '' diff --git a/Packages/SublimeLinter/lint/queue.py b/Packages/SublimeLinter/lint/queue.py new file mode 100644 index 0000000..48166fa --- /dev/null +++ b/Packages/SublimeLinter/lint/queue.py @@ -0,0 +1,134 @@ +# +# queue.py +# Part of SublimeLinter3, a code checking framework for Sublime Text 3 +# +# Written by Ryan Hileman and Aparajita Fishman +# +# Project: https://github.com/SublimeLinter/SublimeLinter3 +# License: MIT +# + +"""This module provides a threaded queue for lint requests.""" + +from queue import Queue, Empty +import threading +import traceback +import time + +from . import persist, util + + +class Daemon: + + """ + This class provides a threaded queue that dispatches lints. + + The following operations can be added to the queue: + + hit - Queue a lint for a given view + delay - Queue a delay for a number of milliseconds + reload - Indicates the main plugin was reloaded + + """ + + MIN_DELAY = 0.1 + running = False + callback = None + q = Queue() + last_runs = {} + + def start(self, callback): + """Start the daemon thread that runs loop.""" + self.callback = callback + + if self.running: + self.q.put('reload') + else: + self.running = True + threading.Thread(target=self.loop).start() + + def loop(self): + """Continually check the queue for new items and process them.""" + + last_runs = {} + + while True: + try: + try: + item = self.q.get(block=True, timeout=self.MIN_DELAY) + except Empty: + for view_id, (timestamp, delay) in last_runs.copy().items(): + # Lint the view if we have gone past the time + # at which the lint wants to run. + if time.monotonic() > timestamp + delay: + self.last_runs[view_id] = time.monotonic() + del last_runs[view_id] + self.lint(view_id, timestamp) + + continue + + if isinstance(item, tuple): + view_id, timestamp, delay = item + + if view_id in self.last_runs and timestamp < self.last_runs[view_id]: + continue + + last_runs[view_id] = timestamp, delay + + elif isinstance(item, (int, float)): + time.sleep(item) + + elif isinstance(item, str): + if item == 'reload': + persist.printf('daemon detected a reload') + self.last_runs.clear() + last_runs.clear() + else: + persist.printf('unknown message sent to daemon:', item) + except: + persist.printf('error in SublimeLinter daemon:') + persist.printf('-' * 20) + persist.printf(traceback.format_exc()) + persist.printf('-' * 20) + + def hit(self, view): + """Add a lint request to the queue, return the time at which the request was enqueued.""" + timestamp = time.monotonic() + self.q.put((view.id(), timestamp, self.get_delay(view))) + return timestamp + + def delay(self, milliseconds=100): + """Add a millisecond delay to the queue.""" + self.q.put(milliseconds / 1000.0) + + def lint(self, view_id, timestamp): + """ + Call back into the main plugin to lint the given view. + + timestamp is used to determine if the view has been modified + since the lint was requested. + + """ + self.callback(view_id, timestamp) + + def get_delay(self, view): + """ + Return the delay between a lint request and when it will be processed. + + If the lint mode is not background, there is no delay. Otherwise, if + a "delay" setting is not available in any of the settings, MIN_DELAY is used. + + """ + + if persist.settings.get('lint_mode') != 'background': + return 0 + + delay = (util.get_view_rc_settings(view) or {}).get('delay') + + if delay is None: + delay = persist.settings.get('delay', self.MIN_DELAY) + + return delay + + +queue = Daemon() diff --git a/Packages/SublimeLinter/lint/ruby_linter.py b/Packages/SublimeLinter/lint/ruby_linter.py new file mode 100644 index 0000000..c0d6a74 --- /dev/null +++ b/Packages/SublimeLinter/lint/ruby_linter.py @@ -0,0 +1,144 @@ +# +# ruby_linter.py +# Part of SublimeLinter3, a code checking framework for Sublime Text 3 +# +# Written by Aparajita Fishman +# +# Project: https://github.com/SublimeLinter/SublimeLinter3 +# License: MIT +# + +"""This module exports the RubyLinter subclass of Linter.""" + +import os +import re +import shlex + +from . import linter, persist, util + +CMD_RE = re.compile(r'(?P.+?)@ruby') + + +class RubyLinter(linter.Linter): + + """ + This Linter subclass provides ruby-specific functionality. + + Linters that check ruby using gems should inherit from this class. + By doing so, they automatically get the following features: + + - comment_re is defined correctly for ruby. + + - Support for rbenv and rvm (via rvm-auto-ruby). + + """ + + comment_re = r'\s*#' + + @classmethod + def initialize(cls): + """Perform class-level initialization.""" + + super().initialize() + + if cls.executable_path is not None: + return + + if not callable(cls.cmd) and cls.cmd: + cls.executable_path = cls.lookup_executables(cls.cmd) + elif cls.executable: + cls.executable_path = cls.lookup_executables(cls.executable) + + if not cls.executable_path: + cls.disabled = True + + @classmethod + def reinitialize(cls): + """Perform class-level initialization after plugins have been loaded at startup.""" + + # Be sure to clear cls.executable_path so that lookup_executables will run. + cls.executable_path = None + cls.initialize() + + @classmethod + def lookup_executables(cls, cmd): + """ + Attempt to locate the gem and ruby specified in cmd, return new cmd list. + + The following forms are valid: + + gem@ruby + gem + ruby + + If rbenv is installed and the gem is also under rbenv control, + the gem will be executed directly. Otherwise [ruby <, gem>] will + be returned. + + If rvm-auto-ruby is installed, [rvm-auto-ruby <, gem>] will be + returned. + + Otherwise [ruby] or [gem] will be returned. + + """ + + ruby = None + rbenv = util.which('rbenv') + + if not rbenv: + ruby = util.which('rvm-auto-ruby') + + if not ruby: + ruby = util.which('ruby') + + if not rbenv and not ruby: + persist.printf( + 'WARNING: {} deactivated, cannot locate ruby, rbenv or rvm-auto-ruby' + .format(cls.name, cmd[0]) + ) + return [] + + if isinstance(cmd, str): + cmd = shlex.split(cmd) + + match = CMD_RE.match(cmd[0]) + + if match: + gem = match.group('gem') + elif cmd[0] != 'ruby': + gem = cmd[0] + else: + gem = '' + + if gem: + gem_path = util.which(gem) + + if gem_path: + if (rbenv and + ('{0}.rbenv{0}shims{0}'.format(os.sep) in gem_path or + (os.altsep and '{0}.rbenv{0}shims{0}'.format(os.altsep in gem_path)))): + ruby_cmd = [gem_path] + else: + ruby_cmd = [ruby, gem_path] + else: + persist.printf( + 'WARNING: {} deactivated, cannot locate the gem \'{}\'' + .format(cls.name, gem) + ) + return [] + else: + ruby_cmd = [ruby] + + if cls.env is None: + # Don't use GEM_HOME with rbenv, it prevents it from using gem shims + if rbenv: + cls.env = {} + else: + gem_home = util.get_environment_variable('GEM_HOME') + + if gem_home: + cls.env = {'GEM_HOME': gem_home} + else: + cls.env = {} + + return ruby_cmd diff --git a/Packages/SublimeLinter/lint/util.py b/Packages/SublimeLinter/lint/util.py new file mode 100644 index 0000000..bcede76 --- /dev/null +++ b/Packages/SublimeLinter/lint/util.py @@ -0,0 +1,1416 @@ +# coding=utf8 +# +# util.py +# Part of SublimeLinter3, a code checking framework for Sublime Text 3 +# +# Written by Ryan Hileman and Aparajita Fishman +# +# Project: https://github.com/SublimeLinter/SublimeLinter3 +# License: MIT +# + +"""This module provides general utility methods.""" + +from functools import lru_cache +from glob import glob +import json +from numbers import Number +import os +import re +import shutil +from string import Template +import sublime +import subprocess +import sys +import tempfile +from xml.etree import ElementTree + +# +# Public constants +# +STREAM_STDOUT = 1 +STREAM_STDERR = 2 +STREAM_BOTH = STREAM_STDOUT + STREAM_STDERR + +PYTHON_CMD_RE = re.compile(r'(?P