Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

first commit

  • Loading branch information...
commit 2fea568a544d298d247ede953766533fc6b6e98f 0 parents
@trentm authored
2  .gitignore
@@ -0,0 +1,2 @@
+/tmp
+/node_modules
3  .npmignore
@@ -0,0 +1,3 @@
+/tmp
+/node_modules
+/tools
5 CHANGES.md
@@ -0,0 +1,5 @@
+# ansidiff Changelog
+
+## ansidiff 1.0.0 (not yet released)
+
+First release.
37 Makefile
@@ -0,0 +1,37 @@
+
+#---- Tools
+
+TAP := ./node_modules/.bin/tap
+
+
+#---- Files
+
+JSSTYLE_FILES := $(shell find lib test tools -name *.js)
+
+
+
+#---- Targets
+
+.PHONY: all
+all:
+
+.PHONY: cutarelease
+cutarelease:
+ [[ ! -d tmp ]] # No 'tmp/' allowed: https://github.com/isaacs/npm/issues/2144
+ ./tools/cutarelease.py -p ansidiff -f package.json
+
+.PHONY: test
+test: $(TAP)
+ TAP=1 $(TAP) test/*.test.js
+
+.PHONY: check-jsstyle
+check-jsstyle: $(JSSTYLE_FILES)
+ ./tools/jsstyle -f ./tools/jsstyle.conf $(JSSTYLE_FILES)
+
+.PHONY: check
+check: check-jsstyle
+ @echo "Check ok."
+
+.PHONY: prepush
+prepush: check test
+ @echo "Okay to push."
36 README.md
@@ -0,0 +1,36 @@
+ansidiff -- a small node.js library for ANSI colored text diffs
+
+
+# Usage
+
+ var ansidiff = require('ansidiff');
+ var log = console.log;
+ log( ansidiff.chars('will work for food', 'will code for foo') );
+ log( ansidiff.words('will work for food', 'will code for foo') );
+ log( ansidiff.lines('one\ntwo\nthree', 'one\ndeux\ntrois\nthree') );
+ log( ansidiff.css('#body { color: blue }', '.content { color: blue }') );
+
+These are using the default `bright` colorer. You can use the `subtle`
+one if you wish:
+
+ log( ansidiff.words('will work for food', 'will code for foo', ansidiff.subtle) );
+
+![ansi color diffs]()
+
+
+# Install
+
+ npm install ansidiff
+
+
+# License
+
+MIT.
+
+
+var ansidiff = require('ansidiff');
+var log = console.log;
+log( ansidiff.chars('will work for food', 'will code for foo') );
+log( ansidiff.words('will work for food', 'will code for foo') );
+log( ansidiff.lines('one\ntwo\nthree', 'one\ndeux\ntrois\nthree') );
+log( ansidiff.css('#body { color: blue }', '.content { color: blue }') );
BIN  examples.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
106 lib/ansidiff.js
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2012 Trent Mick. All rights reserved.
+ */
+
+var diff = require('diff');
+
+
+//---- colorer functions
+
+/**
+ * Bright coloring of a "diff object"
+ */
+function bright(obj) {
+ if (obj.added) {
+ return (
+ '\033[' + 7 + 'm' // inverse
+ + '\033[' + 32 + 'm' // green
+ + obj.value
+ + '\033[' + 39 + 'm'
+ + '\033[' + 27 + 'm'
+ );
+ } else if (obj.removed) {
+ return (
+ '\033[' + 7 + 'm' // inverse
+ + '\033[' + 31 + 'm' // red
+ + obj.value
+ + '\033[' + 39 + 'm'
+ + '\033[' + 27 + 'm'
+ );
+ } else {
+ return obj.value;
+ }
+}
+
+
+/**
+ * More subtle coloring of a "diff object". Just uses foreground coloring.
+ * A downside is that whitespace diffs are not colored.
+ */
+function subtle(obj) {
+ if (obj.added) {
+ return (
+ '\033[' + 32 + 'm' // green
+ + obj.value
+ + '\033[' + 39 + 'm'
+ );
+ } else if (obj.removed) {
+ return (
+ '\033[' + 31 + 'm' // red
+ + obj.value
+ + '\033[' + 39 + 'm'
+ );
+ } else {
+ return obj.value;
+ }
+}
+
+
+
+//---- diffing functions
+
+/**
+ * Return ANSI color coded diff of given texts `a` and `b`, diffing by char.
+ */
+function chars(a, b, colorer) {
+ var objs = diff.diffChars(a, b);
+ return objs.map(colorer || bright).join('');
+}
+
+/**
+ * Return ANSI color coded diff of given texts `a` and `b`, diffing by word.
+ */
+function words(a, b, colorer) {
+ var objs = diff.diffWords(a, b);
+ return objs.map(colorer || bright).join('');
+}
+
+/**
+ * Return ANSI color coded diff of given texts `a` and `b`, diffing by line.
+ */
+function lines(a, b, colorer) {
+ var objs = diff.diffLines(a, b);
+ return objs.map(colorer || bright).join('');
+}
+
+/**
+ * Return ANSI color coded diff of given texts `a` and `b`, diffing by line.
+ */
+function css(a, b, colorer) {
+ var objs = diff.diffCss(a, b);
+ return objs.map(colorer || bright).join('');
+}
+
+
+
+//---- Exports
+
+module.exports = {
+ chars: chars,
+ words: words,
+ lines: lines,
+ css: css,
+
+ bright: bright,
+ subtle: subtle
+}
20 package.json
@@ -0,0 +1,20 @@
+{
+ "name": "ansidiff",
+ "version": "1.0.0",
+ "description": "ANSI colored text diffs",
+ "main": "./lib/ansidiff.js",
+
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/trentm/node-ansidiff.git"
+ },
+ "engines": ["node >=0.4.0"],
+ "keywords": ["diff", "ansi", "color", "colour", "console"],
+
+ "dependencies": {
+ "diff": "1.0"
+ },
+ "devDependencies": {
+ "tap": "0.2"
+ }
+}
13 test/ansidiff.test.js
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2012 Trent Mick. All rights reserved.
+ *
+ * node-ansidiff test suite.
+ */
+
+var test = require('tap').test;
+var ansidiff = require('../lib/ansidiff');
+
+
+test('XXX', function (t) {
+ t.end();
+});
496 tools/cutarelease.py
@@ -0,0 +1,496 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2009-2012 Trent Mick
+
+"""cutarelease -- Cut a release of your project.
+
+A script that will help cut a release for a git-based project that follows
+a few conventions. It'll update your changelog (CHANGES.md), add a git
+tag, push those changes, update your version to the next patch level release
+and create a new changelog section for that new version.
+
+Conventions:
+- XXX
+"""
+
+__version_info__ = (1, 0, 4)
+__version__ = '.'.join(map(str, __version_info__))
+
+import sys
+import os
+from os.path import join, dirname, normpath, abspath, exists, basename, splitext
+from glob import glob
+import re
+import codecs
+import logging
+import optparse
+import json
+
+
+
+#---- globals and config
+
+log = logging.getLogger("cutarelease")
+
+class Error(Exception):
+ pass
+
+
+
+#---- main functionality
+
+def cutarelease(project_name, version_files, dry_run=False):
+ """Cut a release.
+
+ @param project_name {str}
+ @param version_files {list} List of paths to files holding the version
+ info for this project.
+
+ If none are given it attempts to guess the version file:
+ package.json or VERSION.txt or VERSION or $project_name.py
+ or lib/$project_name.py or $project_name.js or lib/$project_name.js.
+
+ The version file can be in one of the following forms:
+
+ - A .py file, in which case the file is expect to have a top-level
+ global called "__version_info__" as follows. [1]
+
+ __version_info__ = (0, 7, 6)
+
+ Note that I typically follow that with the following to get a
+ string version attribute on my modules:
+
+ __version__ = '.'.join(map(str, __version_info__))
+
+ - A .js file, in which case the file is expected to have a top-level
+ global called "VERSION" as follows:
+
+ ver VERSION = "1.2.3";
+
+ - A "package.json" file, typical of a node.js npm-using project.
+ The package.json file must have a "version" field.
+
+ - TODO: A simple version file whose only content is a "1.2.3"-style version
+ string.
+
+ [1]: This is a convention I tend to follow in my projects.
+ Granted it might not be your cup of tea. I should add support for
+ just `__version__ = "1.2.3"`. I'm open to other suggestions too.
+ """
+ if not version_files:
+ log.info("guessing version file")
+ candidates = [
+ "package.json",
+ "VERSION.txt",
+ "VERSION",
+ "%s.py" % project_name,
+ "lib/%s.py" % project_name,
+ "%s.js" % project_name,
+ "lib/%s.js" % project_name,
+ ]
+ for candidate in candidates:
+ if exists(candidate):
+ version_files = [candidate]
+ break
+ else:
+ raise Error("could not find a version file: specify its path or "
+ "add one of the following to your project: '%s'"
+ % "', '".join(candidates))
+ log.info("using '%s' as version file", version_files[0])
+
+ parsed_version_files = [_parse_version_file(f) for f in version_files]
+ version_file_type, version_info = parsed_version_files[0]
+ version = _version_from_version_info(version_info)
+
+ # Confirm
+ if not dry_run:
+ answer = query_yes_no("* * *\n"
+ "Are you sure you want cut a %s release?\n"
+ "This will involved commits and a push." % version,
+ default="no")
+ print "* * *"
+ if answer != "yes":
+ log.info("user abort")
+ return
+ log.info("cutting a %s release", version)
+
+ # Checks: Ensure there is a section in changes for this version.
+ changes_path = "CHANGES.md"
+ if not exists(changes_path):
+ raise Error("'%s' not found" % changes_path)
+ changes_txt = changes_txt_before = codecs.open(changes_path, 'r', 'utf-8').read()
+
+ changes_parser = re.compile(r'^##\s+(?:.*?\s+)?v?(?P<ver>[\d\.abc]+)'
+ r'(?P<nyr>\s+\(not yet released\))?'
+ r'(?P<body>.*?)(?=^##|\Z)', re.M | re.S)
+ changes_sections = changes_parser.findall(changes_txt)
+ try:
+ top_ver = changes_sections[0][0]
+ except IndexError:
+ raise Error("unexpected error parsing `%s': parsed=%r" % (
+ changes_path, changes_sections))
+ if top_ver != version:
+ raise Error("top section in `%s' is for "
+ "version %r, expected version %r: aborting"
+ % (changes_path, top_ver, version))
+ top_nyr = changes_sections[0][1].strip()
+ if not top_nyr:
+ answer = query_yes_no("\n* * *\n"
+ "The top section in `%s' doesn't have the expected\n"
+ "'(not yet released)' marker. Has this been released already?"
+ % changes_path, default="yes")
+ print "* * *"
+ if answer != "no":
+ log.info("abort")
+ return
+ top_body = changes_sections[0][2]
+ if top_body.strip() == "(nothing yet)":
+ raise Error("top section body is `(nothing yet)': it looks like "
+ "nothing has been added to this release")
+
+ # Commits to prepare release.
+ changes_txt = changes_txt.replace(" (not yet released)", "", 1)
+ if not dry_run and changes_txt != changes_txt_before:
+ log.info("prepare `%s' for release", changes_path)
+ f = codecs.open(changes_path, 'w', 'utf-8')
+ f.write(changes_txt)
+ f.close()
+ run('git commit %s -m "prepare for %s release"'
+ % (changes_path, version))
+
+ # Tag version and push.
+ curr_tags = set(t for t in _capture_stdout(["git", "tag", "-l"]).split('\n') if t)
+ if not dry_run and version not in curr_tags:
+ log.info("tag the release")
+ run('git tag -a "%s" -m "version %s"' % (version, version))
+ run('git push --tags')
+
+ # Optionally release.
+ if exists("package.json"):
+ answer = query_yes_no("\n* * *\nPublish to npm?", default="yes")
+ print "* * *"
+ if answer == "yes":
+ run('npm publish')
+ elif exists("setup.py"):
+ answer = query_yes_no("\n* * *\nPublish to pypi?", default="yes")
+ print "* * *"
+ if answer == "yes":
+ run("%spython setup.py sdist --formats zip upload"
+ % _setup_command_prefix())
+
+ # Commits to prepare for future dev and push.
+ # - update changelog file
+ next_version_info = _get_next_version_info(version_info)
+ next_version = _version_from_version_info(next_version_info)
+ log.info("prepare for future dev (version %s)", next_version)
+ marker = "## %s %s\n" % (project_name, version)
+ if marker not in changes_txt:
+ raise Error("couldn't find `%s' marker in `%s' "
+ "content: can't prep for subsequent dev" % (marker, changes_path))
+ changes_txt = changes_txt.replace("## %s %s\n" % (project_name, version),
+ "## %s %s (not yet released)\n\n(nothing yet)\n\n## %s %s\n" % (
+ project_name, next_version, project_name, version))
+ if not dry_run:
+ f = codecs.open(changes_path, 'w', 'utf-8')
+ f.write(changes_txt)
+ f.close()
+
+ # - update version file
+ next_version_tuple = _tuple_from_version(next_version)
+ for i, ver_file in enumerate(version_files):
+ ver_content = codecs.open(ver_file, 'r', 'utf-8').read()
+ ver_file_type, ver_info = parsed_version_files[i]
+ if ver_file_type == "json":
+ marker = '"version": "%s"' % version
+ if marker not in ver_content:
+ raise Error("couldn't find `%s' version marker in `%s' "
+ "content: can't prep for subsequent dev" % (marker, ver_file))
+ ver_content = ver_content.replace(marker,
+ '"version": "%s"' % next_version)
+ elif ver_file_type == "javascript":
+ marker = 'var VERSION = "%s";' % version
+ if marker not in ver_content:
+ raise Error("couldn't find `%s' version marker in `%s' "
+ "content: can't prep for subsequent dev" % (marker, ver_file))
+ ver_content = ver_content.replace(marker,
+ 'var VERSION = "%s";' % next_version)
+ elif ver_file_type == "python":
+ marker = "__version_info__ = %r" % (version_info,)
+ if marker not in ver_content:
+ raise Error("couldn't find `%s' version marker in `%s' "
+ "content: can't prep for subsequent dev" % (marker, ver_file))
+ ver_content = ver_content.replace(marker,
+ "__version_info__ = %r" % (next_version_tuple,))
+ elif ver_file_type == "version":
+ ver_content = next_version
+ else:
+ raise Error("unknown ver_file_type: %r" % ver_file_type)
+ if not dry_run:
+ log.info("update version to '%s' in '%s'", next_version, ver_file)
+ f = codecs.open(ver_file, 'w', 'utf-8')
+ f.write(ver_content)
+ f.close()
+
+ if not dry_run:
+ run('git commit %s %s -m "prep for future dev"' % (
+ changes_path, ' '.join(version_files)))
+ run('git push')
+
+
+
+#---- internal support routines
+
+def _tuple_from_version(version):
+ def _intify(s):
+ try:
+ return int(s)
+ except ValueError:
+ return s
+ return tuple(_intify(b) for b in version.split('.'))
+
+def _get_next_version_info(version_info):
+ next = list(version_info[:])
+ next[-1] += 1
+ return tuple(next)
+
+def _version_from_version_info(version_info):
+ v = str(version_info[0])
+ state_dot_join = True
+ for i in version_info[1:]:
+ if state_dot_join:
+ try:
+ int(i)
+ except ValueError:
+ state_dot_join = False
+ else:
+ pass
+ if state_dot_join:
+ v += "." + str(i)
+ else:
+ v += str(i)
+ return v
+
+_version_re = re.compile(r"^(\d+)\.(\d+)(?:\.(\d+)([abc](\d+)?)?)?$")
+def _version_info_from_version(version):
+ m = _version_re.match(version)
+ if not m:
+ raise Error("could not convert '%s' version to version info" % version)
+ version_info = []
+ for g in m.groups():
+ if g is None:
+ break
+ try:
+ version_info.append(int(g))
+ except ValueError:
+ version_info.append(g)
+ return tuple(version_info)
+
+def _parse_version_file(version_file):
+ """Get version info from the given file. It can be any of:
+
+ Supported version file types (i.e. types of files from which we know
+ how to parse the version string/number -- often by some convention):
+ - json: use the "version" key
+ - javascript: look for a `var VERSION = "1.2.3";`
+ - python: Python script/module with `__version_info__ = (1, 2, 3)`
+ - version: a VERSION.txt or VERSION file where the whole contents are
+ the version string
+
+ @param version_file {str} Can be a path or "type:path", where "type"
+ is one of the supported types.
+ """
+ # Get version file *type*.
+ version_file_type = None
+ match = re.compile("^([a-z]+):(.*)$").search(version_file)
+ if match:
+ version_file = match.group(2)
+ version_file_type = match.group(1)
+ aliases = {
+ "js": "javascript"
+ }
+ if version_file_type in aliases:
+ version_file_type = aliases[version_file_type]
+
+ f = codecs.open(version_file, 'r', 'utf-8')
+ content = f.read()
+ f.close()
+
+ if not version_file_type:
+ # Guess the type.
+ base = basename(version_file)
+ ext = splitext(base)[1]
+ if ext == ".json":
+ version_file_type = "json"
+ elif ext == ".py":
+ version_file_type = "python"
+ elif ext == ".js":
+ version_file_type = "javascript"
+ elif content.startswith("#!"):
+ shebang = content.splitlines(False)[0]
+ shebang_bits = re.split(r'[/ \t]', shebang)
+ for name, typ in {"python": "python", "node": "javascript"}.items():
+ if name in shebang_bits:
+ version_file_type = typ
+ break
+ elif base in ("VERSION", "VERSION.txt"):
+ version_file_type = "version"
+ if not version_file_type:
+ raise RuntimeError("can't extract version from '%s': no idea "
+ "what type of file it it" % version_file)
+
+ if version_file_type == "json":
+ obj = json.loads(content)
+ version_info = _version_info_from_version(obj["version"])
+ elif version_file_type == "python":
+ m = re.search(r'^__version_info__ = (.*?)$', content, re.M)
+ version_info = eval(m.group(1))
+ elif version_file_type == "javascript":
+ m = re.search(r'^var VERSION = "(.*?)";$', content, re.M)
+ version_info = _version_info_from_version(m.group(1))
+ elif version_file_type == "version":
+ version_info = _version_info_from_version(content.strip())
+ else:
+ raise RuntimeError("unexpected version_file_type: %r"
+ % version_file_type)
+ return version_file_type, version_info
+
+
+## {{{ http://code.activestate.com/recipes/577058/ (r2)
+def query_yes_no(question, default="yes"):
+ """Ask a yes/no question via raw_input() and return their answer.
+
+ "question" is a string that is presented to the user.
+ "default" is the presumed answer if the user just hits <Enter>.
+ It must be "yes" (the default), "no" or None (meaning
+ an answer is required of the user).
+
+ The "answer" return value is one of "yes" or "no".
+ """
+ valid = {"yes":"yes", "y":"yes", "ye":"yes",
+ "no":"no", "n":"no"}
+ if default == None:
+ prompt = " [y/n] "
+ elif default == "yes":
+ prompt = " [Y/n] "
+ elif default == "no":
+ prompt = " [y/N] "
+ else:
+ raise ValueError("invalid default answer: '%s'" % default)
+
+ while 1:
+ sys.stdout.write(question + prompt)
+ choice = raw_input().lower()
+ if default is not None and choice == '':
+ return default
+ elif choice in valid.keys():
+ return valid[choice]
+ else:
+ sys.stdout.write("Please respond with 'yes' or 'no' "\
+ "(or 'y' or 'n').\n")
+## end of http://code.activestate.com/recipes/577058/ }}}
+
+def _capture_stdout(argv):
+ import subprocess
+ p = subprocess.Popen(argv, stdout=subprocess.PIPE)
+ return p.communicate()[0]
+
+class _NoReflowFormatter(optparse.IndentedHelpFormatter):
+ """An optparse formatter that does NOT reflow the description."""
+ def format_description(self, description):
+ return description or ""
+
+def run(cmd):
+ """Run the given command.
+
+ Raises OSError is the command returns a non-zero exit status.
+ """
+ log.debug("running '%s'", cmd)
+ fixed_cmd = cmd
+ if sys.platform == "win32" and cmd.count('"') > 2:
+ fixed_cmd = '"' + cmd + '"'
+ retval = os.system(fixed_cmd)
+ if hasattr(os, "WEXITSTATUS"):
+ status = os.WEXITSTATUS(retval)
+ else:
+ status = retval
+ if status:
+ raise OSError(status, "error running '%s'" % cmd)
+
+def _setup_command_prefix():
+ prefix = ""
+ if sys.platform == "darwin":
+ # http://forums.macosxhints.com/archive/index.php/t-43243.html
+ # This is an Apple customization to `tar` to avoid creating
+ # '._foo' files for extended-attributes for archived files.
+ prefix = "COPY_EXTENDED_ATTRIBUTES_DISABLE=1 "
+ return prefix
+
+
+#---- mainline
+
+def main(argv):
+ logging.basicConfig(format="%(name)s: %(levelname)s: %(message)s")
+ log.setLevel(logging.INFO)
+
+ # Parse options.
+ parser = optparse.OptionParser(prog="cutarelease", usage='',
+ version="%prog " + __version__, description=__doc__,
+ formatter=_NoReflowFormatter())
+ parser.add_option("-v", "--verbose", dest="log_level",
+ action="store_const", const=logging.DEBUG,
+ help="more verbose output")
+ parser.add_option("-q", "--quiet", dest="log_level",
+ action="store_const", const=logging.WARNING,
+ help="quieter output (just warnings and errors)")
+ parser.set_default("log_level", logging.INFO)
+ parser.add_option("--test", action="store_true",
+ help="run self-test and exit (use 'eol.py -v --test' for verbose test output)")
+ parser.add_option("-p", "--project-name", metavar="NAME",
+ help='the name of this project (default is the base dir name)',
+ default=basename(os.getcwd()))
+ parser.add_option("-f", "--version-file", metavar="[TYPE:]PATH",
+ action='append', dest="version_files",
+ help='The path to the project file holding the version info. Can be '
+ 'specified multiple times if more than one file should be updated '
+ 'with new version info. If excluded, it will be guessed.')
+ parser.add_option("-n", "--dry-run", action="store_true",
+ help='Do a dry-run', default=False)
+ opts, args = parser.parse_args()
+ log.setLevel(opts.log_level)
+
+ cutarelease(opts.project_name, opts.version_files, dry_run=opts.dry_run)
+
+
+## {{{ http://code.activestate.com/recipes/577258/ (r5)
+if __name__ == "__main__":
+ try:
+ retval = main(sys.argv)
+ except KeyboardInterrupt:
+ sys.exit(1)
+ except SystemExit:
+ raise
+ except:
+ import traceback, logging
+ if not log.handlers and not logging.root.handlers:
+ logging.basicConfig()
+ skip_it = False
+ exc_info = sys.exc_info()
+ if hasattr(exc_info[0], "__name__"):
+ exc_class, exc, tb = exc_info
+ if isinstance(exc, IOError) and exc.args[0] == 32:
+ # Skip 'IOError: [Errno 32] Broken pipe': often a cancelling of `less`.
+ skip_it = True
+ if not skip_it:
+ tb_path, tb_lineno, tb_func = traceback.extract_tb(tb)[-1][:3]
+ log.error("%s (%s:%s in %s)", exc_info[1], tb_path,
+ tb_lineno, tb_func)
+ else: # string exception
+ log.error(exc_info[0])
+ if not skip_it:
+ if log.isEnabledFor(logging.DEBUG):
+ print()
+ traceback.print_exception(*exc_info)
+ sys.exit(1)
+ else:
+ sys.exit(retval)
+## end of http://code.activestate.com/recipes/577258/ }}}
953 tools/jsstyle
@@ -0,0 +1,953 @@
+#!/usr/bin/env perl
+#
+# CDDL HEADER START
+#
+# The contents of this file are subject to the terms of the
+# Common Development and Distribution License (the "License").
+# You may not use this file except in compliance with the License.
+#
+# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+# or http://www.opensolaris.org/os/licensing.
+# See the License for the specific language governing permissions
+# and limitations under the License.
+#
+# When distributing Covered Code, include this CDDL HEADER in each
+# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+# If applicable, add the following below this CDDL HEADER, with the
+# fields enclosed by brackets "[]" replaced with your own identifying
+# information: Portions Copyright [yyyy] [name of copyright owner]
+#
+# CDDL HEADER END
+#
+#
+# Copyright 2008 Sun Microsystems, Inc. All rights reserved.
+# Use is subject to license terms.
+#
+# Copyright 2011 Joyent, Inc. All rights reserved.
+#
+# jsstyle - check for some common stylistic errors.
+#
+# jsstyle is a sort of "lint" for Javascript coding style. This tool is
+# derived from the cstyle tool, used to check for the style used in the
+# Solaris kernel, sometimes known as "Bill Joy Normal Form".
+#
+# There's a lot this can't check for, like proper indentation of code
+# blocks. There's also a lot more this could check for.
+#
+# A note to the non perl literate:
+#
+# perl regular expressions are pretty much like egrep
+# regular expressions, with the following special symbols
+#
+# \s any space character
+# \S any non-space character
+# \w any "word" character [a-zA-Z0-9_]
+# \W any non-word character
+# \d a digit [0-9]
+# \D a non-digit
+# \b word boundary (between \w and \W)
+# \B non-word boundary
+#
+
+require 5.0;
+use IO::File;
+use Getopt::Std;
+use strict;
+
+my $usage =
+"Usage: jsstyle [-h?vcC] [-t <num>] [-f <path>] [-o <config>] file ...
+
+Check your JavaScript file for style.
+See <https://github.com/davepacheco/jsstyle> for details on config options.
+Report bugs to <https://github.com/davepacheco/jsstyle/issues>.
+
+Options:
+ -h print this help and exit
+ -v verbose
+
+ -c check continuation indentation inside functions
+ -t specify tab width for line length calculation
+ -C don't check anything in header block comments
+
+ -f PATH
+ path to a jsstyle config file
+ -o OPTION1,OPTION2
+ set config options, e.g. '-o doxygen,indent=2'
+
+";
+
+my %opts;
+
+if (!getopts("ch?o:t:f:vC", \%opts)) {
+ print $usage;
+ exit 2;
+}
+
+if (defined($opts{'h'}) || defined($opts{'?'})) {
+ print $usage;
+ exit;
+}
+
+my $check_continuation = $opts{'c'};
+my $verbose = $opts{'v'};
+my $ignore_hdr_comment = $opts{'C'};
+my $tab_width = $opts{'t'};
+
+# By default, tabs are 8 characters wide
+if (! defined($opts{'t'})) {
+ $tab_width = 8;
+}
+
+
+# Load config
+my %config = (
+ indent => "tab",
+ doxygen => 0, # doxygen comments: /** ... */
+ splint => 0, # splint comments. Needed?
+ "unparenthesized-return" => 1,
+ "literal-string-quote" => "single", # 'single' or 'double'
+ "blank-after-start-comment" => 1,
+ "continuation-at-front" => 0,
+ "leading-right-paren-ok" => 0,
+ "strict-indent" => 0
+);
+sub add_config_var ($$) {
+ my ($scope, $str) = @_;
+
+ if ($str !~ /^([\w-]+)(?:\s*=\s*(.*?))?$/) {
+ die "$scope: invalid option: '$str'";
+ }
+ my $name = $1;
+ my $value = ($2 eq '' ? 1 : $2);
+ #print "scope: '$scope', str: '$str', name: '$name', value: '$value'\n";
+
+ # Validate config var.
+ if ($name eq "indent") {
+ # A number of spaces or "tab".
+ if ($value !~ /^\d+$/ && $value ne "tab") {
+ die "$scope: invalid '$name': must be a number (of ".
+ "spaces) or 'tab'";
+ }
+ } elsif ($name eq "doxygen" || # boolean vars
+ $name eq "splint" ||
+ $name eq "unparenthesized-return" ||
+ $name eq "continuation-at-front" ||
+ $name eq "leading-right-paren-ok" ||
+ $name eq "leading-comma-ok" ||
+ $name eq "uncuddled-else-ok" ||
+ $name eq "whitespace-after-left-paren-ok" ||
+ $name eq "strict-indent" ||
+ $name eq "blank-after-start-comment") {
+
+ if ($value != 1 && $value != 0) {
+ die "$scope: invalid '$name': don't give a value";
+ }
+ } elsif ($name eq "literal-string-quote") {
+ if ($value !~ /single|double/) {
+ die "$scope: invalid '$name': must be 'single' ".
+ "or 'double'";
+ }
+ } else {
+ die "$scope: unknown config var: $name";
+ }
+ $config{$name} = $value;
+}
+
+if (defined($opts{'f'})) {
+ my $path = $opts{'f'};
+ my $fh = new IO::File $path, "r";
+ if (!defined($fh)) {
+ die "cannot open config path '$path'";
+ }
+ my $line = 0;
+ while (<$fh>) {
+ $line++;
+ s/^\s*//; # drop leading space
+ s/\s*$//; # drop trailing space
+ next if ! $_; # skip empty line
+ next if /^#/; # skip comments
+ add_config_var "$path:$line", $_;
+ }
+}
+
+if (defined($opts{'o'})) {
+ for my $x (split /,/, $opts{'o'}) {
+ add_config_var "'-o' option", $x;
+ }
+}
+
+
+my ($filename, $line, $prev); # shared globals
+
+my $fmt;
+my $hdr_comment_start;
+
+if ($verbose) {
+ $fmt = "%s: %d: %s\n%s\n";
+} else {
+ $fmt = "%s: %d: %s\n";
+}
+
+if ($config{"doxygen"}) {
+ # doxygen comments look like "/*!" or "/**"; allow them.
+ $hdr_comment_start = qr/^\s*\/\*[\!\*]?$/;
+} else {
+ $hdr_comment_start = qr/^\s*\/\*$/;
+}
+
+# Note, following must be in single quotes so that \s and \w work right.
+my $lint_re = qr/\/\*(?:
+ jsl:\w+?|ARGSUSED[0-9]*|NOTREACHED|LINTLIBRARY|VARARGS[0-9]*|
+ CONSTCOND|CONSTANTCOND|CONSTANTCONDITION|EMPTY|
+ FALLTHRU|FALLTHROUGH|LINTED.*?|PRINTFLIKE[0-9]*|
+ PROTOLIB[0-9]*|SCANFLIKE[0-9]*|JSSTYLED.*?
+ )\*\//x;
+
+my $splint_re = qr/\/\*@.*?@\*\//x;
+
+my $err_stat = 0; # exit status
+
+if ($#ARGV >= 0) {
+ foreach my $arg (@ARGV) {
+ my $fh = new IO::File $arg, "r";
+ if (!defined($fh)) {
+ printf "%s: cannot open\n", $arg;
+ } else {
+ &jsstyle($arg, $fh);
+ close $fh;
+ }
+ }
+} else {
+ &jsstyle("<stdin>", *STDIN);
+}
+exit $err_stat;
+
+my $no_errs = 0; # set for JSSTYLED-protected lines
+
+sub err($) {
+ my ($error) = @_;
+ unless ($no_errs) {
+ printf $fmt, $filename, $., $error, $line;
+ $err_stat = 1;
+ }
+}
+
+sub err_prefix($$) {
+ my ($prevline, $error) = @_;
+ my $out = $prevline."\n".$line;
+ unless ($no_errs) {
+ printf $fmt, $filename, $., $error, $out;
+ $err_stat = 1;
+ }
+}
+
+sub err_prev($) {
+ my ($error) = @_;
+ unless ($no_errs) {
+ printf $fmt, $filename, $. - 1, $error, $prev;
+ $err_stat = 1;
+ }
+}
+
+sub jsstyle($$) {
+
+my ($fn, $filehandle) = @_;
+$filename = $fn; # share it globally
+
+my $in_cpp = 0;
+my $next_in_cpp = 0;
+
+my $in_comment = 0;
+my $in_header_comment = 0;
+my $comment_done = 0;
+my $in_function = 0;
+my $in_function_header = 0;
+my $in_declaration = 0;
+my $note_level = 0;
+my $nextok = 0;
+my $nocheck = 0;
+
+my $in_string = 0;
+
+my ($okmsg, $comment_prefix);
+
+$line = '';
+$prev = '';
+reset_indent();
+
+line: while (<$filehandle>) {
+ s/\r?\n$//; # strip return and newline
+
+ # save the original line, then remove all text from within
+ # double or single quotes, we do not want to check such text.
+
+ $line = $_;
+
+ #
+ # C allows strings to be continued with a backslash at the end of
+ # the line. We translate that into a quoted string on the previous
+ # line followed by an initial quote on the next line.
+ #
+ # (we assume that no-one will use backslash-continuation with character
+ # constants)
+ #
+ $_ = '"' . $_ if ($in_string && !$nocheck && !$in_comment);
+
+ #
+ # normal strings and characters
+ #
+ s/'([^\\']|\\.)*'/\'\'/g;
+ s/"([^\\"]|\\.)*"/\"\"/g;
+
+ #
+ # detect string continuation
+ #
+ if ($nocheck || $in_comment) {
+ $in_string = 0;
+ } else {
+ #
+ # Now that all full strings are replaced with "", we check
+ # for unfinished strings continuing onto the next line.
+ #
+ $in_string =
+ (s/([^"](?:"")*)"([^\\"]|\\.)*\\$/$1""/ ||
+ s/^("")*"([^\\"]|\\.)*\\$/""/);
+ }
+
+ #
+ # figure out if we are in a cpp directive
+ #
+ $in_cpp = $next_in_cpp || /^\s*#/; # continued or started
+ $next_in_cpp = $in_cpp && /\\$/; # only if continued
+
+ # strip off trailing backslashes, which appear in long macros
+ s/\s*\\$//;
+
+ # an /* END JSSTYLED */ comment ends a no-check block.
+ if ($nocheck) {
+ if (/\/\* *END *JSSTYLED *\*\//) {
+ $nocheck = 0;
+ } else {
+ reset_indent();
+ next line;
+ }
+ }
+
+ # a /*JSSTYLED*/ comment indicates that the next line is ok.
+ if ($nextok) {
+ if ($okmsg) {
+ err($okmsg);
+ }
+ $nextok = 0;
+ $okmsg = 0;
+ if (/\/\* *JSSTYLED.*\*\//) {
+ /^.*\/\* *JSSTYLED *(.*) *\*\/.*$/;
+ $okmsg = $1;
+ $nextok = 1;
+ }
+ $no_errs = 1;
+ } elsif ($no_errs) {
+ $no_errs = 0;
+ }
+
+ # check length of line.
+ # first, a quick check to see if there is any chance of being too long.
+ if ((($line =~ tr/\t/\t/) * ($tab_width - 1)) + length($line) > 80) {
+ # yes, there is a chance.
+ # replace tabs with spaces and check again.
+ my $eline = $line;
+ 1 while $eline =~
+ s/\t+/' ' x
+ (length($&) * $tab_width - length($`) % $tab_width)/e;
+ if (length($eline) > 80) {
+ err("line > 80 characters");
+ }
+ }
+
+ # ignore NOTE(...) annotations (assumes NOTE is on lines by itself).
+ if ($note_level || /\b_?NOTE\s*\(/) { # if in NOTE or this is NOTE
+ s/[^()]//g; # eliminate all non-parens
+ $note_level += s/\(//g - length; # update paren nest level
+ next;
+ }
+
+ # a /* BEGIN JSSTYLED */ comment starts a no-check block.
+ if (/\/\* *BEGIN *JSSTYLED *\*\//) {
+ $nocheck = 1;
+ }
+
+ # a /*JSSTYLED*/ comment indicates that the next line is ok.
+ if (/\/\* *JSSTYLED.*\*\//) {
+ /^.*\/\* *JSSTYLED *(.*) *\*\/.*$/;
+ $okmsg = $1;
+ $nextok = 1;
+ }
+ if (/\/\/ *JSSTYLED/) {
+ /^.*\/\/ *JSSTYLED *(.*)$/;
+ $okmsg = $1;
+ $nextok = 1;
+ }
+
+ # universal checks; apply to everything
+ if (/\t +\t/) {
+ err("spaces between tabs");
+ }
+ if (/ \t+ /) {
+ err("tabs between spaces");
+ }
+ if (/\s$/) {
+ err("space or tab at end of line");
+ }
+ if (/[^ \t(]\/\*/ && !/\w\(\/\*.*\*\/\);/) {
+ err("comment preceded by non-blank");
+ }
+
+ # is this the beginning or ending of a function?
+ # (not if "struct foo\n{\n")
+ if (/^{$/ && $prev =~ /\)\s*(const\s*)?(\/\*.*\*\/\s*)?\\?$/) {
+ $in_function = 1;
+ $in_declaration = 1;
+ $in_function_header = 0;
+ $prev = $line;
+ next line;
+ }
+ if (/^}\s*(\/\*.*\*\/\s*)*$/) {
+ if ($prev =~ /^\s*return\s*;/) {
+ err_prev("unneeded return at end of function");
+ }
+ $in_function = 0;
+ reset_indent(); # we don't check between functions
+ $prev = $line;
+ next line;
+ }
+ if (/^\w*\($/) {
+ $in_function_header = 1;
+ }
+
+ # a blank line terminates the declarations within a function.
+ # XXX - but still a problem in sub-blocks.
+ if ($in_declaration && /^$/) {
+ $in_declaration = 0;
+ }
+
+ if ($comment_done) {
+ $in_comment = 0;
+ $in_header_comment = 0;
+ $comment_done = 0;
+ }
+ # does this looks like the start of a block comment?
+ if (/$hdr_comment_start/) {
+ if ($config{"indent"} eq "tab") {
+ if (!/^\t*\/\*/) {
+ err("block comment not indented by tabs");
+ }
+ } elsif (!/^ *\/\*/) {
+ err("block comment not indented by spaces");
+ }
+ $in_comment = 1;
+ /^(\s*)\//;
+ $comment_prefix = $1;
+ if ($comment_prefix eq "") {
+ $in_header_comment = 1;
+ }
+ $prev = $line;
+ next line;
+ }
+ # are we still in the block comment?
+ if ($in_comment) {
+ if (/^$comment_prefix \*\/$/) {
+ $comment_done = 1;
+ } elsif (/\*\//) {
+ $comment_done = 1;
+ err("improper block comment close")
+ unless ($ignore_hdr_comment && $in_header_comment);
+ } elsif (!/^$comment_prefix \*[ \t]/ &&
+ !/^$comment_prefix \*$/) {
+ err("improper block comment")
+ unless ($ignore_hdr_comment && $in_header_comment);
+ }
+ }
+
+ if ($in_header_comment && $ignore_hdr_comment) {
+ $prev = $line;
+ next line;
+ }
+
+ # check for errors that might occur in comments and in code.
+
+ # allow spaces to be used to draw pictures in header comments.
+ #if (/[^ ] / && !/".* .*"/ && !$in_header_comment) {
+ # err("spaces instead of tabs");
+ #}
+ #if (/^ / && !/^ \*[ \t\/]/ && !/^ \*$/ &&
+ # (!/^ \w/ || $in_function != 0)) {
+ # err("indent by spaces instead of tabs");
+ #}
+ if ($config{"indent"} eq "tab") {
+ if (/^ {2,}/ && !/^ [^ ]/) {
+ err("indent by spaces instead of tabs");
+ }
+ } elsif (/^\t/) {
+ err("indent by tabs instead of spaces")
+ } elsif (/^( +)/ && !$in_comment) {
+ my $indent = $1;
+ if (length($indent) < $config{"indent"}) {
+ err("indent of " . length($indent) .
+ " space(s) instead of " . $config{"indent"});
+ } elsif ($config{"strict-indent"} &&
+ length($indent) % $config{"indent"} != 0) {
+ err("indent is " . length($indent) .
+ " not a multiple of " . $config{'indent'} . " spaces");
+ }
+ }
+ if (/^\t+ [^ \t\*]/ || /^\t+ \S/ || /^\t+ \S/) {
+ err("continuation line not indented by 4 spaces");
+ }
+
+ # A multi-line block comment must not have content on the first line.
+ if (/^\s*\/\*./ && !/^\s*\/\*.*\*\// && !/$hdr_comment_start/) {
+ err("improper first line of block comment");
+ }
+
+ if ($in_comment) { # still in comment, don't do further checks
+ $prev = $line;
+ next line;
+ }
+
+ if ((/[^(]\/\*\S/ || /^\/\*\S/) &&
+ !(/$lint_re/ || ($config{"splint"} && /$splint_re/))) {
+ err("missing blank after open comment");
+ }
+ if (/\S\*\/[^)]|\S\*\/$/ &&
+ !(/$lint_re/ || ($config{"splint"} && /$splint_re/))) {
+ err("missing blank before close comment");
+ }
+ if ($config{"blank-after-start-comment"} && /(?<!\w:)\/\/\S/) { # C++ comments
+ err("missing blank after start comment");
+ }
+ # check for unterminated single line comments, but allow them when
+ # they are used to comment out the argument list of a function
+ # declaration.
+ if (/\S.*\/\*/ && !/\S.*\/\*.*\*\// && !/\(\/\*/) {
+ err("unterminated single line comment");
+ }
+
+ if (/^(#else|#endif|#include)(.*)$/) {
+ $prev = $line;
+ next line;
+ }
+
+ #
+ # delete any comments and check everything else. Note that
+ # ".*?" is a non-greedy match, so that we don't get confused by
+ # multiple comments on the same line.
+ #
+ s/\/\*.*?\*\///g;
+ s/\/\/.*$//; # C++ comments
+
+ # delete any trailing whitespace; we have already checked for that.
+ s/\s*$//;
+
+ # following checks do not apply to text in comments.
+ my $quote = $config{"literal-string-quote"};
+ if ($quote eq "single") {
+ if (/"/) {
+ err("literal string using double-quote instead of single");
+ }
+ } elsif ($quote eq "double") {
+ if (/'/) {
+ err("literal string using single-quote instead of double");
+ }
+ }
+
+ if (/[^=!<>\s][!<>=]=/ || /[^<>!=][!<>=]==?[^\s,=]/ ||
+ (/[^->]>[^,=>\s]/ && !/[^->]>$/) ||
+ (/[^<]<[^,=<\s]/ && !/[^<]<$/) ||
+ /[^<\s]<[^<]/ || /[^->\s]>[^>]/) {
+ err("missing space around relational operator");
+ }
+ if (/\S>>=/ || /\S<<=/ || />>=\S/ || /<<=\S/ || /\S[-+*\/&|^%]=/ ||
+ (/[^-+*\/&|^%!<>=\s]=[^=]/ && !/[^-+*\/&|^%!<>=\s]=$/) ||
+ (/[^!<>=]=[^=\s]/ && !/[^!<>=]=$/)) {
+ # XXX - should only check this for C++ code
+ # XXX - there are probably other forms that should be allowed
+ if (!/\soperator=/) {
+ err("missing space around assignment operator");
+ }
+ }
+ if (/[,;]\S/ && !/\bfor \(;;\)/ &&
+ # Allow a comma in a regex quantifier.
+ !/\/.*?\{\d+,?\d*\}.*?\//) {
+ err("comma or semicolon followed by non-blank");
+ }
+ # check for commas preceded by blanks
+ if ((!$config{"leading-comma-ok"} && /^\s*,/) || (!/^\s*,/ && /\s,/)) {
+ err("comma preceded by blank");
+ }
+ # check for semicolons preceded by blanks
+ # allow "for" statements to have empty "while" clauses
+ if (/\s;/ && !/^[\t]+;$/ && !/^\s*for \([^;]*; ;[^;]*\)/) {
+ err("semicolon preceded by blank");
+ }
+ if (!$config{"continuation-at-front"} && /^\s*(&&|\|\|)/) {
+ err("improper boolean continuation");
+ } elsif ($config{"continuation-at-front"} && /(&&|\|\||\+)$/) {
+ err("improper continuation");
+ }
+ if (/\S *(&&|\|\|)/ || /(&&|\|\|) *\S/) {
+ err("more than one space around boolean operator");
+ }
+ # We allow methods which look like obj.delete() but not keywords without
+ # spaces ala: delete(obj)
+ if (/(?<!\.)\b(delete|typeof|instanceof|throw|with|catch|new|function|in|for|if|while|switch|return|case)\(/) {
+ err("missing space between keyword and paren");
+ }
+ if (/(\b(catch|for|if|with|while|switch|return)\b.*){2,}/) {
+ # multiple "case" and "sizeof" allowed
+ err("more than one keyword on line");
+ }
+ if (/\b(delete|typeof|instanceOf|with|throw|catch|new|function|in|for|if|while|switch|return|case)\s\s+\(/ &&
+ !/^#if\s+\(/) {
+ err("extra space between keyword and paren");
+ }
+ # try to detect "func (x)" but not "if (x)" or
+ # "#define foo (x)" or "int (*func)();"
+ if (/\w\s\(/) {
+ my $s = $_;
+ # strip off all keywords on the line
+ s/\b(delete|typeof|instanceOf|throw|with|catch|new|function|in|for|if|while|switch|return|case)\s\(/XXX(/g;
+ s/#elif\s\(/XXX(/g;
+ s/^#define\s+\w+\s+\(/XXX(/;
+ # do not match things like "void (*f)();"
+ # or "typedef void (func_t)();"
+ s/\w\s\(+\*/XXX(*/g;
+ s/\b(void)\s+\(+/XXX(/og;
+ if (/\w\s\(/) {
+ err("extra space between function name and left paren");
+ }
+ $_ = $s;
+ }
+
+ if ($config{"unparenthesized-return"} &&
+ /^\s*return\W[^;]*;/ && !/^\s*return\s*\(.*\);/) {
+ err("unparenthesized return expression");
+ }
+ if (/\btypeof\b/ && !/\btypeof\s*\(.*\)/) {
+ err("unparenthesized typeof expression");
+ }
+ if (!$config{"whitespace-after-left-paren-ok"} && /\(\s/) {
+ err("whitespace after left paren");
+ }
+ # allow "for" statements to have empty "continue" clauses
+ if (/\s\)/ && !/^\s*for \([^;]*;[^;]*; \)/) {
+ if ($config{"leading-right-paren-ok"} && /^\s+\)/) {
+ # this is allowed
+ } else {
+ err("whitespace before right paren");
+ }
+ }
+ if (/^\s*\(void\)[^ ]/) {
+ err("missing space after (void) cast");
+ }
+ if (/\S{/ && !/({|\(){/ &&
+ # Allow a brace in a regex quantifier.
+ !/\/.*?\{\d+,?\d*\}.*?\//) {
+ err("missing space before left brace");
+ }
+ if ($in_function && /^\s+{/ &&
+ ($prev =~ /\)\s*$/ || $prev =~ /\bstruct\s+\w+$/)) {
+ err("left brace starting a line");
+ }
+ if (/}(else|while)/) {
+ err("missing space after right brace");
+ }
+ if (/}\s\s+(else|while)/) {
+ err("extra space after right brace");
+ }
+ if (/^\s+#/) {
+ err("preprocessor statement not in column 1");
+ }
+ if (/^#\s/) {
+ err("blank after preprocessor #");
+ }
+
+ #
+ # We completely ignore, for purposes of indentation:
+ # * lines outside of functions
+ # * preprocessor lines
+ #
+ if ($check_continuation && $in_function && !$in_cpp) {
+ process_indent($_);
+ }
+
+ if (/^\s*else\W/) {
+ if (!$config{"uncuddled-else-ok"} && $prev =~ /^\s*}$/) {
+ err_prefix($prev,
+ "else and right brace should be on same line");
+ }
+ }
+ $prev = $line;
+}
+
+if ($prev eq "") {
+ err("last line in file is blank");
+}
+
+}
+
+#
+# Continuation-line checking
+#
+# The rest of this file contains the code for the continuation checking
+# engine. It's a pretty simple state machine which tracks the expression
+# depth (unmatched '('s and '['s).
+#
+# Keep in mind that the argument to process_indent() has already been heavily
+# processed; all comments have been replaced by control-A, and the contents of
+# strings and character constants have been elided.
+#
+
+my $cont_in; # currently inside of a continuation
+my $cont_off; # skipping an initializer or definition
+my $cont_noerr; # suppress cascading errors
+my $cont_start; # the line being continued
+my $cont_base; # the base indentation
+my $cont_first; # this is the first line of a statement
+my $cont_multiseg; # this continuation has multiple segments
+
+my $cont_special; # this is a C statement (if, for, etc.)
+my $cont_macro; # this is a macro
+my $cont_case; # this is a multi-line case
+
+my @cont_paren; # the stack of unmatched ( and [s we've seen
+
+sub
+reset_indent()
+{
+ $cont_in = 0;
+ $cont_off = 0;
+}
+
+sub
+delabel($)
+{
+ #
+ # replace labels with tabs. Note that there may be multiple
+ # labels on a line.
+ #
+ local $_ = $_[0];
+
+ while (/^(\t*)( *(?:(?:\w+\s*)|(?:case\b[^:]*)): *)(.*)$/) {
+ my ($pre_tabs, $label, $rest) = ($1, $2, $3);
+ $_ = $pre_tabs;
+ while ($label =~ s/^([^\t]*)(\t+)//) {
+ $_ .= "\t" x (length($2) + length($1) / 8);
+ }
+ $_ .= ("\t" x (length($label) / 8)).$rest;
+ }
+
+ return ($_);
+}
+
+sub
+process_indent($)
+{
+ require strict;
+ local $_ = $_[0]; # preserve the global $_
+
+ s///g; # No comments
+ s/\s+$//; # Strip trailing whitespace
+
+ return if (/^$/); # skip empty lines
+
+ # regexps used below; keywords taking (), macros, and continued cases
+ my $special = '(?:(?:\}\s*)?else\s+)?(?:if|for|while|switch)\b';
+ my $macro = '[A-Z_][A-Z_0-9]*\(';
+ my $case = 'case\b[^:]*$';
+
+ # skip over enumerations, array definitions, initializers, etc.
+ if ($cont_off <= 0 && !/^\s*$special/ &&
+ (/(?:(?:\b(?:enum|struct|union)\s*[^\{]*)|(?:\s+=\s*)){/ ||
+ (/^\s*{/ && $prev =~ /=\s*(?:\/\*.*\*\/\s*)*$/))) {
+ $cont_in = 0;
+ $cont_off = tr/{/{/ - tr/}/}/;
+ return;
+ }
+ if ($cont_off) {
+ $cont_off += tr/{/{/ - tr/}/}/;
+ return;
+ }
+
+ if (!$cont_in) {
+ $cont_start = $line;
+
+ if (/^\t* /) {
+ err("non-continuation indented 4 spaces");
+ $cont_noerr = 1; # stop reporting
+ }
+ $_ = delabel($_); # replace labels with tabs
+
+ # check if the statement is complete
+ return if (/^\s*\}?$/);
+ return if (/^\s*\}?\s*else\s*\{?$/);
+ return if (/^\s*do\s*\{?$/);
+ return if (/{$/);
+ return if (/}[,;]?$/);
+
+ # Allow macros on their own lines
+ return if (/^\s*[A-Z_][A-Z_0-9]*$/);
+
+ # cases we don't deal with, generally non-kosher
+ if (/{/) {
+ err("stuff after {");
+ return;
+ }
+
+ # Get the base line, and set up the state machine
+ /^(\t*)/;
+ $cont_base = $1;
+ $cont_in = 1;
+ @cont_paren = ();
+ $cont_first = 1;
+ $cont_multiseg = 0;
+
+ # certain things need special processing
+ $cont_special = /^\s*$special/? 1 : 0;
+ $cont_macro = /^\s*$macro/? 1 : 0;
+ $cont_case = /^\s*$case/? 1 : 0;
+ } else {
+ $cont_first = 0;
+
+ # Strings may be pulled back to an earlier (half-)tabstop
+ unless ($cont_noerr || /^$cont_base / ||
+ (/^\t*(?: )?(?:gettext\()?\"/ && !/^$cont_base\t/)) {
+ err_prefix($cont_start,
+ "continuation should be indented 4 spaces");
+ }
+ }
+
+ my $rest = $_; # keeps the remainder of the line
+
+ #
+ # The split matches 0 characters, so that each 'special' character
+ # is processed separately. Parens and brackets are pushed and
+ # popped off the @cont_paren stack. For normal processing, we wait
+ # until a ; or { terminates the statement. "special" processing
+ # (if/for/while/switch) is allowed to stop when the stack empties,
+ # as is macro processing. Case statements are terminated with a :
+ # and an empty paren stack.
+ #
+ foreach $_ (split /[^\(\)\[\]\{\}\;\:]*/) {
+ next if (length($_) == 0);
+
+ # rest contains the remainder of the line
+ my $rxp = "[^\Q$_\E]*\Q$_\E";
+ $rest =~ s/^$rxp//;
+
+ if (/\(/ || /\[/) {
+ push @cont_paren, $_;
+ } elsif (/\)/ || /\]/) {
+ my $cur = $_;
+ tr/\)\]/\(\[/;
+
+ my $old = (pop @cont_paren);
+ if (!defined($old)) {
+ err("unexpected '$cur'");
+ $cont_in = 0;
+ last;
+ } elsif ($old ne $_) {
+ err("'$cur' mismatched with '$old'");
+ $cont_in = 0;
+ last;
+ }
+
+ #
+ # If the stack is now empty, do special processing
+ # for if/for/while/switch and macro statements.
+ #
+ next if (@cont_paren != 0);
+ if ($cont_special) {
+ if ($rest =~ /^\s*{?$/) {
+ $cont_in = 0;
+ last;
+ }
+ if ($rest =~ /^\s*;$/) {
+ err("empty if/for/while body ".
+ "not on its own line");
+ $cont_in = 0;
+ last;
+ }
+ if (!$cont_first && $cont_multiseg == 1) {
+ err_prefix($cont_start,
+ "multiple statements continued ".
+ "over multiple lines");
+ $cont_multiseg = 2;
+ } elsif ($cont_multiseg == 0) {
+ $cont_multiseg = 1;
+ }
+ # We've finished this section, start
+ # processing the next.
+ goto section_ended;
+ }
+ if ($cont_macro) {
+ if ($rest =~ /^$/) {
+ $cont_in = 0;
+ last;
+ }
+ }
+ } elsif (/\;/) {
+ if ($cont_case) {
+ err("unexpected ;");
+ } elsif (!$cont_special) {
+ err("unexpected ;") if (@cont_paren != 0);
+ if (!$cont_first && $cont_multiseg == 1) {
+ err_prefix($cont_start,
+ "multiple statements continued ".
+ "over multiple lines");
+ $cont_multiseg = 2;
+ } elsif ($cont_multiseg == 0) {
+ $cont_multiseg = 1;
+ }
+ if ($rest =~ /^$/) {
+ $cont_in = 0;
+ last;
+ }
+ if ($rest =~ /^\s*special/) {
+ err("if/for/while/switch not started ".
+ "on its own line");
+ }
+ goto section_ended;
+ }
+ } elsif (/\{/) {
+ err("{ while in parens/brackets") if (@cont_paren != 0);
+ err("stuff after {") if ($rest =~ /[^\s}]/);
+ $cont_in = 0;
+ last;
+ } elsif (/\}/) {
+ err("} while in parens/brackets") if (@cont_paren != 0);
+ if (!$cont_special && $rest !~ /^\s*(while|else)\b/) {
+ if ($rest =~ /^$/) {
+ err("unexpected }");
+ } else {
+ err("stuff after }");
+ }
+ $cont_in = 0;
+ last;
+ }
+ } elsif (/\:/ && $cont_case && @cont_paren == 0) {
+ err("stuff after multi-line case") if ($rest !~ /$^/);
+ $cont_in = 0;
+ last;
+ }
+ next;
+section_ended:
+ # End of a statement or if/while/for loop. Reset
+ # cont_special and cont_macro based on the rest of the
+ # line.
+ $cont_special = ($rest =~ /^\s*$special/)? 1 : 0;
+ $cont_macro = ($rest =~ /^\s*$macro/)? 1 : 0;
+ $cont_case = 0;
+ next;
+ }
+ $cont_noerr = 0 if (!$cont_in);
+}
5 tools/jsstyle.conf
@@ -0,0 +1,5 @@
+indent=2
+doxygen
+unparenthesized-return=0
+blank-after-start-comment=0
+leading-right-paren-ok
Please sign in to comment.
Something went wrong with that request. Please try again.