├── tests ├── fixtures │ └── files │ │ ├── 1.php │ │ ├── no.txt │ │ ├── ignore │ │ └── 4.php │ │ └── inner │ │ ├── 2.php │ │ └── 3.php ├── help.js ├── rules │ ├── ternary-max-length.js │ ├── forbidden-function-prefix.js │ ├── variable-length.js │ ├── class-comment.js │ ├── empty-catch.js │ ├── if-assigment.js │ ├── max-nest.js │ ├── missing-braces.js │ ├── forbidden-functions.js │ ├── complex-if.js │ ├── namespace-max-files.js │ ├── loop-max-nest.js │ ├── argument-override.js │ ├── loop-max-size.js │ ├── function-max-size.js │ └── file-max-size.js ├── utils.js ├── errors.js └── tokenize.js ├── .gitignore ├── docs ├── logo.png ├── add-rule.md ├── rules.md └── api.md ├── .editorconfig ├── src ├── rules │ ├── forbidden-functions.js │ ├── empty-catch.js │ ├── forbidden-function-prefix.js │ ├── if-assigment.js │ ├── class-comment.js │ ├── max-nest.js │ ├── variable-length.js │ ├── missing-braces.js │ ├── namespace-max-files.js │ ├── complex-if.js │ ├── loop-max-nest.js │ ├── ternary-max-length.js │ ├── file-max-size.js │ ├── function-max-size.js │ ├── loop-max-size.js │ └── argument-override.js ├── config-loader.js ├── rule-test.js ├── default.readable.json ├── utils.js ├── help.js ├── errors.js ├── stream.js ├── line-count.js ├── lint.js └── tokenize.js ├── .eslintrc.js ├── Dockerfile ├── .github └── workflows │ └── push.yml ├── package.json ├── bin └── readable.js ├── README.md ├── CODE_OF_CONDUCT.md └── LICENSE /tests/fixtures/files/1.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/files/no.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/files/ignore/4.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/files/inner/2.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/files/inner/3.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .nyc_output 3 | coverage -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/officient/readable/HEAD/docs/logo.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true -------------------------------------------------------------------------------- /tests/help.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const help = require('../src/errors'); 3 | 4 | test('increases test coverage', (t) => { 5 | t.true(help.length > 0); 6 | }); 7 | -------------------------------------------------------------------------------- /src/rules/forbidden-functions.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | check(functions, tokens, report) { 3 | tokens.matchAll(functions, (token) => { 4 | report(`Dangerous call to ${token.body()}.`, token.current()); 5 | }); 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/rules/empty-catch.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | check(options, tokens, report) { 3 | tokens.matchAll('catch', (catchToken) => { 4 | const next = catchToken.copy().stepTo('{').step(); 5 | 6 | if (next.body() === '}') { 7 | report('Empty catch block.', catchToken.current()); 8 | } 9 | }); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | }, 6 | extends: [ 7 | 'airbnb-base', 8 | ], 9 | globals: { 10 | Atomics: 'readonly', 11 | SharedArrayBuffer: 'readonly', 12 | }, 13 | parserOptions: { 14 | ecmaVersion: 2018, 15 | sourceType: 'module', 16 | }, 17 | rules: { 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/rules/forbidden-function-prefix.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | check(prefixes, tokens, report) { 3 | tokens.matchAll('function', (token) => { 4 | const name = token.step().body(); 5 | prefixes.forEach((prefix) => { 6 | if (name.startsWith(prefix)) { 7 | report(`Function name can't start from ${prefix} [${name}].`, token.current()); 8 | } 9 | }); 10 | }); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/rules/if-assigment.js: -------------------------------------------------------------------------------- 1 | const matches = ['if', 'elseif']; 2 | 3 | module.exports = { 4 | check(_, tokens, report) { 5 | tokens.matchAll(matches, (token) => { 6 | const braket = token.copy().step(); 7 | braket.stepToClosing((assigment) => { 8 | if (assigment.body() === '=') { 9 | report('Assignment inside of an if statement.', token.current()); 10 | } 11 | }); 12 | }); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Image page: 2 | FROM node:12.18.2-alpine as builder 3 | 4 | # Prepare sources directory 5 | RUN set -x \ 6 | && mkdir /src \ 7 | && chown node:node /src 8 | 9 | WORKDIR /src 10 | 11 | # Copy sources into image 12 | COPY --chown=node . . 13 | 14 | # Use an unprivileged user 15 | USER node:node 16 | 17 | # Install dependencies 18 | RUN npm install --only=prod 19 | 20 | ENTRYPOINT ["/src/bin/readable.js"] 21 | -------------------------------------------------------------------------------- /src/rules/class-comment.js: -------------------------------------------------------------------------------- 1 | const { types } = require('../tokenize'); 2 | 3 | module.exports = { 4 | check(options, tokens, report) { 5 | tokens.matchAll('class', (classToken) => { 6 | const prev = classToken.copy(); 7 | do { 8 | prev.step(true, true); 9 | } while ((prev.type() === types.whitespace) || (prev.body() === 'abstract')); 10 | 11 | if (prev.type() !== types.comment) { 12 | report('Class have no comment above.', classToken.current()); 13 | } 14 | }); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /tests/rules/ternary-max-length.js: -------------------------------------------------------------------------------- 1 | const ruleTest = require('../../src/rule-test'); 2 | const rule = require('../../src/rules/ternary-max-length'); 3 | 4 | const src = ` 5 | { 4 | let level = 0; 5 | token.copy().stepTo('{').stepToClosing((inner) => { 6 | if (inner.body() === '{') { 7 | level += 1; 8 | if (level > maxLevel) { 9 | report(`Blocks are nested more than ${maxLevel} lines [${level}].`, inner.current()); 10 | } 11 | } 12 | if (inner.body() === '}') { 13 | level -= 1; 14 | } 15 | }); 16 | }); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /tests/rules/forbidden-function-prefix.js: -------------------------------------------------------------------------------- 1 | const ruleTest = require('../../src/rule-test'); 2 | const rule = require('../../src/rules/forbidden-function-prefix'); 3 | 4 | const code = ` 5 | { 5 | const variable = token.body(); 6 | if (!variable.startsWith('$') || (whitelist.includes(variable))) { 7 | return; 8 | } 9 | 10 | const length = variable.length - 1; 11 | const minLength = options['min-length']; 12 | if (length < minLength) { 13 | report(`Variable ${variable} name is shorter than ${minLength} [${length}].`, token.current()); 14 | } 15 | }); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /tests/rules/class-comment.js: -------------------------------------------------------------------------------- 1 | const ruleTest = require('../../src/rule-test'); 2 | const rule = require('../../src/rules/class-comment'); 3 | 4 | const valid1 = ` 5 | // good case 6 | class Good() 7 | { 8 | }`; 9 | 10 | const valid2 = ` 11 | /* 12 | also good 13 | */ 14 | abstract class Good() 15 | { 16 | }`; 17 | 18 | const invalid = ` 19 | class Bad() 20 | { 21 | }`; 22 | 23 | ruleTest('class-comment', rule, { 24 | valid: [ 25 | { src: valid1 }, 26 | { src: valid2 }, 27 | ], 28 | invalid: [ 29 | { 30 | src: invalid, 31 | messageIncludes: 'no comment above.', 32 | }, 33 | ], 34 | }); 35 | -------------------------------------------------------------------------------- /tests/rules/empty-catch.js: -------------------------------------------------------------------------------- 1 | const ruleTest = require('../../src/rule-test'); 2 | const rule = require('../../src/rules/empty-catch'); 3 | 4 | const valid = ` 5 | { 6 | const braket = token.copy().step(); 7 | // special treatment for else if 8 | if ((token.body() === 'else') && (braket.body() === 'if')) { 9 | return; 10 | } 11 | if (braket.body() === '(') { 12 | braket.stepToClosing().step(); 13 | } 14 | if (braket.body() !== '{') { 15 | report('If statement or loop without braces.', token.current()); 16 | } 17 | }); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /tests/rules/if-assigment.js: -------------------------------------------------------------------------------- 1 | const ruleTest = require('../../src/rule-test'); 2 | const rule = require('../../src/rules/if-assigment'); 3 | 4 | 5 | const src = ` 6 | $value) { 10 | for ($i = 1; $i <= 10; $i++) { 11 | foreach ($arr as $key => $value) { 12 | echo $i; 13 | } 14 | } 15 | } 16 | } 17 | `; 18 | 19 | ruleTest('max-nest', rule, { 20 | valid: [ 21 | { 22 | src, 23 | config: 3, 24 | }, 25 | ], 26 | invalid: [ 27 | { 28 | src, 29 | config: 2, 30 | messageIncludes: 'are nested more', 31 | }, 32 | ], 33 | }); 34 | -------------------------------------------------------------------------------- /src/rules/namespace-max-files.js: -------------------------------------------------------------------------------- 1 | const { dirname } = require('path'); 2 | 3 | function checkMaxFiles(maxFiles, files, report) { 4 | let count = 0; 5 | let lastFile = ''; 6 | files.forEach((file) => { 7 | if (typeof file === 'object') { 8 | checkMaxFiles(maxFiles, file, report); 9 | return; 10 | } 11 | lastFile = file; 12 | count += 1; 13 | }); 14 | 15 | if (count > maxFiles) { 16 | const path = dirname(lastFile); 17 | report(path, `namespace contains more than ${maxFiles} files [${count}].`); 18 | } 19 | } 20 | 21 | module.exports = { 22 | checkFiles(maxFiles, files, report) { 23 | checkMaxFiles(maxFiles, files, report); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /tests/rules/missing-braces.js: -------------------------------------------------------------------------------- 1 | const ruleTest = require('../../src/rule-test'); 2 | const rule = require('../../src/rules/missing-braces'); 3 | 4 | 5 | const valid = ` 6 | $value) { 9 | print_r($arr); 10 | } 11 | 12 | if ($a == 1) { 13 | do(); 14 | } else if ($b == 1) { 15 | do(); 16 | }; 17 | `; 18 | 19 | const invalid = ` 20 | $b) 23 | echo "something"; 24 | `; 25 | 26 | ruleTest('missing-braces', rule, { 27 | valid: [ 28 | { 29 | src: valid, 30 | config: true, 31 | }, 32 | ], 33 | invalid: [ 34 | { 35 | src: invalid, 36 | config: true, 37 | messageIncludes: 'If statement or loop without braces', 38 | }, 39 | ], 40 | }); 41 | -------------------------------------------------------------------------------- /tests/utils.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../src/utils'); 3 | 4 | 5 | test('build dir tree', (t) => { 6 | const dirs = [ 7 | './tests/fixtures/files/', 8 | '!./tests/fixtures/files/ignore/', 9 | ]; 10 | t.deepEqual(utils.dirsTree(dirs, '.php'), [ 11 | [ 12 | 'tests/fixtures/files/1.php', 13 | [ 14 | 'tests/fixtures/files/inner/2.php', 15 | 'tests/fixtures/files/inner/3.php', 16 | ], 17 | ], 18 | ]); 19 | }); 20 | 21 | test('keeps order on stringify', (t) => { 22 | const obj1 = {}; 23 | obj1.prop1 = '1'; 24 | obj1.prop2 = '2'; 25 | const obj2 = {}; 26 | obj2.prop2 = '2'; 27 | obj2.prop1 = '1'; 28 | t.is(utils.stringify(obj1), utils.stringify(obj2)); 29 | }); 30 | -------------------------------------------------------------------------------- /src/rules/complex-if.js: -------------------------------------------------------------------------------- 1 | const matches = ['if', 'elseif']; 2 | 3 | module.exports = { 4 | check(_, tokens, report) { 5 | tokens.matchAll(matches, (token) => { 6 | const braket = token.copy().step(); 7 | let countAnd = 0; 8 | let countOr = 0; 9 | braket.stepToClosing((operator) => { 10 | if (operator.body() === '&&') { 11 | countAnd += 1; 12 | } 13 | if (operator.body() === '||') { 14 | countOr += 1; 15 | } 16 | }); 17 | if (countOr && countAnd) { 18 | report('Mixing && and || inside if statement.', token.current()); 19 | } 20 | if ((countOr + countAnd) > 2) { 21 | report('More than 2 && or || inside if statement.', token.current()); 22 | } 23 | }); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /tests/rules/forbidden-functions.js: -------------------------------------------------------------------------------- 1 | const ruleTest = require('../../src/rule-test'); 2 | const rule = require('../../src/rules/forbidden-functions'); 3 | 4 | const invalid2 = ` 5 | { 12 | if (inner.matches(loops)) { 13 | if (depth === maxNest) { 14 | report(`Loop are nested more than ${maxNest} [${depth + 1}].`, inner.current()); 15 | } else { 16 | check(maxNest, inner, report, depth + 1); 17 | } 18 | } 19 | }); 20 | } 21 | 22 | module.exports = { 23 | check(maxNest, tokens, report) { 24 | tokens.matchAll(loops, (token) => { 25 | check(maxNest, token, report, 1); 26 | }); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/rules/ternary-max-length.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | check(maxLength, tokens, report) { 3 | tokens.matchAll('?', (token) => { 4 | const lineEnd = token.copy(); 5 | let isTernary = false; 6 | lineEnd.stepTo(';', (operator) => { 7 | if (operator.body() === ':') { 8 | // check if it's really ternary 9 | isTernary = true; 10 | } 11 | }); 12 | 13 | const lineStart = token.copy(); 14 | while (lineStart.current().line === token.current().line) { 15 | lineStart.step(true); 16 | } 17 | 18 | const length = lineEnd.current().column - lineStart.step().current().line; 19 | if (isTernary && (length > maxLength)) { 20 | report(`Line with ternaty longer than ${maxLength} [${length}]`, token.current()); 21 | } 22 | }); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /tests/rules/complex-if.js: -------------------------------------------------------------------------------- 1 | const ruleTest = require('../../src/rule-test'); 2 | const rule = require('../../src/rules/complex-if'); 3 | 4 | 5 | const src = ` 6 | { 7 | checkFiles(3, files, () => { 8 | t.fail(); 9 | t.end(); 10 | }); 11 | t.pass(); 12 | t.end(); 13 | }); 14 | 15 | test.cb('namespace-max-files reports', (t) => { 16 | t.plan(2); 17 | checkFiles(2, files, (path, message) => { 18 | t.is(path, './src'); 19 | t.true(message.includes('than 2 files [3].')); 20 | t.end(); 21 | }); 22 | }); 23 | 24 | const files2 = [['./src/1.php'], './src/2.php', './src/3.php', './src/4.php']; 25 | test.cb('namespace-max-files reports when first file is dir', (t) => { 26 | checkFiles(2, files2, () => { 27 | t.end(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/rules/loop-max-nest.js: -------------------------------------------------------------------------------- 1 | const ruleTest = require('../../src/rule-test'); 2 | const rule = require('../../src/rules/loop-max-nest'); 3 | 4 | const src = ` 5 | $value) { 8 | echo $key; 9 | for ($i = 1; $i <= 10; $i++) { 10 | foreach ($arr as $key => $value) { 11 | echo $i; 12 | } 13 | } 14 | } 15 | `; 16 | 17 | const invalid = ` 18 | $value) { 23 | echo $i; 24 | } 25 | } 26 | } while ($i > 0); 27 | 28 | `; 29 | 30 | 31 | ruleTest('loop-max-nest', rule, { 32 | valid: [ 33 | { 34 | src, 35 | config: 3, 36 | }, 37 | ], 38 | invalid: [ 39 | { 40 | src, 41 | config: 2, 42 | messageIncludes: 'are nested more', 43 | }, 44 | { 45 | src: invalid, 46 | config: 2, 47 | }, 48 | ], 49 | }); 50 | -------------------------------------------------------------------------------- /src/config-loader.js: -------------------------------------------------------------------------------- 1 | 2 | /* eslint global-require:off */ 3 | 4 | const fs = require('fs'); 5 | const { merge } = require('lodash'); 6 | const path = require('path'); 7 | 8 | const fileName = path.join('.', '.readable.json'); 9 | 10 | function init() { 11 | const source = path.join(__dirname, `default${fileName}`); 12 | 13 | fs.copyFileSync(source, fileName); 14 | } 15 | 16 | function load(ignoreBaseLine) { 17 | const data = fs.readFileSync(fileName, 'utf8'); 18 | const defaultConfig = require('./default.readable.json'); 19 | const config = merge(defaultConfig, JSON.parse(data)); 20 | 21 | // load the baseline 22 | if (ignoreBaseLine || (!config.baseline)) { 23 | config.baseline = {}; 24 | 25 | return config; 26 | } 27 | 28 | const baseline = fs.readFileSync(config.baseline, 'utf8'); 29 | config.baseline = JSON.parse(baseline); 30 | 31 | return config; 32 | } 33 | 34 | module.exports = { 35 | init, 36 | fileName, 37 | load, 38 | }; 39 | -------------------------------------------------------------------------------- /src/rules/file-max-size.js: -------------------------------------------------------------------------------- 1 | const countLines = require('../line-count'); 2 | 3 | module.exports = { 4 | check(options, tokens, report) { 5 | // backward compatibility 6 | const oldConfig = (typeof options === 'number'); 7 | const maxLines = oldConfig ? options : options['max-lines']; 8 | const comments = oldConfig ? true : options['include-comments']; 9 | const emptyLines = oldConfig ? true : options['include-empty-lines']; 10 | let brackets = oldConfig ? true : options['include-brackets']; 11 | if (brackets === undefined) { 12 | brackets = true; // Backwards compatibility 13 | } 14 | 15 | const settings = { comments, emptyLines, brackets }; 16 | const startToken = tokens.copy(); 17 | const endToken = tokens.copy().stepToEof(); 18 | const { lineCount, currentToken } = countLines(startToken, endToken, settings); 19 | if (lineCount > maxLines) { 20 | report(`file contains more than ${maxLines} lines [${lineCount}].`, currentToken); 21 | } 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/rule-test.js: -------------------------------------------------------------------------------- 1 | 2 | /* eslint import/no-extraneous-dependencies:off */ 3 | 4 | const ava = require('ava'); 5 | const { tokenize } = require('./tokenize'); 6 | 7 | function ruleTest(name, rule, tests) { 8 | tests.valid.forEach((test, i) => { 9 | const tokens = tokenize(test.src); 10 | ava.cb(`${name} passes [${i + 1}]`, (t) => { 11 | rule.check(test.config, tokens, () => { 12 | t.fail(); 13 | t.end(); 14 | }); 15 | t.pass(); 16 | t.end(); 17 | }); 18 | }); 19 | tests.invalid.forEach((test, i) => { 20 | const tokens = tokenize(test.src); 21 | ava.cb(`${name} reports [${i + 1}]`, (t) => { 22 | rule.check(test.config, tokens, (message) => { 23 | if (test.messageIncludes) { 24 | t.true( 25 | message.includes(test.messageIncludes), 26 | `Expected '${test.messageIncludes}' got '${message}'`, 27 | ); 28 | } 29 | t.end(); 30 | }); 31 | }); 32 | }); 33 | } 34 | 35 | module.exports = ruleTest; 36 | -------------------------------------------------------------------------------- /tests/rules/argument-override.js: -------------------------------------------------------------------------------- 1 | const ruleTest = require('../../src/rule-test'); 2 | const rule = require('../../src/rules/argument-override'); 3 | 4 | const src = ` 5 | some = 1; 35 | } 36 | `; 37 | 38 | const invalid3 = ` 39 | function some($one, $two) 40 | { 41 | ++$one; 42 | } 43 | `; 44 | 45 | 46 | const invalid4 = ` 47 | function some($one, $two) 48 | { 49 | $one += 1; 50 | } 51 | `; 52 | 53 | ruleTest('argument-override', rule, { 54 | valid: [ 55 | { src }, 56 | { 57 | src: valid, 58 | config: { 59 | 'allow-pass-by-reference': true, 60 | }, 61 | }, 62 | ], 63 | invalid: [ 64 | { 65 | src: invalid1, 66 | messageIncludes: 'Overriding of a function\'s argument', 67 | }, 68 | { src: invalid2 }, 69 | { src: invalid3 }, 70 | { src: invalid4 }, 71 | ], 72 | }); 73 | -------------------------------------------------------------------------------- /src/default.readable.json: -------------------------------------------------------------------------------- 1 | { 2 | "paths": [ 3 | "src/", 4 | "!src/vendor/" 5 | ], 6 | "rules": { 7 | "empty-catch": true, 8 | "if-assigment": true, 9 | "complex-if": true, 10 | "namespace-max-files": 15, 11 | "loop-max-size": { 12 | "max-lines": 15, 13 | "include-comments": true, 14 | "include-empty-lines": true, 15 | "include-brackets": true 16 | }, 17 | "loop-max-nest": 2, 18 | "max-nest": 3, 19 | "ternary-max-length": 50, 20 | "function-max-size": { 21 | "max-lines": 50, 22 | "include-comments": true, 23 | "include-empty-lines": true, 24 | "include-brackets": true 25 | }, 26 | "class-comment": true, 27 | "missing-braces": true, 28 | "forbidden-function-prefix": ["check"], 29 | "file-max-size": { 30 | "max-lines": 200, 31 | "include-comments": true, 32 | "include-empty-lines": true, 33 | "include-brackets": true 34 | }, 35 | "argument-override": { 36 | "allow-pass-by-reference": true 37 | }, 38 | "forbidden-functions": [ 39 | "eval", "print_r", "var_export", "var_dump", "phpinfo", "exec" 40 | ], 41 | "variable-length": { 42 | "min-length": 3, 43 | "whitelist": ["$id", "$i"] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@officient/readable", 3 | "private": false, 4 | "version": "1.0.3", 5 | "description": "PHP code linter", 6 | "bin": { 7 | "readable": "./bin/readable.js" 8 | }, 9 | "main": "index.js", 10 | "scripts": { 11 | "lint": "eslint bin/ src/ tests/", 12 | "fix": "eslint --fix bin/ src/ tests/", 13 | "test": "ava", 14 | "test:ci": "nyc --all --include 'src/**' ava", 15 | "test:ci:report": "nyc report --reporter=lcov", 16 | "docs": "jsdoc2md src/*.js > docs/api.md" 17 | }, 18 | "files": [ 19 | "README.md", 20 | "bin", 21 | "src" 22 | ], 23 | "engines": { 24 | "node": ">=10.0.0" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/officient/readable.git" 29 | }, 30 | "author": "", 31 | "license": "ISC", 32 | "bugs": { 33 | "url": "https://github.com/officient/readable/issues" 34 | }, 35 | "homepage": "https://github.com/officient/readable#readme", 36 | "dependencies": { 37 | "lodash": "^4.17.15", 38 | "promise.allsettled": "^1.0.2", 39 | "v8-compile-cache": "^2.1.0" 40 | }, 41 | "devDependencies": { 42 | "ava": "^3.5.0", 43 | "eslint": "^6.8.0", 44 | "eslint-config-airbnb-base": "^14.1.0", 45 | "eslint-plugin-import": "^2.20.1", 46 | "jsdoc-to-markdown": "^5.0.3", 47 | "nyc": "^15.0.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/rules/function-max-size.js: -------------------------------------------------------------------------------- 1 | const countLines = require('../line-count'); 2 | 3 | function getOptions(options) { 4 | // backward compatibility 5 | const oldConfig = (typeof options === 'number'); 6 | const maxLines = oldConfig ? options : options['max-lines']; 7 | let comments = oldConfig ? true : options['include-comments']; 8 | let emptyLines = oldConfig ? true : options['include-empty-lines']; 9 | let brackets = oldConfig ? true : options['include-brackets']; 10 | if (comments === undefined) { 11 | comments = true; 12 | } 13 | if (emptyLines === undefined) { 14 | emptyLines = true; 15 | } 16 | if (brackets === undefined) { 17 | brackets = true; 18 | } 19 | 20 | return { 21 | maxLines, comments, emptyLines, brackets, 22 | }; 23 | } 24 | 25 | 26 | module.exports = { 27 | check(options, tokens, report) { 28 | const settings = getOptions(options); 29 | 30 | tokens 31 | .matchAll('function', (token) => { 32 | const name = token.copy().step().body(); 33 | const startToken = token.copy(); 34 | const endToken = token.copy().stepTo('{').stepToClosing(); 35 | const { lineCount, currentToken } = countLines(startToken, endToken, settings); 36 | 37 | if (lineCount > settings.maxLines) { 38 | report(`Function ${name} is longer than ${settings.maxLines} lines [${lineCount}].`, currentToken); 39 | } 40 | }); 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { join, extname } = require('path'); 3 | 4 | function normalisePath(path) { 5 | return path.replace(/\\/g, '/'); 6 | } 7 | 8 | function walkDir(dir, ext, exclude) { 9 | const filesTree = fs.readdirSync(dir).map((file) => { 10 | const path = join(dir, file); 11 | if (exclude.includes(normalisePath(path))) { 12 | // we will filter it out on line 22 13 | return 'Exclude'; 14 | } 15 | if (fs.statSync(path).isDirectory()) { 16 | return walkDir(join(path, ''), ext, exclude); 17 | } 18 | return path; 19 | }); 20 | 21 | return filesTree.filter((file) => ( 22 | (typeof file === 'object') || (extname(file) === ext))); 23 | } 24 | 25 | // list files recursivelly (filter by extension) 26 | function dirsTree(dirs, ext) { 27 | const exclude = []; 28 | const include = dirs.filter((d) => { 29 | if (d.startsWith('!')) { 30 | const path = d.slice(1).replace(/\/$/, ''); 31 | exclude.push(join(path)); 32 | return false; 33 | } 34 | return true; 35 | }); 36 | return include.map((d) => walkDir(d, ext, exclude)); 37 | } 38 | 39 | // stringify keeping order 40 | function stringify(obj) { 41 | const ordered = {}; 42 | Object.keys(obj).sort().forEach((key) => { 43 | ordered[key] = obj[key]; 44 | }); 45 | 46 | return JSON.stringify(ordered, null, 2); 47 | } 48 | 49 | module.exports = { 50 | dirsTree, 51 | stringify, 52 | normalisePath, 53 | }; 54 | -------------------------------------------------------------------------------- /src/rules/loop-max-size.js: -------------------------------------------------------------------------------- 1 | 2 | const countLines = require('../line-count'); 3 | 4 | function getOptions(options) { 5 | // backward compatibility 6 | const oldConfig = (typeof options === 'number'); 7 | const maxLines = oldConfig ? options : options['max-lines']; 8 | let comments = oldConfig ? true : options['include-comments']; 9 | let emptyLines = oldConfig ? true : options['include-empty-lines']; 10 | let brackets = oldConfig ? true : options['include-brackets']; 11 | if (comments === undefined) { 12 | comments = true; 13 | } 14 | if (emptyLines === undefined) { 15 | emptyLines = true; 16 | } 17 | if (brackets === undefined) { 18 | brackets = true; 19 | } 20 | 21 | return { 22 | maxLines, comments, emptyLines, brackets, 23 | }; 24 | } 25 | 26 | 27 | module.exports = { 28 | check(options, tokens, report) { 29 | const settings = getOptions(options); 30 | 31 | const loops = ['for', 'foreach']; 32 | tokens.matchAll(loops, (token) => { 33 | const startToken = token.copy(); 34 | const endToken = token.copy() 35 | .step() 36 | .stepToClosing() // skip () 37 | .step() 38 | .stepToClosing(); // Go to ending bracelet 39 | 40 | const { lineCount, currentToken } = countLines(startToken, endToken, settings); 41 | if (lineCount > settings.maxLines) { 42 | report(`Loop is longer than ${settings.maxLines} lines [${lineCount}].`, currentToken); 43 | } 44 | }); 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /tests/errors.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Errors = require('../src/errors'); 3 | 4 | 5 | test('generaets baseline ', (t) => { 6 | const errors = new Errors(); 7 | errors.report('path1', 'rule1', 'msg', { line: 1 }); 8 | errors.report('path1', 'rule1', 'msg', { line: 1 }); 9 | errors.report('path1', 'rule2', 'msg', { line: 1 }); 10 | t.deepEqual(errors.generateBaseline(), { path1: { rule1: 2, rule2: 1 } }); 11 | }); 12 | 13 | test('iognores baselined errors ', (t) => { 14 | const errors = new Errors(); 15 | errors.report('path1', 'rule1', 'msg', { line: 1 }); 16 | errors.report('path1', 'rule1', 'msg', { line: 1 }); 17 | 18 | const baseline = errors.generateBaseline(); 19 | const errors2 = new Errors(baseline); 20 | errors2.report('path1', 'rule1', 'msg1', { line: 1 }); 21 | errors2.report('path1', 'rule1', 'msg2', { line: 1 }); 22 | errors2.report('path1', 'rule1', 'msg3', { line: 1 }); 23 | t.deepEqual(errors2.errors, { 24 | path1: { 25 | rule1: { msg3: [{ line: 1 }] }, 26 | }, 27 | }); 28 | }); 29 | 30 | test('normalises baseline paths', (t) => { 31 | const errors = new Errors(); 32 | errors.report('path1\\file1', 'rule1', 'msg', { line: 1 }); 33 | errors.report('path2/file2', 'rule1', 'msg', { line: 1 }); 34 | 35 | const baseline = errors.generateBaseline(); 36 | 37 | const errors2 = new Errors(baseline); 38 | errors2.report('path1/file1', 'rule1', 'msg1', { line: 1 }); 39 | errors2.report('path2\\file2', 'rule1', 'msg2', { line: 1 }); 40 | t.deepEqual(errors2.errors, {}); 41 | }); 42 | -------------------------------------------------------------------------------- /src/rules/argument-override.js: -------------------------------------------------------------------------------- 1 | const increments = ['++', '--']; 2 | const assigment = ['=', '+=', '-=', '*=', '/=', '%=', '.=']; 3 | 4 | module.exports = { 5 | check(options, tokens, report) { 6 | const allowKey = 'allow-pass-by-reference'; 7 | const allowReference = (options && allowKey in options) ? options[allowKey] : false; 8 | tokens.matchAll('function', (token) => { 9 | const args = []; 10 | // extract args from () 11 | token.step().step().stepToClosing((t1) => { 12 | if (t1.body().startsWith('$')) { 13 | if (allowReference && t1.copy().step(true).matches('&')) { 14 | return; 15 | } 16 | args.push(t1.body()); 17 | } 18 | }); 19 | // search for args in function body 20 | token.step().stepToClosing((argToken) => { 21 | if (argToken.matches(args)) { 22 | const rep = (arg, t) => { 23 | report(`Overriding of a function's argument ${arg}.`, t.current()); 24 | }; 25 | const arg = argToken.body(); 26 | const prev = argToken.copy().step(true); 27 | if (prev.matches(increments)) { 28 | rep(arg, prev); 29 | } 30 | // check if token in beginning of statement 31 | if (!prev.matches(['{', ';', ')'])) { 32 | return; 33 | } 34 | argToken.stepTo(';', (t3) => { 35 | if (t3.matches(increments) || t3.matches(assigment)) { 36 | rep(arg, t3); 37 | } 38 | }); 39 | } 40 | }); 41 | }); 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/help.js: -------------------------------------------------------------------------------- 1 | const reset = '\x1b[0m'; 2 | const main = '\x1b[37m'; 3 | const readable = `${reset}readable${main}`; 4 | 5 | const help = ` 6 | ${readable} - static analysis tool for PHP with a focus on code readability${reset} 7 | 8 | GETTING STARTED 9 | npx readable --init 10 | 11 | ${main}Will generate ${reset}.readable.json${main} with default config. 12 | Check the ${reset}"paths"${main} param, and you ready to lint:${reset} 13 | 14 | npx readable 15 | 16 | BASELINE 17 | ${main}If you have a bunch of errors and you don't want to fix them all 18 | at once, ${readable} can ignore errors in existing code, while 19 | ensuring that new code doesn't have errors:${reset} 20 | 21 | npx readable --save-base-line .baseline.json 22 | 23 | ${main}will generate or update ${reset}.baseline.json${main} file containing the 24 | current errors. Add ${reset}"baseline"${main} param to your ${reset}.readable.json${main}:${reset} 25 | 26 | { 27 | "baseline": ".baseline.json", 28 | ... 29 | } 30 | ${main}You can commit the changes so that ${readable} running in other 31 | places (e.g. CI) won't complain about those errors. If you want 32 | to see all errors run ${readable} with ${reset}--disable-base-line${main} flag${main}:${reset} 33 | 34 | npx readable --disable-base-line 35 | 36 | EXIT STATUS 37 | 0 ${main}No errors${reset} 38 | 39 | 1 ${main}Found errors${reset} 40 | 41 | 2 ${main}Unexpected behaviour${reset} 42 | 43 | LICENCE 44 | ${main}GNU General Public License v3.0 45 | https://github.com/officient/readable/${reset} 46 | `; 47 | 48 | module.exports = help; 49 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | const { normalisePath } = require('./utils'); 2 | 3 | // gather errors by path and message 4 | class Errors { 5 | constructor(baseline) { 6 | this.errors = {}; 7 | this.baseline = baseline || {}; 8 | } 9 | 10 | isBaselined(path, rule) { 11 | // errors report pathes OS dependent 12 | // baseline stores unix-style baseline 13 | const normalPath = normalisePath(path); 14 | if (!(normalPath in this.baseline)) { 15 | return false; 16 | } 17 | 18 | if (!(rule in this.baseline[normalPath])) { 19 | return false; 20 | } 21 | 22 | const left = this.baseline[normalPath][rule]; 23 | if (left === 0) { 24 | return false; 25 | } 26 | 27 | this.baseline[normalPath][rule] = left - 1; 28 | return true; 29 | } 30 | 31 | report(path, rule, message, token) { 32 | if (this.isBaselined(path, rule)) { 33 | return; 34 | } 35 | 36 | if (!(path in this.errors)) { 37 | this.errors[path] = {}; 38 | } 39 | if (!(rule in this.errors[path])) { 40 | this.errors[path][rule] = {}; 41 | } 42 | if (!(message in this.errors[path][rule])) { 43 | this.errors[path][rule][message] = []; 44 | } 45 | this.errors[path][rule][message].push(token); 46 | } 47 | 48 | generateBaseline() { 49 | const baseline = {}; 50 | Object.keys(this.errors).forEach((path) => { 51 | const normalPath = normalisePath(path); 52 | baseline[normalPath] = {}; 53 | Object.keys(this.errors[path]).forEach((r) => { 54 | const rule = this.errors[path][r]; 55 | const messages = Object.keys(rule); 56 | const count = messages.reduce((acc, msg) => acc + rule[msg].length, 0); 57 | baseline[normalPath][r] = count; 58 | }); 59 | }); 60 | 61 | return baseline; 62 | } 63 | } 64 | 65 | module.exports = Errors; 66 | -------------------------------------------------------------------------------- /src/stream.js: -------------------------------------------------------------------------------- 1 | // helper to parse string and keep track 2 | // of line numbers 3 | class Stream { 4 | constructor(string) { 5 | this.string = string; 6 | this.start = 0; 7 | this.pos = 0; 8 | this.line = 1; 9 | this.column = 1; 10 | } 11 | 12 | eof() { 13 | return (this.start === this.string.length); 14 | } 15 | 16 | next() { 17 | // update counters 18 | const lines = this.current().split(/\r?\n/); 19 | const newLines = lines.length - 1; 20 | if (newLines > 0) { 21 | this.line += lines.length - 1; 22 | this.column = lines[newLines].length + 1; 23 | } else { 24 | this.column += this.pos - this.start; 25 | } 26 | 27 | this.start = this.pos; 28 | } 29 | 30 | current() { 31 | return this.string.slice(this.start, this.pos); 32 | } 33 | 34 | eatString(pattern) { 35 | if (this.string.startsWith(pattern, this.pos)) { 36 | this.pos += pattern.length; 37 | return true; 38 | } 39 | 40 | return false; 41 | } 42 | 43 | eat(pattern) { 44 | if (typeof pattern === 'string') { 45 | return this.eatString(pattern); 46 | } 47 | 48 | const p = pattern; 49 | p.lastIndex = this.pos; 50 | const match = p.exec(this.string); 51 | if ((match === null) || (match.index !== this.pos)) { 52 | return false; 53 | } 54 | 55 | this.pos += match[0].length; 56 | return true; 57 | } 58 | 59 | eatUntil(pattern, include) { 60 | const p = pattern; 61 | p.lastIndex = this.pos; 62 | const match = p.exec(this.string); 63 | if ((match === null)) { 64 | // not found utill end of file 65 | this.pos = this.string.length; 66 | } else { 67 | this.pos = match.index; 68 | if (include) { 69 | this.pos += match[0].length; 70 | } 71 | } 72 | } 73 | } 74 | 75 | module.exports = Stream; 76 | -------------------------------------------------------------------------------- /tests/rules/loop-max-size.js: -------------------------------------------------------------------------------- 1 | const ruleTest = require('../../src/rule-test'); 2 | const rule = require('../../src/rules/loop-max-size'); 3 | 4 | const src = ` 5 | $value) { 8 | one(); 9 | two(); 10 | three(); 11 | } 12 | `; 13 | 14 | const src2 = ` 4 lines are comments 17 | * -> 3 lines are empty new lines 18 | * -> 2 lines are brackets 19 | */ 20 | const src2 = ` { 28 | const name = token.step().body(); 29 | prefixes.forEach((prefix) => { 30 | if (name.startsWith(prefix)) { 31 | report(`Function name can't start from ${prefix} [${name}].`, token.current()); 32 | } 33 | }); 34 | }); 35 | }, 36 | }; 37 | ``` 38 | First param of a function is a config from config file. 39 | Secon is an instance of [Tokens](api.md#module_tokenize..Tokens) class, 40 | it helps to navigate over ther tokens array. Assume we have next php 41 | file: 42 | 43 | ```php 44 | { 25 | // TODO: check why it catches not all exceptions 26 | console.error('Something unexpected happend'); 27 | console.error(err.stack); 28 | process.exitCode = 2; 29 | }); 30 | 31 | function run(ignore) { 32 | let config = {}; 33 | try { 34 | config = configLoader.load(ignore); 35 | } catch (err) { 36 | console.error(`Can't load config ${configLoader.fileName}`); 37 | console.error(err.message); 38 | return 2; 39 | } 40 | 41 | return lint(config); 42 | } 43 | 44 | function printErrors(errors) { 45 | const paths = Object.keys(errors); 46 | if (paths.length === 0) { 47 | return 0; 48 | } 49 | 50 | paths.forEach((path) => { 51 | Object.keys(errors[path]).forEach((rule) => { 52 | Object.keys(errors[path][rule]).forEach((message) => { 53 | const tokens = errors[path][rule][message]; 54 | tokens.forEach((token) => { 55 | const line = (token !== true) ? `:${token.line}:${token.column}` : ''; 56 | console.error(`${path}${line}`); 57 | console.error(` ${message}`); 58 | }); 59 | }); 60 | }); 61 | }); 62 | 63 | return 1; 64 | } 65 | 66 | if (help) { 67 | console.log(helpMsg); 68 | } else if (init) { 69 | configLoader.init(); 70 | console.info(`Created default config in ${configLoader.fileName}`); 71 | } else if (saveBaseLine) { 72 | const fileNameIndex = process.argv.indexOf('--save-base-line') + 1; 73 | const fileName = process.argv[fileNameIndex] || '.baseline.json'; 74 | run(true).then((errors) => { 75 | const baseline = errors.generateBaseline(); 76 | const data = stringify(baseline); 77 | fs.writeFileSync(fileName, data); 78 | }); 79 | } else { 80 | run(disableBaseLine).then((errors) => { 81 | process.exitCode = printErrors(errors.errors); 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /tests/rules/file-max-size.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-tabs */ 2 | const ruleTest = require('../../src/rule-test'); 3 | const rule = require('../../src/rules/file-max-size'); 4 | 5 | const src = ` 6 | first(); 7 | second(); 8 | third(); 9 | `; 10 | 11 | const src2 = ` typesOnLine.every((tokenType) => tokenType === types.whitespace)) 39 | .length; 40 | 41 | // Count lines including empty lines. Excludes comments if `include-comments` is falsy. 42 | const lines = new Set(); 43 | while (tokens.pos <= endToken.pos && tokens.type() !== types.eof) { 44 | tokens.body().split(/\r?\n/).forEach((_, i) => { 45 | if (tokens.type() === types.whitespace) { 46 | return; 47 | } 48 | if (tokens.type() === types.comment && !comments) { 49 | return; 50 | } 51 | if ((tokens.type() === types.bracket && !brackets)) { 52 | return; 53 | } 54 | lines.add(tokens.current().line + i); 55 | }); 56 | tokens.step(false, comments || emptyLines); 57 | } 58 | 59 | if (tokens.current().type === types.eof) { 60 | tokens.step(true); 61 | } 62 | // Count the total amount of lines based on the settings 63 | // +1 to include the first line. 64 | let lineCount = tokens.current().line - startToken.current().line + 1; 65 | if (!emptyLines || !comments || !brackets) { 66 | lineCount = lines.size; 67 | if (emptyLines) { 68 | lineCount += emptyLineCount; 69 | } 70 | } 71 | 72 | return { 73 | lineCount, 74 | currentToken: tokens.current(), 75 | }; 76 | } 77 | 78 | module.exports = countLines; 79 | -------------------------------------------------------------------------------- /docs/rules.md: -------------------------------------------------------------------------------- 1 | # Rules and default configuration 2 | 3 | ## How to disable a specific rule? 4 | 5 | ```JSON 6 | "namespace-max-files": false, 7 | ``` 8 | 9 | ## List of rules: 10 | 11 | ### namespace-max-files 12 | 13 | Avoid namespaces with 15+ files: 14 | 15 | ```JSON 16 | "namespace-max-files": 15 17 | ``` 18 | 19 | ### argument-override 20 | 21 | Avoid overriding of a function's arguments: 22 | 23 | ```JSON 24 | "argument-override": { 25 | "allow-pass-by-reference": true, 26 | }, 27 | ``` 28 | 29 | ### file-max-size 30 | 31 | Avoid files with more than 200 lines: 32 | 33 | ```JSON 34 | "file-max-size": { 35 | "max-lines": 200, 36 | "include-comments": true, 37 | "include-empty-lines": true, 38 | "include-brackets": true 39 | }, 40 | ``` 41 | 42 | ### empty-catch 43 | 44 | Avoid empty catch blocks: 45 | 46 | ```JSON 47 | "empty-catch": true 48 | ``` 49 | 50 | ### class-comment 51 | 52 | Avoid a class without a single comment at the top stating purpose: 53 | 54 | ```JSON 55 | "class-comment": true 56 | ``` 57 | 58 | ### forbidden-functions 59 | 60 | Avoid dangerous calls to `eval`, `print_r`, `var_export`, `var_dump`, `phpinfo`, `exec`, ... (forbidden function list): 61 | 62 | ```JSON 63 | "forbidden-functions": [ 64 | "eval", "print_r", "var_export", "var_dump", "phpinfo", "exec" 65 | ], 66 | ``` 67 | 68 | ### missing-braces 69 | 70 | Avoid an if statement or for loop without braces: 71 | 72 | ```JSON 73 | "missing-braces": true, 74 | ``` 75 | 76 | ### variable-length 77 | 78 | Avoid variables with names shorter than 3 letters (while whitelisting $i or $id) 79 | 80 | ```JSON 81 | "variable-length": { 82 | "min-length": 3, 83 | "whitelist": ["$id", "$i"] 84 | } 85 | ``` 86 | 87 | ### function-max-size 88 | 89 | Avoid any function longer than 50 lines: 90 | 91 | ```JSON 92 | "function-max-size": 50, 93 | ``` 94 | 95 | ### loop-max-size 96 | 97 | Aavoid loops with inside of them more than 15 lines (a block that should be a function): 98 | 99 | ```JSON 100 | "loop-max-size": 15, 101 | ``` 102 | 103 | ### forbidden-function-prefix 104 | 105 | Avoid function names starting with a certain pattern: 106 | 107 | ```JSON 108 | "forbidden-function-prefix": ["check"], 109 | ``` 110 | 111 | ### if-assigment 112 | 113 | Avoid assignment inside of an if statement: 114 | 115 | ```JSON 116 | "if-assigment": true, 117 | ``` 118 | 119 | ### complex-if 120 | 121 | Avoid complicated ifs (eg more than 2 `&&`, combination of `&&` and `||`): 122 | 123 | ```JSON 124 | "complex-if": true, 125 | ``` 126 | 127 | ### ternary-max-length 128 | 129 | Avoid ternary operator combined with line length exceeding 50 chars: 130 | 131 | ```JSON 132 | "ternary-max-length": 50, 133 | ``` 134 | 135 | ### loop-max-nest 136 | 137 | Avoid triple inner for/foreach (eg for within for within for loop): 138 | 139 | ```JSON 140 | "loop-max-nest": 2, 141 | ``` 142 | 143 | ### max-nest 144 | 145 | Avoid indent deeper than 4 (maximum block nesting, eg for loop within an if within a for loop within a for loop): 146 | 147 | ```JSON 148 | "max-nest": 3, 149 | ``` 150 | -------------------------------------------------------------------------------- /tests/tokenize.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const { tokenize } = require('../src/tokenize'); 3 | 4 | function hasToken(tokens, token) { 5 | let found = false; 6 | tokens.matchAll(token, () => { 7 | found = true; 8 | }); 9 | 10 | return found; 11 | } 12 | 13 | test('tokenize track lines and columns', (t) => { 14 | const tokens = tokenize(`first 15 | 16 | third 17 | forth`).array; 18 | 19 | const last = tokens[tokens.length - 1]; 20 | t.is(last.column, 2); 21 | t.is(last.line, 4); 22 | }); 23 | 24 | test('tokenize detects comments', (t) => { 25 | const tokens = tokenize(` 26 | // comment 27 | /* 28 | multiline comment 29 | */ 30 | # old style`); 31 | t.is(tokens.array[tokens.array.length - 1].body, '# old style'); 32 | }); 33 | 34 | test('tokenize string', (t) => { 35 | const tokens = tokenize(` 36 | "double \\" "; 37 | 'single \\' '; 38 | "\\\\"; 39 | `); 40 | 41 | t.true(hasToken(tokens, '"double \\" "')); 42 | t.true(hasToken(tokens, "'single \\' '")); 43 | t.true(hasToken(tokens, '"\\\\"')); 44 | }); 45 | 46 | test('tokenize empty strings', (t) => { 47 | const tokens = tokenize(` 48 | $a = ""; 49 | $a = ''; 50 | `); 51 | 52 | t.true(hasToken(tokens, '""')); 53 | t.true(hasToken(tokens, "''")); 54 | }); 55 | 56 | 57 | test('tokenize numbers', (t) => { 58 | const tokens = tokenize(` 59 | // yes, this is a valid php numbers in 7.4 60 | 1_234.567 61 | 6.674_083e-11 62 | 0xCAFE_F00D 63 | `); 64 | 65 | t.true(hasToken(tokens, '1_234.567')); 66 | }); 67 | 68 | test('tokenize operators', (t) => { 69 | const tokens = tokenize(` 70 | if( !isset($city['id'])) 71 | { 72 | for($i = 2005; $i <= $year_end;$i++){ 73 | $a=$b/$c; 74 | } 75 | }`); 76 | 77 | t.true(hasToken(tokens, 'isset')); 78 | t.true(hasToken(tokens, '!')); 79 | t.true(hasToken(tokens, ';')); 80 | t.true(hasToken(tokens, '/')); 81 | }); 82 | 83 | test('tokenize detects function calls', (t) => { 84 | const tokens = tokenize('var_dump($var);'); 85 | t.true(hasToken(tokens, 'var_dump')); 86 | }); 87 | 88 | test('tokenize detects variables', (t) => { 89 | const tokens = tokenize('var_dump($var);'); 90 | t.true(hasToken(tokens, '$var')); 91 | }); 92 | 93 | test('tokenize detects namespaces', (t) => { 94 | const tokens = tokenize('$a = \\b\\c::d();'); 95 | t.true(hasToken(tokens, '\\b\\c')); 96 | }); 97 | 98 | test('tokenize steps to', (t) => { 99 | const tokens = tokenize('one(); next(); last();'); 100 | 101 | t.is(tokens.stepTo('next').body(), 'next'); 102 | }); 103 | 104 | test('tokenize steps to close', (t) => { 105 | const tokens = tokenize('{ { } } }'); 106 | const closed = tokens.stepToClosing(); 107 | t.is(closed.current().column, 7); 108 | }); 109 | 110 | test('tokenize :: ', (t) => { 111 | const src = ` 112 | if($i == Auth::getUser()) 113 | { 114 | throw new Exception("null"); 115 | } 116 | `; 117 | const tokens = tokenize(src); 118 | t.true(hasToken(tokens, '::')); 119 | }); 120 | 121 | 122 | test('tokenize dot operator ', (t) => { 123 | const src = ` 124 | $msg = $mwg."some"; 125 | `; 126 | const tokens = tokenize(src); 127 | t.true(hasToken(tokens, '.')); 128 | }); 129 | -------------------------------------------------------------------------------- /src/lint.js: -------------------------------------------------------------------------------- 1 | 2 | /* eslint import/no-dynamic-require:off */ 3 | /* eslint global-require:off */ 4 | 5 | const { readFile } = require('fs').promises; 6 | const { flattenDeep } = require('lodash'); 7 | const allSettled = require('promise.allsettled'); 8 | const { dirsTree } = require('./utils.js'); 9 | const { tokenize } = require('./tokenize'); 10 | const Errors = require('./errors'); 11 | 12 | /** 13 | * @typedef {Object} NamespaceRule 14 | * @property {checkFiles} checkFiles function to check 15 | */ 16 | 17 | /** 18 | * Rule funtion to check the file tree 19 | * @callback checkFiles 20 | * @param {*} options passed from config file 21 | * @param {string[][]} files tree 22 | * @param {namespaseReport} report error callback 23 | */ 24 | 25 | /** 26 | * This callback is called by rule to log an error in files tree 27 | * @callback namespaseReport 28 | * @param {string} path where error occured 29 | * @param {string} messege 30 | */ 31 | 32 | /** 33 | * @typedef {Object} Rule 34 | * @property {check} check tokens stream for errors 35 | */ 36 | 37 | /** 38 | * Rule funtion to check the file tree 39 | * @callback check 40 | * @param {*} options passed from config file 41 | * @param {tokenize~Tokens} tokens 42 | * @param {report} report error callback 43 | */ 44 | 45 | /** 46 | * This callback is called by rule to log an error 47 | * @callback report 48 | * @param {string} messege 49 | * @param {tokenize~Token} token where error occured 50 | */ 51 | 52 | // load rules from config 53 | function loadRules(rulesConfig) { 54 | const namespace = []; 55 | const file = []; 56 | 57 | Object.keys(rulesConfig).forEach((name) => { 58 | if (rulesConfig[name] === false) { 59 | return; 60 | } 61 | 62 | const rule = { 63 | name, 64 | module: require(`./rules/${name}`), 65 | config: rulesConfig[name], 66 | }; 67 | 68 | if (typeof rule.module.checkFiles === 'function') { 69 | // namespace scope of rule 70 | namespace.push(rule); 71 | } else { 72 | file.push(rule); 73 | } 74 | }); 75 | 76 | return { 77 | namespace, 78 | file, 79 | }; 80 | } 81 | 82 | 83 | // main lint 84 | async function lint(config) { 85 | const rules = loadRules(config.rules); 86 | const files = dirsTree(config.paths, '.php'); 87 | 88 | const errors = new Errors(config.baseline); 89 | // rules with namespace scope 90 | rules.namespace.forEach( 91 | (rule) => rule.module.checkFiles(rule.config, files, (path, message) => { 92 | errors.report(path, rule.name, message, true); 93 | }), 94 | ); 95 | 96 | const promises = flattenDeep(files).map((fileName) => readFile(fileName, 'utf8').then((result) => { 97 | const tokens = tokenize(result); 98 | if (tokens === false) { 99 | errors.report(fileName, 'Cant parse file'); 100 | return; 101 | } 102 | rules.file.forEach( 103 | (rule) => rule.module.check(rule.config, tokens, (message, token) => { 104 | errors.report(fileName, rule.name, message, token); 105 | }), 106 | ); 107 | })); 108 | 109 | return allSettled(promises).then(() => errors); 110 | } 111 | 112 | module.exports = lint; 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![readable](docs/logo.png) 2 | 3 | PHP code analyzer, similar to eslint, but with a focus on readability. The idea is to build an analyzer that can guarantee long term code maintainability for a project. The main project values are extendibility, ease of use, speed of analysis and simplicity. 4 | 5 | quick links: [Rules and default configuration](docs/rules.md) 6 | 7 | [![codecov](https://codecov.io/gh/officient/readable/branch/master/graph/badge.svg)](https://codecov.io/gh/officient/readable) 8 | 9 | [![Codeship Status for officient/readable](https://app.codeship.com/projects/4fd4eea0-676f-0138-8ef4-52f6c3762b41/status?branch=master)](https://app.codeship.com/projects/393877) 10 | 11 | ### Installation and Usage 12 | 13 | You can install readable using npm: 14 | 15 | $ npm install @officient/readable --save-dev 16 | 17 | You can get the help on usage: 18 | 19 | $ npx readable --help 20 | 21 | You should then set up a configuration file: 22 | 23 | $ npx readable --init 24 | 25 | After that, you can run readable on any file or directory like this: 26 | 27 | $ npx readable 28 | 29 | You can also add it to your NPM scripts: 30 | 31 | ```json 32 | "scripts": { 33 | "test": "readable" 34 | }, 35 | ``` 36 | 37 | $ npm run test 38 | 39 | Also you can use our docker-image: 40 | 41 | $ docker run --rm \ 42 | -v "$(pwd):/rootfs:ro" \ 43 | --user "$(id -u):$(id -g)" \ 44 | -w /rootfs \ 45 | officient/readable:latest 46 | 47 | ### Exit code 48 | 49 |
50 |
0
51 |
No errors
52 |
1
53 |
Found errors
54 |
2
55 |
Unexpected behaviour
56 |
57 | 58 | ### Configuration 59 | 60 | After running `readable --init`, you'll have a `.readable.json` file in your directory. In it, you'll see some rules configured like this: 61 | 62 | ```JSON 63 | { 64 | "paths": [ 65 | "src/", 66 | "!src/vendor/" 67 | ], 68 | "rules": {} 69 | } 70 | ``` 71 | 72 | Start a path with `!` to ignore the folder. 73 | 74 | ## Baseline 75 | 76 | If you have a bunch of errors and you don't want to fix them all 77 | at once, readable can ignore errors in existing code, while 78 | ensuring that new code doesn't have errors: 79 | 80 | $ npx readable --save-base-line .baseline.json 81 | 82 | will generate or update `.baseline.json` file containing the 83 | current errors. Add `"baseline"` param to your `.readable.json:` 84 | 85 | ```JSON 86 | { 87 | "baseline": ".baseline.json", 88 | "..." 89 | } 90 | ``` 91 | 92 | You can commit the changes so that readable running in other 93 | places (e.g. CI) won't complain about those errors. If you want 94 | to see all errors run with `--disable-base-line` flag: 95 | 96 | $ npx readable --disable-base-line 97 | 98 | ## Rules 99 | 100 | See [Rules and default configuration](docs/rules.md) for default rules. 101 | Or read [how to create a custom rule](docs/add-rule.md). 102 | 103 | ### Development 104 | 105 | To lint code run: 106 | 107 | npm run lint 108 | 109 | To fix lint: 110 | 111 | $ npm run fix 112 | 113 | To test: 114 | 115 | $ npm run test 116 | 117 | To update api docs: 118 | 119 | $ npm run docs 120 | 121 | While developing you can update to latest master with 122 | 123 | $ npm install @officient/readable@latest 124 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at support@officient.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/tokenize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A module for tokenizing PHP code. 3 | * @module tokenize 4 | */ 5 | 6 | const Stream = require('./stream'); 7 | 8 | /** 9 | * Token types enum. 10 | * @readonly 11 | * @enum {number} 12 | */ 13 | const types = { 14 | whitespace: 0, 15 | comment: 1, 16 | label: 2, 17 | variable: 3, 18 | other: 4, 19 | bracket: 5, 20 | operator: 6, 21 | string: 7, 22 | number: 8, 23 | eof: 9, 24 | }; 25 | 26 | /** 27 | * @typedef Token 28 | * @type {object} 29 | * @property {string} type - token type 30 | * @property {string} body - token body 31 | * @property {number} line - token line 32 | * @property {number} column - token column 33 | */ 34 | 35 | /** 36 | * Class for navigation over array tokens 37 | */ 38 | class Tokens { 39 | constructor(tokens, pos) { 40 | this.array = tokens; 41 | this.pos = pos || 0; 42 | } 43 | 44 | /** 45 | * Is current token a code (not whitespace and comment) 46 | * @return {Boolean} 47 | */ 48 | isCode() { 49 | const type = this.type(); 50 | return (type !== types.whitespace) && (type !== types.comment); 51 | } 52 | 53 | /** 54 | * Moves current position 55 | * @param {Boolean} [backward] move backward 56 | * @param {Boolean} [includeAll] include comments and whitespace 57 | * @return {this} 58 | */ 59 | step(backward, includeAll) { 60 | const step = backward ? -1 : 1; 61 | do { 62 | this.pos += step; 63 | if (includeAll) { 64 | return this; 65 | } 66 | } while (!this.isCode()); 67 | 68 | return this; 69 | } 70 | 71 | /** 72 | * Check if current body matches string 73 | * or array of strings 74 | * @param {(string|string[])} 75 | * @return {Boolean} 76 | */ 77 | matches(strings) { 78 | if (typeof strings === 'string') { 79 | return this.body() === strings; 80 | } 81 | 82 | return strings.includes(this.body()); 83 | } 84 | 85 | /** 86 | * Steps to next occutance of strings 87 | * 88 | * @param {(string|string[])} 89 | * @param {tockensCallback} callback callback for each step 90 | * @return {this} 91 | */ 92 | stepTo(strings, callback) { 93 | do { 94 | this.step(); 95 | this.call(callback); 96 | } while (!(this.matches(strings) || this.type() === types.eof)); 97 | return this; 98 | } 99 | 100 | stepToEof() { 101 | while (this.type() !== types.eof) { 102 | this.step(); 103 | } 104 | return this; 105 | } 106 | 107 | /** 108 | * Steps to correct closing brace 109 | * @param {tockensCallback} callback callback for each step 110 | * 111 | * @return {this} 112 | */ 113 | stepToClosing(callback) { 114 | const pairs = { '{': '}', '[': ']', '(': ')' }; 115 | const open = this.body(); 116 | if (!(open in pairs)) { 117 | // do nothing if not brace 118 | this.call(callback); 119 | return this; 120 | } 121 | const close = pairs[open]; 122 | let level = 1; 123 | while (level > 0) { 124 | this.step(); 125 | if (this.body() === open) { 126 | level += 1; 127 | } 128 | if (this.body() === close) { 129 | level -= 1; 130 | } 131 | if (this.type() === types.eof) { 132 | // reached end of file 133 | break; 134 | } 135 | this.call(callback); 136 | } 137 | 138 | return this; 139 | } 140 | 141 | /** 142 | * Returns current token body 143 | * @return {string} 144 | */ 145 | body() { return this.current().body; } 146 | 147 | /** 148 | * Returns current token type 149 | * @return {string} 150 | */ 151 | type() { return this.current().type; } 152 | 153 | copy() { return new Tokens(this.array, this.pos); } 154 | 155 | /** 156 | * Returns current token 157 | * @return {Token} 158 | */ 159 | current() { 160 | if ((this.pos < 0) || (this.pos >= this.array.length)) { 161 | return { body: '', type: types.eof }; 162 | } 163 | 164 | return this.array[this.pos]; 165 | } 166 | 167 | /** 168 | * Call callback preserving current position; 169 | * @param {tockensCallback} callback 170 | */ 171 | call(callback) { 172 | if (callback) { 173 | const { pos } = this; 174 | callback(this); 175 | this.pos = pos; 176 | } 177 | } 178 | 179 | forEach(callback) { 180 | const { pos } = this; 181 | for (let i = 0; i < this.array.length; i += 1) { 182 | this.pos = i; 183 | if (this.isCode()) { 184 | callback(this); 185 | } 186 | } 187 | // keep position 188 | this.pos = pos; 189 | } 190 | 191 | /** 192 | * This callback is called by matchAll and for Each 193 | * @callback tockensCallback 194 | * @param {Tokens} tokens 195 | */ 196 | 197 | /** 198 | * Match all occurances of string or array of 199 | * string 200 | * 201 | * @param {(string|string[])} 202 | * @param {tockensCallback} callback function 203 | */ 204 | matchAll(strings, callback) { 205 | const array = (typeof strings === 'string') ? [strings] : strings; 206 | 207 | this.forEach((tokens) => { 208 | const body = tokens.body(); 209 | if (array.includes(body)) { 210 | callback(tokens); 211 | } 212 | }); 213 | } 214 | } 215 | 216 | const space = /\s+/g; 217 | const nonSpace = /\S+/g; 218 | const brackets = /[[\]{}()]/g; 219 | // from official PHP docs 220 | const label = /[a-zA-Z_\u0080-\u00ff\\][a-zA-Z0-9_\u0080-\u00ff\\]*/g; 221 | const operators = /[.*+/\-%!^&|?><>=@:]+/g; 222 | const separators = /[,;]/g; 223 | const number = /[0-9][0-9._]*/g; 224 | // terminators 225 | const enfOfLine = /[\r\n]/gm; 226 | const endOfComment = /\*\//gm; 227 | const enfOfQuote = /((\\\\|[^\\])")/gm; 228 | const endOfSingleQuote = /((\\\\|[^\\])')/gm; 229 | 230 | function readToken(stream) { 231 | if (stream.eat('\r') || stream.eat('\n')) { 232 | return types.whitespace; 233 | } 234 | 235 | if (stream.eat(space)) { 236 | return types.whitespace; 237 | } 238 | 239 | if (stream.eat('//') || stream.eat('#')) { 240 | stream.eatUntil(enfOfLine); 241 | return types.comment; 242 | } 243 | 244 | if (stream.eat('/*')) { 245 | stream.eatUntil(endOfComment, true); 246 | return types.comment; 247 | } 248 | 249 | if (stream.eat(brackets)) { 250 | return types.bracket; 251 | } 252 | 253 | if (stream.eat(label)) { 254 | return types.label; 255 | } 256 | 257 | if (stream.eat('$')) { 258 | stream.eat(label); 259 | return types.variable; 260 | } 261 | 262 | if (stream.eat('""') || stream.eat("''")) { 263 | return types.string; 264 | } 265 | 266 | if (stream.eat('"')) { 267 | stream.eatUntil(enfOfQuote, true); 268 | return types.string; 269 | } 270 | 271 | if (stream.eat("'")) { 272 | stream.eatUntil(endOfSingleQuote, true); 273 | return types.string; 274 | } 275 | 276 | if (stream.eat(number)) { 277 | return types.number; 278 | } 279 | 280 | if (stream.eat(operators)) { 281 | return types.operator; 282 | } 283 | 284 | if (stream.eat(separators)) { 285 | return types.operator; 286 | } 287 | 288 | if (stream.eat(nonSpace)) { 289 | return types.other; 290 | } 291 | 292 | return false; 293 | } 294 | 295 | function tokenize(str) { 296 | const stream = new Stream(str); 297 | const tokens = []; 298 | 299 | while (!stream.eof()) { 300 | const token = { 301 | line: stream.line, 302 | column: stream.column, 303 | }; 304 | token.type = readToken(stream); 305 | if (token.type === false) { 306 | return false; 307 | } 308 | token.body = stream.current(); 309 | 310 | tokens.push(token); 311 | stream.next(); 312 | } 313 | return new Tokens(tokens); 314 | } 315 | 316 | module.exports = { 317 | types, 318 | tokenize, 319 | }; 320 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | ## Modules 2 | 3 |
4 |
tokenize
5 |

A module for tokenizing PHP code.

6 |
7 |
8 | 9 | ## Typedefs 10 | 11 |
12 |
NamespaceRule : Object
13 |
14 |
checkFiles : function
15 |

Rule funtion to check the file tree

16 |
17 |
namespaseReport : function
18 |

This callback is called by rule to log an error in files tree

19 |
20 |
Rule : Object
21 |
22 |
check : function
23 |

Rule funtion to check the file tree

24 |
25 |
report : function
26 |

This callback is called by rule to log an error

27 |
28 |
29 | 30 | 31 | 32 | ## tokenize 33 | A module for tokenizing PHP code. 34 | 35 | 36 | * [tokenize](#module_tokenize) 37 | * [~Tokens](#module_tokenize..Tokens) 38 | * [.isCode()](#module_tokenize..Tokens+isCode) ⇒ Boolean 39 | * [.step([backward], [includeAll])](#module_tokenize..Tokens+step) ⇒ this 40 | * [.matches(strings)](#module_tokenize..Tokens+matches) ⇒ Boolean 41 | * [.stepTo(strings, callback)](#module_tokenize..Tokens+stepTo) ⇒ this 42 | * [.stepToClosing(callback)](#module_tokenize..Tokens+stepToClosing) ⇒ this 43 | * [.body()](#module_tokenize..Tokens+body) ⇒ string 44 | * [.type()](#module_tokenize..Tokens+type) ⇒ string 45 | * [.current()](#module_tokenize..Tokens+current) ⇒ Token 46 | * [.call(callback)](#module_tokenize..Tokens+call) 47 | * [.matchAll(strings, callback)](#module_tokenize..Tokens+matchAll) 48 | * [~types](#module_tokenize..types) : enum 49 | * [~Token](#module_tokenize..Token) : object 50 | * [~tockensCallback](#module_tokenize..tockensCallback) : function 51 | 52 | 53 | 54 | ### tokenize~Tokens 55 | Class for navigation over array tokens 56 | 57 | **Kind**: inner class of [tokenize](#module_tokenize) 58 | 59 | * [~Tokens](#module_tokenize..Tokens) 60 | * [.isCode()](#module_tokenize..Tokens+isCode) ⇒ Boolean 61 | * [.step([backward], [includeAll])](#module_tokenize..Tokens+step) ⇒ this 62 | * [.matches(strings)](#module_tokenize..Tokens+matches) ⇒ Boolean 63 | * [.stepTo(strings, callback)](#module_tokenize..Tokens+stepTo) ⇒ this 64 | * [.stepToClosing(callback)](#module_tokenize..Tokens+stepToClosing) ⇒ this 65 | * [.body()](#module_tokenize..Tokens+body) ⇒ string 66 | * [.type()](#module_tokenize..Tokens+type) ⇒ string 67 | * [.current()](#module_tokenize..Tokens+current) ⇒ Token 68 | * [.call(callback)](#module_tokenize..Tokens+call) 69 | * [.matchAll(strings, callback)](#module_tokenize..Tokens+matchAll) 70 | 71 | 72 | 73 | #### tokens.isCode() ⇒ Boolean 74 | Is current token a code (not whitespace and comment) 75 | 76 | **Kind**: instance method of [Tokens](#module_tokenize..Tokens) 77 | 78 | 79 | #### tokens.step([backward], [includeAll]) ⇒ this 80 | Moves current position 81 | 82 | **Kind**: instance method of [Tokens](#module_tokenize..Tokens) 83 | 84 | | Param | Type | Description | 85 | | --- | --- | --- | 86 | | [backward] | Boolean | move backward | 87 | | [includeAll] | Boolean | include comments and whitespace | 88 | 89 | 90 | 91 | #### tokens.matches(strings) ⇒ Boolean 92 | Check if current body matches string 93 | or array of strings 94 | 95 | **Kind**: instance method of [Tokens](#module_tokenize..Tokens) 96 | 97 | | Param | Type | 98 | | --- | --- | 99 | | strings | string \| Array.<string> | 100 | 101 | 102 | 103 | #### tokens.stepTo(strings, callback) ⇒ this 104 | Steps to next occutance of strings 105 | 106 | **Kind**: instance method of [Tokens](#module_tokenize..Tokens) 107 | 108 | | Param | Type | Description | 109 | | --- | --- | --- | 110 | | strings | string \| Array.<string> | | 111 | | callback | tockensCallback | callback for each step | 112 | 113 | 114 | 115 | #### tokens.stepToClosing(callback) ⇒ this 116 | Steps to correct closing brace 117 | 118 | **Kind**: instance method of [Tokens](#module_tokenize..Tokens) 119 | 120 | | Param | Type | Description | 121 | | --- | --- | --- | 122 | | callback | tockensCallback | callback for each step | 123 | 124 | 125 | 126 | #### tokens.body() ⇒ string 127 | Returns current token body 128 | 129 | **Kind**: instance method of [Tokens](#module_tokenize..Tokens) 130 | 131 | 132 | #### tokens.type() ⇒ string 133 | Returns current token type 134 | 135 | **Kind**: instance method of [Tokens](#module_tokenize..Tokens) 136 | 137 | 138 | #### tokens.current() ⇒ Token 139 | Returns current token 140 | 141 | **Kind**: instance method of [Tokens](#module_tokenize..Tokens) 142 | 143 | 144 | #### tokens.call(callback) 145 | Call callback preserving current position; 146 | 147 | **Kind**: instance method of [Tokens](#module_tokenize..Tokens) 148 | 149 | | Param | Type | 150 | | --- | --- | 151 | | callback | tockensCallback | 152 | 153 | 154 | 155 | #### tokens.matchAll(strings, callback) 156 | Match all occurances of string or array of 157 | string 158 | 159 | **Kind**: instance method of [Tokens](#module_tokenize..Tokens) 160 | 161 | | Param | Type | Description | 162 | | --- | --- | --- | 163 | | strings | string \| Array.<string> | | 164 | | callback | tockensCallback | function | 165 | 166 | 167 | 168 | ### tokenize~types : enum 169 | Token types enum. 170 | 171 | **Kind**: inner enum of [tokenize](#module_tokenize) 172 | **Read only**: true 173 | **Properties** 174 | 175 | | Name | Type | Default | 176 | | --- | --- | --- | 177 | | whitespace | number | 0 | 178 | | comment | number | 1 | 179 | | label | number | 2 | 180 | | variable | number | 3 | 181 | | other | number | 4 | 182 | | bracket | number | 5 | 183 | | operator | number | 6 | 184 | | string | number | 7 | 185 | | number | number | 8 | 186 | | eof | number | 9 | 187 | 188 | 189 | 190 | ### tokenize~Token : object 191 | **Kind**: inner typedef of [tokenize](#module_tokenize) 192 | **Properties** 193 | 194 | | Name | Type | Description | 195 | | --- | --- | --- | 196 | | type | string | token type | 197 | | body | string | token body | 198 | | line | number | token line | 199 | | column | number | token column | 200 | 201 | 202 | 203 | ### tokenize~tockensCallback : function 204 | This callback is called by matchAll and for Each 205 | 206 | **Kind**: inner typedef of [tokenize](#module_tokenize) 207 | 208 | | Param | Type | 209 | | --- | --- | 210 | | tokens | Tokens | 211 | 212 | 213 | 214 | ## NamespaceRule : Object 215 | **Kind**: global typedef 216 | **Properties** 217 | 218 | | Name | Type | Description | 219 | | --- | --- | --- | 220 | | checkFiles | [checkFiles](#checkFiles) | function to check | 221 | 222 | 223 | 224 | ## checkFiles : function 225 | Rule funtion to check the file tree 226 | 227 | **Kind**: global typedef 228 | 229 | | Param | Type | Description | 230 | | --- | --- | --- | 231 | | options | \* | passed from config file | 232 | | files | Array.<Array.<string>> | tree | 233 | | report | [namespaseReport](#namespaseReport) | error callback | 234 | 235 | 236 | 237 | ## namespaseReport : function 238 | This callback is called by rule to log an error in files tree 239 | 240 | **Kind**: global typedef 241 | 242 | | Param | Type | Description | 243 | | --- | --- | --- | 244 | | path | string | where error occured | 245 | | messege | string | | 246 | 247 | 248 | 249 | ## Rule : Object 250 | **Kind**: global typedef 251 | **Properties** 252 | 253 | | Name | Type | Description | 254 | | --- | --- | --- | 255 | | check | [check](#check) | tokens stream for errors | 256 | 257 | 258 | 259 | ## check : function 260 | Rule funtion to check the file tree 261 | 262 | **Kind**: global typedef 263 | 264 | | Param | Type | Description | 265 | | --- | --- | --- | 266 | | options | \* | passed from config file | 267 | | tokens | tokenize~Tokens | | 268 | | report | [report](#report) | error callback | 269 | 270 | 271 | 272 | ## report : function 273 | This callback is called by rule to log an error 274 | 275 | **Kind**: global typedef 276 | 277 | | Param | Type | Description | 278 | | --- | --- | --- | 279 | | messege | string | | 280 | | token | tokenize~Token | where error occured | 281 | 282 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------