├── .eslintignore ├── test ├── .eslintignore ├── unit │ ├── fixtures │ │ └── parse-jsdoc │ │ │ ├── src │ │ │ ├── optional-parameter.js │ │ │ ├── parameter-parametrized.js │ │ │ ├── no-parameters.js │ │ │ ├── no-return.js │ │ │ ├── optional-parameter-closure-compiler-style.js │ │ │ └── basic-doc.js │ │ │ └── expected │ │ │ ├── no-return.js │ │ │ ├── no-parameters.js │ │ │ ├── basic-doc.js │ │ │ ├── optional-parameter.js │ │ │ ├── parameter-parametrized.js │ │ │ └── optional-parameter-closure-compiler-style.js │ └── parse-jsdoc.spec.js ├── .eslintrc ├── smoke │ ├── initialization.js │ ├── fixtures │ │ ├── directive-parsing │ │ │ ├── src │ │ │ │ ├── defaults-global-directive-before-function.js │ │ │ │ ├── defaults-no-directive.js │ │ │ │ ├── defaults-directive-in-function.js │ │ │ │ ├── defaults-collision-between-global-and-local.js │ │ │ │ └── defaults-global-directive.js │ │ │ └── expected │ │ │ │ ├── defaults-global-directive-before-function.js │ │ │ │ ├── defaults-no-directive.js │ │ │ │ ├── defaults-collision-between-global-and-local.js │ │ │ │ ├── defaults-directive-in-function.js │ │ │ │ └── defaults-global-directive.js │ │ ├── runtime │ │ │ ├── expected │ │ │ │ ├── wrong-rest-argument.txt │ │ │ │ ├── wrong-simple-argument.txt │ │ │ │ ├── wrong-optional-argument.txt │ │ │ │ ├── wrong-validate-in-scope.txt │ │ │ │ ├── wrong-array-type-application-argument.txt │ │ │ │ ├── wrong-object-type-application-argument.txt │ │ │ │ ├── wrong-record-argument.txt │ │ │ │ ├── wrong-arguments-union.txt │ │ │ │ └── wrong-object-type-application-arguments-multiline-jsdoc.txt │ │ │ └── src │ │ │ │ ├── exception │ │ │ │ ├── wrong-array-type-application-argument.js │ │ │ │ ├── wrong-rest-argument.js │ │ │ │ ├── wrong-simple-argument.js │ │ │ │ ├── wrong-optional-argument.js │ │ │ │ ├── wrong-object-type-application-argument.js │ │ │ │ ├── wrong-validate-in-scope.js │ │ │ │ ├── wrong-arguments-union.js │ │ │ │ ├── wrong-record-argument.js │ │ │ │ └── wrong-object-type-application-arguments-multiline-jsdoc.js │ │ │ │ └── no-exception │ │ │ │ └── siblings-object-null-validation.js │ │ ├── strict-mode │ │ │ ├── no-exception │ │ │ │ ├── return-type-any.js │ │ │ │ ├── return-empty-with-no-type-annotation.js │ │ │ │ └── argument-type-any.js │ │ │ └── exception │ │ │ │ ├── return-no-jsdoc.js │ │ │ │ ├── argument-no-jsdoc.js │ │ │ │ ├── argument-no-type-definition.js │ │ │ │ ├── return-empty.js │ │ │ │ └── argument-unused-jsdoc.js │ │ └── assertion-inject │ │ │ ├── src │ │ │ ├── without-type-definition.js │ │ │ ├── arrow-function-expression.js │ │ │ ├── arrow-function.js │ │ │ ├── arguments-function-type.js │ │ │ ├── construtor-in-scope.js │ │ │ ├── without-return.js │ │ │ ├── without-typecheck.js │ │ │ ├── empty-return.js │ │ │ ├── class-constructor.js │ │ │ ├── arguments-rest.js │ │ │ ├── variable-function.js │ │ │ ├── arguments-array-destructuring.js │ │ │ ├── class-method.js │ │ │ ├── function-declaration.js │ │ │ ├── jsdoc-multiline-object.js │ │ │ ├── object-method.js │ │ │ ├── arguments-object-destructuring.js │ │ │ ├── class-return-in-statement.js │ │ │ ├── variable-class.js │ │ │ └── class-getter-setter.js │ │ │ ├── expected │ │ │ ├── without-typecheck.js │ │ │ ├── without-type-definition.js │ │ │ ├── arguments-function-type.js │ │ │ ├── construtor-in-scope.js │ │ │ ├── arrow-function.js │ │ │ ├── arrow-function-expression.js │ │ │ ├── class-constructor.js │ │ │ ├── without-return.js │ │ │ ├── class-return-in-statement.js │ │ │ ├── jsdoc-multiline-object.js │ │ │ ├── object-method.js │ │ │ ├── empty-return.js │ │ │ ├── arguments-rest.js │ │ │ ├── class-method.js │ │ │ ├── arguments-array-destructuring.js │ │ │ ├── variable-class.js │ │ │ ├── arguments-object-destructuring.js │ │ │ ├── class-getter-setter.js │ │ │ ├── variable-function.js │ │ │ └── function-declaration.js │ │ │ └── integration │ │ │ ├── without-typecheck.js │ │ │ ├── without-type-definition.js │ │ │ ├── arguments-function-type.js │ │ │ ├── arrow-function.js │ │ │ ├── arrow-function-expression.js │ │ │ ├── without-return.js │ │ │ ├── jsdoc-multiline-object.js │ │ │ ├── empty-return.js │ │ │ ├── object-method.js │ │ │ ├── construtor-in-scope.js │ │ │ ├── variable-function.js │ │ │ ├── function-declaration.js │ │ │ ├── class-constructor.js │ │ │ ├── arguments-rest.js │ │ │ ├── arguments-object-destructuring.js │ │ │ ├── arguments-array-destructuring.js │ │ │ ├── class-return-in-statement.js │ │ │ ├── variable-class.js │ │ │ ├── class-method.js │ │ │ └── class-getter-setter.js │ ├── helpers │ │ └── compare.js │ ├── assertion-inject.spec.js │ ├── directive-parsing.spec.js │ ├── strict-mode.spec.js │ └── runtime.spec.js ├── config.json ├── utils.js └── index.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── src ├── lib │ ├── typecheck-function-template │ │ ├── index.js │ │ └── function-call.js │ ├── normalize-function-body.js │ ├── normalize-validator.js │ ├── find-global-directive.js │ ├── strict-mode.js │ ├── check-function.js │ ├── find-comment.js │ ├── insert-parameters-check.js │ └── parse-jsdoc.js ├── visitors │ ├── return-statement.js │ ├── helpers.js │ └── index.js └── index.js ├── .babelrc ├── .editorconfig ├── shared ├── config.json └── helper-function-declaration.js ├── .eslintrc ├── LICENSE ├── package.json ├── CHANGELOG.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/** 2 | -------------------------------------------------------------------------------- /test/.eslintignore: -------------------------------------------------------------------------------- 1 | **/fixtures/** 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | TODO.md 4 | npm-debug.log 5 | 6 | dist/ 7 | sandbox/ 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | TODO.md 4 | npm-debug.log 5 | 6 | src/ 7 | sandbox/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7.8" 4 | 5 | script: 6 | - npm run build:ci 7 | -------------------------------------------------------------------------------- /test/unit/fixtures/parse-jsdoc/src/optional-parameter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} [a] 3 | */ 4 | -------------------------------------------------------------------------------- /test/unit/fixtures/parse-jsdoc/src/parameter-parametrized.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Array} a 3 | */ 4 | -------------------------------------------------------------------------------- /test/unit/fixtures/parse-jsdoc/src/no-parameters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name test 3 | * @returns {String} 4 | */ 5 | -------------------------------------------------------------------------------- /test/unit/fixtures/parse-jsdoc/src/no-return.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | */ 5 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc", 3 | "env": { 4 | "mocha": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/unit/fixtures/parse-jsdoc/src/optional-parameter-closure-compiler-style.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number=} a 3 | */ 4 | -------------------------------------------------------------------------------- /test/unit/fixtures/parse-jsdoc/src/basic-doc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name testDoc 3 | * @param {Number} a 4 | * @returns {Number} a 5 | */ 6 | -------------------------------------------------------------------------------- /test/smoke/initialization.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | 3 | chai.use(require('chai-diff')); 4 | chai.config.truncateThreshold = 0; 5 | -------------------------------------------------------------------------------- /test/smoke/fixtures/directive-parsing/src/defaults-global-directive-before-function.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | function test(a) { 4 | return a; 5 | } 6 | -------------------------------------------------------------------------------- /test/smoke/fixtures/directive-parsing/expected/defaults-global-directive-before-function.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | function test(a) { 4 | return a; 5 | } 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/parse-jsdoc/expected/no-return.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parameters: { 3 | a: 'Number', 4 | b: 'Number' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /test/unit/fixtures/parse-jsdoc/expected/no-parameters.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'test', 3 | parameters: {}, 4 | returnStatement: 'String' 5 | }; 6 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/expected/wrong-rest-argument.txt: -------------------------------------------------------------------------------- 1 | Parameter "b" in function "test" has wrong type. 2 | Expected type: Number 3 | Current value: "2" 4 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/expected/wrong-simple-argument.txt: -------------------------------------------------------------------------------- 1 | Parameter "b" in function "test" has wrong type. 2 | Expected type: Number 3 | Current value: "2" 4 | -------------------------------------------------------------------------------- /test/smoke/fixtures/strict-mode/no-exception/return-type-any.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | /** 4 | * @returns {*} 5 | */ 6 | function test () { 7 | return 1; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/typecheck-function-template/index.js: -------------------------------------------------------------------------------- 1 | module.exports.function = require('../../../shared/helper-function-declaration'); 2 | module.exports.call = require('./function-call'); 3 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/expected/wrong-optional-argument.txt: -------------------------------------------------------------------------------- 1 | Parameter "b" in function "test" has wrong type. 2 | Expected type: Number (optional) 3 | Current value: "2" 4 | 5 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/expected/wrong-validate-in-scope.txt: -------------------------------------------------------------------------------- 1 | Parameter "data" in function "testFunction" has wrong type. 2 | Expected type: Test 3 | Current value: "wrong data" 4 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/without-type-definition.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param a 3 | * @returns {Number} 4 | * @typecheck 5 | */ 6 | function firstFunction(a) { 7 | return a; 8 | } 9 | -------------------------------------------------------------------------------- /test/smoke/fixtures/strict-mode/exception/return-no-jsdoc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @typecheck 5 | */ 6 | function test(a, b) { 7 | return a + b; 8 | } 9 | -------------------------------------------------------------------------------- /test/unit/fixtures/parse-jsdoc/expected/basic-doc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'testDoc', 3 | parameters: { 4 | a: 'Number' 5 | }, 6 | returnStatement: 'Number' 7 | }; 8 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/arrow-function-expression.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @returns {Number} 4 | * @typecheck 5 | */ 6 | let arrowFunctionExpression = a => a * 2; 7 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/expected/wrong-array-type-application-argument.txt: -------------------------------------------------------------------------------- 1 | Parameter "arr" in function "test" has wrong type. 2 | Expected type: Array 3 | Current value: "[1,"2"]" 4 | -------------------------------------------------------------------------------- /test/smoke/fixtures/strict-mode/exception/argument-no-jsdoc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @returns {Number} 4 | * @typecheck 5 | */ 6 | function test(a, b) { 7 | return a + b; 8 | } 9 | -------------------------------------------------------------------------------- /test/smoke/fixtures/strict-mode/exception/argument-no-type-definition.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param a 3 | * @returns {Number} 4 | * @typecheck 5 | */ 6 | function test(a, b) { 7 | return a + b; 8 | } 9 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/arrow-function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @returns {Number} 4 | * @typecheck 5 | */ 6 | let arrowFunction = (a) => { 7 | return a * 2; 8 | }; 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "loose": true, 5 | "targets": { 6 | "node": 4.7 7 | } 8 | }] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/expected/wrong-object-type-application-argument.txt: -------------------------------------------------------------------------------- 1 | Parameter "obj" in function "test" has wrong type. 2 | Expected type: Object 3 | Current value: "{"a":1,"b":"2"}" 4 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/expected/wrong-record-argument.txt: -------------------------------------------------------------------------------- 1 | Parameter "record" in function "test" has wrong type. 2 | Expected type: {"a":"Number","b":"Number"} 3 | Current value: "{"a":1,"b":"2"}" 4 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/expected/wrong-arguments-union.txt: -------------------------------------------------------------------------------- 1 | Parameter "data" in function "test" has wrong type. 2 | Expected type: {"id":"Number","name":"String"}|null 3 | Current value: "{"id":"2"}" 4 | -------------------------------------------------------------------------------- /test/smoke/fixtures/strict-mode/no-exception/return-empty-with-no-type-annotation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @typecheck 5 | */ 6 | function test(a, b) { 7 | return; 8 | } 9 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/arguments-function-type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {function()} callback 3 | * @returns {*} 4 | * @typecheck 5 | */ 6 | function test(callback) { 7 | return callback(); 8 | } 9 | -------------------------------------------------------------------------------- /test/smoke/fixtures/strict-mode/no-exception/argument-type-any.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | /** 4 | * @param {*} a 5 | * @returns {Number} 6 | */ 7 | function test(a) { 8 | return parseInt(a, 10); 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/fixtures/parse-jsdoc/expected/optional-parameter.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parameters: { 3 | a: { 4 | optional: true, 5 | parameter: 'Number' 6 | } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/construtor-in-scope.js: -------------------------------------------------------------------------------- 1 | class Test {} 2 | 3 | /** 4 | * @param {Test} a 5 | * @returns {Test} 6 | * @typecheck 7 | */ 8 | function test(a) { 9 | return a; 10 | } 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/strict-mode/exception/return-empty.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @returns {Number} 5 | * @typecheck 6 | */ 7 | function test(a, b) { 8 | return; 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/fixtures/parse-jsdoc/expected/parameter-parametrized.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parameters: { 3 | a: { 4 | root: 'Array', 5 | children: ['Number'] 6 | } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/without-return.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @typecheck 6 | */ 7 | function test(a, b, c) { 8 | let d = a + b + c; 9 | } 10 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/without-typecheck.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | */ 7 | function test(a, b, c) { 8 | return a + b + c; 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/fixtures/parse-jsdoc/expected/optional-parameter-closure-compiler-style.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parameters: { 3 | a: { 4 | optional: true, 5 | parameter: 'Number' 6 | } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/without-typecheck.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | */ 7 | function test(a, b, c) { 8 | return a + b + c; 9 | } 10 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/empty-return.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function test(a, b, c) { 9 | return; 10 | } 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/directive-parsing/src/defaults-no-directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | */ 7 | function test(a, b, c) { 8 | return a + b + c; 9 | } 10 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/expected/wrong-object-type-application-arguments-multiline-jsdoc.txt: -------------------------------------------------------------------------------- 1 | Parameter "data" in function "test" has wrong type. 2 | Expected type: {"a":"Number","b":"Number"} 3 | Current value: "{"a":1,"c":"2"}" 4 | 5 | -------------------------------------------------------------------------------- /test/smoke/fixtures/directive-parsing/expected/defaults-no-directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | */ 7 | function test(a, b, c) { 8 | return a + b + c; 9 | } 10 | -------------------------------------------------------------------------------- /test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": { 3 | "smokeTest": "./test/smoke", 4 | "smokeTestData": "./test/smoke/fixtures", 5 | "unitTest": "./test/unit", 6 | "unitTestData": "./test/unit/fixtures", 7 | "plugin": "./src/index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/without-type-definition.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param a 3 | * @returns {Number} 4 | * @typecheck 5 | */ 6 | function firstFunction(a) { 7 | return __executeTypecheck__("firstFunction", "return", a, "\"Number\""); 8 | } 9 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/class-constructor.js: -------------------------------------------------------------------------------- 1 | class Test { 2 | /** 3 | * @param {Number} a 4 | * @param {Number} b 5 | * @typecheck 6 | */ 7 | constructor(a, b) { 8 | this._c = a + b; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/strict-mode/exception/argument-unused-jsdoc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function test(a, b) { 9 | return a + b; 10 | } 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/src/exception/wrong-array-type-application-argument.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | /** 4 | * @param {Array} arr 5 | * @returns {Number} 6 | */ 7 | function test(arr) { 8 | return arr[0]; 9 | } 10 | 11 | test([1, '2']); 12 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/src/exception/wrong-rest-argument.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | /** 4 | * @param {Number} a 5 | * @param {Number} b 6 | * @returns {Number} 7 | */ 8 | function test([a, b]) { 9 | return a + b; 10 | } 11 | 12 | test([1, '2']); 13 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/src/exception/wrong-simple-argument.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | /** 4 | * @param {Number} a 5 | * @param {Number} b 6 | * @returns {Number} 7 | */ 8 | function test(a, b) { 9 | return a + b; 10 | } 11 | 12 | test(1, '2'); 13 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/arguments-rest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} num 3 | * @param {Array} parameters 4 | * @returns {Number} 5 | * @typecheck 6 | */ 7 | function test(num, ...parameters) { 8 | return num + parameters[0]; 9 | } 10 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/variable-function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | let variable = function(a, b, c) { 9 | return a + b + c; 10 | }; 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/src/exception/wrong-optional-argument.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | /** 4 | * @param {Number} a 5 | * @param {Number} [b] 6 | * @returns {Number} 7 | */ 8 | function test(a, b) { 9 | return a + b; 10 | } 11 | 12 | test(1, '2'); 13 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/without-typecheck.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param {Number} a 5 | * @param {Number} b 6 | * @param {Number} c 7 | * @returns {Number} 8 | */ 9 | function test(a, b, c) { 10 | return a + b + c; 11 | } 12 | -------------------------------------------------------------------------------- /test/smoke/fixtures/directive-parsing/src/defaults-directive-in-function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function test(a, b, c) { 9 | return a + b + c; 10 | } 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/arguments-array-destructuring.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function test([a = 1, b, c]) { 9 | return a + b + c; 10 | } 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/class-method.js: -------------------------------------------------------------------------------- 1 | class Test { 2 | /** 3 | * @param {Number} a 4 | * @param {Number} b 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | myMethod(a, b) { 9 | return a + b; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/function-declaration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function functionDeclaration(a, b, c) { 9 | return a + b + c; 10 | } 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/jsdoc-multiline-object.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Object} data 3 | * @param {Number} data.a 4 | * @param {Number} data.b 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function test(data) { 9 | return data.a + data.b; 10 | } 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/src/exception/wrong-object-type-application-argument.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | /** 4 | * @param {Object} obj 5 | * @returns {Number} 6 | */ 7 | function test(obj) { 8 | return obj.a + obj.b; 9 | } 10 | 11 | test({a: 1, b: '2'}); 12 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/object-method.js: -------------------------------------------------------------------------------- 1 | let myObject = { 2 | /** 3 | * @param {Number} a 4 | * @param {Number} b 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | myMethod(a, b) { 9 | return a + b; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/arguments-object-destructuring.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function test({t: a, b = 1, c} = {}) { 9 | return a + b + c; 10 | } 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/src/exception/wrong-validate-in-scope.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | function Test() {} 4 | 5 | /** 6 | * @param {Test} data 7 | * @returns {null} 8 | */ 9 | function testFunction(data) { 10 | return null; 11 | } 12 | 13 | testFunction('wrong data'); 14 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/without-type-definition.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param a 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function firstFunction(a) { 9 | return __executeTypecheck__("firstFunction", "return", a, "\"Number\""); 10 | } 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/arguments-function-type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {function()} callback 3 | * @returns {*} 4 | * @typecheck 5 | */ 6 | function test(callback) { 7 | __executeTypecheck__("test", "callback", callback, "\"Function\""); 8 | 9 | return callback(); 10 | } 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/src/exception/wrong-arguments-union.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | /** 4 | * @param {{id: Number, name: String}|null} data 5 | * @returns {Number} 6 | */ 7 | function test(data) { 8 | if (data) { 9 | return data.id; 10 | } 11 | } 12 | 13 | test({id: '2'}); 14 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/src/exception/wrong-record-argument.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | /** 4 | * @param {{a: Number, b: Number}} record 5 | * @returns {Number} 6 | */ 7 | function test(record) { 8 | return record.a + record.b; 9 | } 10 | 11 | test({ 12 | a: 1, 13 | b: '2' 14 | }); 15 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/class-return-in-statement.js: -------------------------------------------------------------------------------- 1 | class Test { 2 | /** 3 | * @returns {Number} 4 | * @typecheck 5 | */ 6 | myMethod() { 7 | if (true) { 8 | return 1; 9 | } else { 10 | return 2; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/construtor-in-scope.js: -------------------------------------------------------------------------------- 1 | class Test {} 2 | 3 | /** 4 | * @param {Test} a 5 | * @returns {Test} 6 | * @typecheck 7 | */ 8 | function test(a) { 9 | __executeTypecheck__("test", "a", a, Test); 10 | 11 | return __executeTypecheck__("test", "return", a, Test); 12 | } 13 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/arguments-function-type.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param {function()} callback 5 | * @returns {*} 6 | * @typecheck 7 | */ 8 | function test(callback) { 9 | __executeTypecheck__("test", "callback", callback, "\"Function\""); 10 | 11 | return callback(); 12 | } 13 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/arrow-function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @returns {Number} 4 | * @typecheck 5 | */ 6 | let arrowFunction = (a) => { 7 | __executeTypecheck__("Anonymous function", "a", a, "\"Number\""); 8 | 9 | return __executeTypecheck__("Anonymous function", "return", a * 2, "\"Number\""); 10 | }; 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/variable-class.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | let variable = class { 4 | /** 5 | * @param {Number} a 6 | */ 7 | constructor(a) { 8 | this._a = a; 9 | } 10 | 11 | /** 12 | * @returns {Number} 13 | */ 14 | getA() { 15 | return this._a; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /test/smoke/fixtures/directive-parsing/src/defaults-collision-between-global-and-local.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @returns {Number} 4 | * @typecheck 5 | */ 6 | function firstFunction(a) { 7 | return a; 8 | } 9 | 10 | /** 11 | * @param {Number} a 12 | * @returns {Number} 13 | */ 14 | function secondFunction(a) { 15 | return a; 16 | } 17 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/arrow-function-expression.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @returns {Number} 4 | * @typecheck 5 | */ 6 | let arrowFunctionExpression = (a) => { 7 | __executeTypecheck__("Anonymous function", "a", a, "\"Number\""); 8 | 9 | return __executeTypecheck__("Anonymous function", "return", a * 2, "\"Number\""); 10 | }; 11 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/src/exception/wrong-object-type-application-arguments-multiline-jsdoc.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | /** 4 | * @param {Object} data 5 | * @param {Number} data.a 6 | * @param {Number} data.b 7 | * @returns {Number} 8 | */ 9 | function test(data) { 10 | return data.a + data.b; 11 | } 12 | 13 | test({ 14 | a: 1, 15 | c: '2' 16 | }); 17 | -------------------------------------------------------------------------------- /test/smoke/fixtures/runtime/src/no-exception/siblings-object-null-validation.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | /** 4 | * @param {{id: Number, name: String}|null} data 5 | * @returns {Number} 6 | */ 7 | function test(data) { 8 | if (data) { 9 | return data.id; 10 | } else { 11 | return 1; 12 | } 13 | } 14 | 15 | test({id: 2, name: 'test'}); 16 | test(null); 17 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/arrow-function.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param {Number} a 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | var arrowFunction = function arrowFunction(a) { 9 | __executeTypecheck__("Anonymous function", "a", a, "\"Number\""); 10 | 11 | return __executeTypecheck__("Anonymous function", "return", a * 2, "\"Number\""); 12 | }; 13 | -------------------------------------------------------------------------------- /test/smoke/fixtures/directive-parsing/src/defaults-global-directive.js: -------------------------------------------------------------------------------- 1 | //@typecheck 2 | 3 | /** 4 | * @param {Number} a 5 | * @param {Number} b 6 | * @param {Number} c 7 | * @returns {Number} 8 | */ 9 | function test(a, b, c) { 10 | return a + b + c; 11 | } 12 | 13 | /** 14 | * @param {String} a 15 | * @returns {Number} 16 | */ 17 | function parse(a) { 18 | return parseInt(a, 10); 19 | } 20 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/class-constructor.js: -------------------------------------------------------------------------------- 1 | class Test { 2 | /** 3 | * @param {Number} a 4 | * @param {Number} b 5 | * @typecheck 6 | */ 7 | constructor(a, b) { 8 | __executeTypecheck__("Test.constructor", "a", a, "\"Number\""); 9 | 10 | __executeTypecheck__("Test.constructor", "b", b, "\"Number\""); 11 | 12 | this._c = a + b; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/without-return.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @typecheck 6 | */ 7 | function test(a, b, c) { 8 | __executeTypecheck__("test", "a", a, "\"Number\""); 9 | 10 | __executeTypecheck__("test", "b", b, "\"Number\""); 11 | 12 | __executeTypecheck__("test", "c", c, "\"Number\""); 13 | 14 | let d = a + b + c; 15 | } 16 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/src/class-getter-setter.js: -------------------------------------------------------------------------------- 1 | class Test { 2 | constructor() { 3 | this._a = a; 4 | } 5 | 6 | /** 7 | * @returns {Number} 8 | * @typecheck 9 | */ 10 | get a() { 11 | return this._a; 12 | } 13 | 14 | /** 15 | * @param {Number} a 16 | * @typecheck 17 | */ 18 | set a(a) { 19 | this._a = a; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/arrow-function-expression.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param {Number} a 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | var arrowFunctionExpression = function arrowFunctionExpression(a) { 9 | __executeTypecheck__("Anonymous function", "a", a, "\"Number\""); 10 | 11 | return __executeTypecheck__("Anonymous function", "return", a * 2, "\"Number\""); 12 | }; 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | indent_style = space 12 | indent_size = 4 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | 19 | [*.json] 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/class-return-in-statement.js: -------------------------------------------------------------------------------- 1 | class Test { 2 | /** 3 | * @returns {Number} 4 | * @typecheck 5 | */ 6 | myMethod() { 7 | if (true) { 8 | return __executeTypecheck__("method Test.myMethod", "return", 1, "\"Number\""); 9 | } else { 10 | return __executeTypecheck__("method Test.myMethod", "return", 2, "\"Number\""); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/without-return.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param {Number} a 5 | * @param {Number} b 6 | * @param {Number} c 7 | * @typecheck 8 | */ 9 | function test(a, b, c) { 10 | __executeTypecheck__("test", "a", a, "\"Number\""); 11 | 12 | __executeTypecheck__("test", "b", b, "\"Number\""); 13 | 14 | __executeTypecheck__("test", "c", c, "\"Number\""); 15 | 16 | var d = a + b + c; 17 | } 18 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/jsdoc-multiline-object.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Object} data 3 | * @param {Number} data.a 4 | * @param {Number} data.b 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function test(data) { 9 | __executeTypecheck__("test", "data", data, "{\"record\":\"Object\",\"fields\":{\"a\":\"Number\",\"b\":\"Number\"}}"); 10 | 11 | return __executeTypecheck__("test", "return", data.a + data.b, "\"Number\""); 12 | } 13 | -------------------------------------------------------------------------------- /shared/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "useDirective": "typecheck", 4 | "useStrict": false, 5 | "_useHelper": true 6 | }, 7 | "doctrineConfig": { 8 | "unwrap": true, 9 | "sloppy": true, 10 | "recoverable": true, 11 | "tags": ["name", "param", "return", "returns", "typedef"] 12 | }, 13 | "functionName": "__executeTypecheck__", 14 | "defaultFunctionName": "Anonymous function", 15 | "defaultClassName": "Anonymous class" 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/normalize-function-body.js: -------------------------------------------------------------------------------- 1 | const babelTemplate = require('babel-template'); 2 | 3 | const bodyTemplate = babelTemplate('{ return STATEMENT }'); 4 | 5 | module.exports = (path) => { 6 | if (!path.node.expression) { 7 | return; 8 | } 9 | 10 | const body = path.get('body'); 11 | 12 | body.replaceWith( 13 | bodyTemplate({ 14 | STATEMENT: body.node 15 | }) 16 | ); 17 | 18 | path.node.expression = false; 19 | }; 20 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/object-method.js: -------------------------------------------------------------------------------- 1 | let myObject = { 2 | /** 3 | * @param {Number} a 4 | * @param {Number} b 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | myMethod(a, b) { 9 | __executeTypecheck__("myMethod", "a", a, "\"Number\""); 10 | 11 | __executeTypecheck__("myMethod", "b", b, "\"Number\""); 12 | 13 | return __executeTypecheck__("myMethod", "return", a + b, "\"Number\""); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/jsdoc-multiline-object.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param {Object} data 5 | * @param {Number} data.a 6 | * @param {Number} data.b 7 | * @returns {Number} 8 | * @typecheck 9 | */ 10 | function test(data) { 11 | __executeTypecheck__("test", "data", data, "{\"record\":\"Object\",\"fields\":{\"a\":\"Number\",\"b\":\"Number\"}}"); 12 | 13 | return __executeTypecheck__("test", "return", data.a + data.b, "\"Number\""); 14 | } 15 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/empty-return.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function test(a, b, c) { 9 | __executeTypecheck__("test", "a", a, "\"Number\""); 10 | 11 | __executeTypecheck__("test", "b", b, "\"Number\""); 12 | 13 | __executeTypecheck__("test", "c", c, "\"Number\""); 14 | 15 | return __executeTypecheck__("test", "return", void(0), "\"Number\""); 16 | } 17 | -------------------------------------------------------------------------------- /test/smoke/fixtures/directive-parsing/expected/defaults-collision-between-global-and-local.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @returns {Number} 4 | * @typecheck 5 | */ 6 | function firstFunction(a) { 7 | __executeTypecheck__("firstFunction", "a", a, "\"Number\""); 8 | 9 | return __executeTypecheck__("firstFunction", "return", a, "\"Number\""); 10 | } 11 | 12 | /** 13 | * @param {Number} a 14 | * @returns {Number} 15 | */ 16 | function secondFunction(a) { 17 | return a; 18 | } 19 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/arguments-rest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} num 3 | * @param {Array} parameters 4 | * @returns {Number} 5 | * @typecheck 6 | */ 7 | function test(num, ...parameters) { 8 | __executeTypecheck__("test", "num", num, "\"Number\""); 9 | 10 | __executeTypecheck__("test", "parameters", parameters, "{\"root\":\"Array\",\"children\":[\"Number\"]}"); 11 | 12 | return __executeTypecheck__("test", "return", num + parameters[0], "\"Number\""); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /test/smoke/fixtures/directive-parsing/expected/defaults-directive-in-function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function test(a, b, c) { 9 | __executeTypecheck__("test", "a", a, "\"Number\""); 10 | 11 | __executeTypecheck__("test", "b", b, "\"Number\""); 12 | 13 | __executeTypecheck__("test", "c", c, "\"Number\""); 14 | 15 | return __executeTypecheck__("test", "return", a + b + c, "\"Number\""); 16 | } 17 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/class-method.js: -------------------------------------------------------------------------------- 1 | class Test { 2 | /** 3 | * @param {Number} a 4 | * @param {Number} b 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | myMethod(a, b) { 9 | __executeTypecheck__("method Test.myMethod", "a", a, "\"Number\""); 10 | 11 | __executeTypecheck__("method Test.myMethod", "b", b, "\"Number\""); 12 | 13 | return __executeTypecheck__("method Test.myMethod", "return", a + b, "\"Number\""); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/arguments-array-destructuring.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function test([a = 1, b, c]) { 9 | __executeTypecheck__("test", "a", a, "\"Number\""); 10 | 11 | __executeTypecheck__("test", "b", b, "\"Number\""); 12 | 13 | __executeTypecheck__("test", "c", c, "\"Number\""); 14 | 15 | return __executeTypecheck__("test", "return", a + b + c, "\"Number\""); 16 | } 17 | 18 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/variable-class.js: -------------------------------------------------------------------------------- 1 | // @typecheck 2 | 3 | let variable = class { 4 | /** 5 | * @param {Number} a 6 | */ 7 | constructor(a) { 8 | __executeTypecheck__("Anonymous class.constructor", "a", a, "\"Number\""); 9 | 10 | this._a = a; 11 | } 12 | 13 | /** 14 | * @returns {Number} 15 | */ 16 | getA() { 17 | return __executeTypecheck__("method Anonymous class.getA", "return", this._a, "\"Number\""); 18 | } 19 | }; 20 | 21 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/empty-return.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param {Number} a 5 | * @param {Number} b 6 | * @param {Number} c 7 | * @returns {Number} 8 | * @typecheck 9 | */ 10 | function test(a, b, c) { 11 | __executeTypecheck__("test", "a", a, "\"Number\""); 12 | 13 | __executeTypecheck__("test", "b", b, "\"Number\""); 14 | 15 | __executeTypecheck__("test", "c", c, "\"Number\""); 16 | 17 | return __executeTypecheck__("test", "return", void(0), "\"Number\""); 18 | } 19 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/object-method.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var myObject = { 4 | /** 5 | * @param {Number} a 6 | * @param {Number} b 7 | * @returns {Number} 8 | * @typecheck 9 | */ 10 | myMethod: function myMethod(a, b) { 11 | __executeTypecheck__("myMethod", "a", a, "\"Number\""); 12 | 13 | __executeTypecheck__("myMethod", "b", b, "\"Number\""); 14 | 15 | return __executeTypecheck__("myMethod", "return", a + b, "\"Number\""); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/arguments-object-destructuring.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function test({ t: a, b = 1, c } = {}) { 9 | __executeTypecheck__("test", "a", a, "\"Number\""); 10 | 11 | __executeTypecheck__("test", "b", b, "\"Number\""); 12 | 13 | __executeTypecheck__("test", "c", c, "\"Number\""); 14 | 15 | return __executeTypecheck__("test", "return", a + b + c, "\"Number\""); 16 | } 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/class-getter-setter.js: -------------------------------------------------------------------------------- 1 | class Test { 2 | constructor() { 3 | this._a = a; 4 | } 5 | 6 | /** 7 | * @returns {Number} 8 | * @typecheck 9 | */ 10 | get a() { 11 | return __executeTypecheck__("get Test.a", "return", this._a, "\"Number\""); 12 | } 13 | 14 | /** 15 | * @param {Number} a 16 | * @typecheck 17 | */ 18 | set a(a) { 19 | __executeTypecheck__("set Test.a", "a", a, "\"Number\""); 20 | 21 | this._a = a; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "rules": { 4 | "indent": ["error", 4, { "SwitchCase": 1 }], 5 | "linebreak-style": ["error", "unix"], 6 | "quotes": ["error", "single"], 7 | "semi": ["error", "always"], 8 | "comma-dangle": ["error", "never"] 9 | }, 10 | "env": { 11 | "browser": false, 12 | "amd": false, 13 | "node": true, 14 | "es6": true 15 | }, 16 | "parserOptions": { 17 | "ecmaVersion": 6, 18 | "sourceType": "module" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/variable-function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | let variable = function (a, b, c) { 9 | __executeTypecheck__("Anonymous function", "a", a, "\"Number\""); 10 | 11 | __executeTypecheck__("Anonymous function", "b", b, "\"Number\""); 12 | 13 | __executeTypecheck__("Anonymous function", "c", c, "\"Number\""); 14 | 15 | return __executeTypecheck__("Anonymous function", "return", a + b + c, "\"Number\""); 16 | }; 17 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/expected/function-declaration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a 3 | * @param {Number} b 4 | * @param {Number} c 5 | * @returns {Number} 6 | * @typecheck 7 | */ 8 | function functionDeclaration(a, b, c) { 9 | __executeTypecheck__("functionDeclaration", "a", a, "\"Number\""); 10 | 11 | __executeTypecheck__("functionDeclaration", "b", b, "\"Number\""); 12 | 13 | __executeTypecheck__("functionDeclaration", "c", c, "\"Number\""); 14 | 15 | return __executeTypecheck__("functionDeclaration", "return", a + b + c, "\"Number\""); 16 | } 17 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/construtor-in-scope.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 4 | 5 | var Test = function Test() { 6 | _classCallCheck(this, Test); 7 | }; 8 | 9 | /** 10 | * @param {Test} a 11 | * @returns {Test} 12 | * @typecheck 13 | */ 14 | 15 | 16 | function test(a) { 17 | __executeTypecheck__("test", "a", a, Test); 18 | 19 | return __executeTypecheck__("test", "return", a, Test); 20 | } 21 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/variable-function.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param {Number} a 5 | * @param {Number} b 6 | * @param {Number} c 7 | * @returns {Number} 8 | * @typecheck 9 | */ 10 | var variable = function variable(a, b, c) { 11 | __executeTypecheck__("Anonymous function", "a", a, "\"Number\""); 12 | 13 | __executeTypecheck__("Anonymous function", "b", b, "\"Number\""); 14 | 15 | __executeTypecheck__("Anonymous function", "c", c, "\"Number\""); 16 | 17 | return __executeTypecheck__("Anonymous function", "return", a + b + c, "\"Number\""); 18 | }; 19 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/function-declaration.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param {Number} a 5 | * @param {Number} b 6 | * @param {Number} c 7 | * @returns {Number} 8 | * @typecheck 9 | */ 10 | function functionDeclaration(a, b, c) { 11 | __executeTypecheck__("functionDeclaration", "a", a, "\"Number\""); 12 | 13 | __executeTypecheck__("functionDeclaration", "b", b, "\"Number\""); 14 | 15 | __executeTypecheck__("functionDeclaration", "c", c, "\"Number\""); 16 | 17 | return __executeTypecheck__("functionDeclaration", "return", a + b + c, "\"Number\""); 18 | } 19 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/class-constructor.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 4 | 5 | var Test = 6 | /** 7 | * @param {Number} a 8 | * @param {Number} b 9 | * @typecheck 10 | */ 11 | function Test(a, b) { 12 | _classCallCheck(this, Test); 13 | 14 | __executeTypecheck__("Test.constructor", "a", a, "\"Number\""); 15 | 16 | __executeTypecheck__("Test.constructor", "b", b, "\"Number\""); 17 | 18 | this._c = a + b; 19 | }; 20 | 21 | -------------------------------------------------------------------------------- /src/lib/normalize-validator.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_TYPES = ['Object', 'Number', 'String', 'Boolean', 'Array', 'Function']; 2 | 3 | /** 4 | * @param {NodePath} path 5 | * @param {String|Object|Array} type 6 | * @param {Object} t 7 | * @returns {Node} 8 | */ 9 | module.exports = (path, type, t) => { 10 | let validator; 11 | 12 | if ( 13 | typeof type === 'string' && 14 | !DEFAULT_TYPES.includes(type) && 15 | path.scope.hasBinding(type) 16 | ) { 17 | validator = t.identifier(type); 18 | } else { 19 | validator = t.stringLiteral(JSON.stringify(type)); 20 | } 21 | 22 | return validator; 23 | }; 24 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/arguments-rest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param {Number} num 5 | * @param {Array} parameters 6 | * @returns {Number} 7 | * @typecheck 8 | */ 9 | function test(num) { 10 | __executeTypecheck__("test", "num", num, "\"Number\""); 11 | 12 | for (var _len = arguments.length, parameters = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 13 | parameters[_key - 1] = arguments[_key]; 14 | } 15 | 16 | __executeTypecheck__("test", "parameters", parameters, "{\"root\":\"Array\",\"children\":[\"Number\"]}"); 17 | 18 | return __executeTypecheck__("test", "return", num + parameters[0], "\"Number\""); 19 | } 20 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/arguments-object-destructuring.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param {Number} a 5 | * @param {Number} b 6 | * @param {Number} c 7 | * @returns {Number} 8 | * @typecheck 9 | */ 10 | function test() { 11 | var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, 12 | a = _ref.t, 13 | _ref$b = _ref.b, 14 | b = _ref$b === undefined ? 1 : _ref$b, 15 | c = _ref.c; 16 | 17 | __executeTypecheck__("test", "a", a, "\"Number\""); 18 | 19 | __executeTypecheck__("test", "b", b, "\"Number\""); 20 | 21 | __executeTypecheck__("test", "c", c, "\"Number\""); 22 | 23 | return __executeTypecheck__("test", "return", a + b + c, "\"Number\""); 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/smoke/fixtures/directive-parsing/expected/defaults-global-directive.js: -------------------------------------------------------------------------------- 1 | //@typecheck 2 | 3 | /** 4 | * @param {Number} a 5 | * @param {Number} b 6 | * @param {Number} c 7 | * @returns {Number} 8 | */ 9 | function test(a, b, c) { 10 | __executeTypecheck__("test", "a", a, "\"Number\""); 11 | 12 | __executeTypecheck__("test", "b", b, "\"Number\""); 13 | 14 | __executeTypecheck__("test", "c", c, "\"Number\""); 15 | 16 | return __executeTypecheck__("test", "return", a + b + c, "\"Number\""); 17 | } 18 | 19 | /** 20 | * @param {String} a 21 | * @returns {Number} 22 | */ 23 | function parse(a) { 24 | __executeTypecheck__("parse", "a", a, "\"String\""); 25 | 26 | return __executeTypecheck__("parse", "return", parseInt(a, 10), "\"Number\""); 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/lib/typecheck-function-template/function-call.js: -------------------------------------------------------------------------------- 1 | const babelTemplate = require('babel-template'); 2 | 3 | /** 4 | * @param {String} name 5 | * @param {Object} t 6 | */ 7 | module.exports = (name, t) => { 8 | const template = babelTemplate(`${name}(FUNCTION_NAME, NAME, ARGUMENT, VALIDATOR);`); 9 | 10 | /** 11 | * @param {String} functionName 12 | * @param {String} name 13 | * @param {Node} argument 14 | * @param {Node} validator 15 | */ 16 | return (functionName, name, argument, validator) => { 17 | return template({ 18 | FUNCTION_NAME: t.stringLiteral(functionName), 19 | NAME: t.stringLiteral(name), 20 | ARGUMENT: argument, 21 | VALIDATOR: validator 22 | }); 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const FILE_ENCODING = 'utf8'; 4 | 5 | /** 6 | * @param {String} src 7 | * @returns {Promise>} 8 | */ 9 | module.exports.readDirectory = (src) => { 10 | return new Promise((resolve, reject) => { 11 | fs.readdir(src, {}, (err, files) => { 12 | if (err) { 13 | return reject(`Failed to read data from ${src} -> ${err}`); 14 | } 15 | 16 | resolve(files); 17 | }); 18 | }); 19 | }; 20 | 21 | module.exports.readFile = (src) => { 22 | return new Promise((resolve, reject) => { 23 | fs.readFile(src, {encoding: FILE_ENCODING}, (err, file) => { 24 | if (err) { 25 | return reject(`Failed to read data from ${src} -> ${err}`); 26 | } 27 | 28 | resolve(file); 29 | }); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const config = require('./config.json'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const Mocha = require('mocha'); 5 | 6 | const JS_REGEXP = /\.js$/; 7 | 8 | const mocha = new Mocha(); 9 | 10 | addFilesFrom(mocha, path.resolve(config.path.smokeTest)); 11 | addFilesFrom(mocha, path.resolve(config.path.unitTest)); 12 | 13 | mocha.run((failures) => { 14 | process.on('exit', () => { 15 | process.exit(failures); // exit with non-zero status if there were failures 16 | }); 17 | }); 18 | 19 | /** 20 | * @param {Mocha} mocha 21 | * @param {String} directory 22 | */ 23 | function addFilesFrom(mocha, directory) { 24 | fs.readdirSync(directory) 25 | .filter((filename) => JS_REGEXP.test(filename)) 26 | .forEach((filename) => { 27 | mocha.addFile( 28 | path.join(directory, filename) 29 | ); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Sergey Zhuravlev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/unit/parse-jsdoc.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const path = require('path'); 3 | const utils = require('../utils'); 4 | const config = require('../config.json'); 5 | 6 | const {parseFunctionDeclaration} = require('../../src/lib/parse-jsdoc'); 7 | 8 | const DATA_DIRECTORY = path.join(config.path.unitTestData, 'parse-jsdoc'); 9 | const SOURCE_DIRECTORY = path.join(DATA_DIRECTORY, 'src'); 10 | const EXPECTED_DIRECTORY = path.resolve(DATA_DIRECTORY, 'expected'); 11 | 12 | function createTest(filename) { 13 | it(`in '${filename}'`, () => { 14 | utils.readFile(path.join(SOURCE_DIRECTORY, filename)).then((fileSource) => { 15 | const expectedJSON = require(path.join(EXPECTED_DIRECTORY, filename)); 16 | const parsingResult = parseFunctionDeclaration(fileSource); 17 | 18 | chai.expect(parsingResult).to.deep.equal(expectedJSON); 19 | }); 20 | }); 21 | } 22 | 23 | utils.readDirectory(SOURCE_DIRECTORY).then((sources) => { 24 | 25 | describe('[UNIT] Parse jsDoc', () => { 26 | 27 | describe('correctly parse docs', () => { 28 | sources.forEach(createTest); 29 | }); 30 | 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /test/smoke/helpers/compare.js: -------------------------------------------------------------------------------- 1 | const config = require('../../config.json'); 2 | 3 | const chai = require('chai'); 4 | const babel = require('babel-core'); 5 | 6 | const DEFAULT_BABEL_PLUGIN_PARAMETER = { 7 | _insertHelper: false 8 | }; 9 | 10 | const ERROR = { 11 | EMPTY_SOURCE: 'Source file is empty', 12 | EMPTY_EXPECTED: 'Expected file is empty' 13 | }; 14 | 15 | function trimString(string) { 16 | return string.replace(/(\n| )/gim, '').trim(); 17 | } 18 | 19 | /** 20 | * @param {String} fileSource 21 | * @param {String} fileExpected 22 | * @param {Object} [parameters] 23 | */ 24 | module.exports = (fileSource, fileExpected, parameters = {}) => { 25 | parameters.presets = Array.isArray(parameters.presets) ? parameters.presets : []; 26 | 27 | chai.assert.notEqual(trimString(fileSource).length, 0, ERROR.EMPTY_SOURCE); 28 | chai.assert.notEqual(trimString(fileExpected).length, 0, ERROR.EMPTY_EXPECTED); 29 | 30 | let transformationResult = babel.transform(fileSource, { 31 | presets: parameters.presets, 32 | plugins: [ 33 | [config.path.plugin, Object.assign(DEFAULT_BABEL_PLUGIN_PARAMETER, parameters)] 34 | ] 35 | }); 36 | 37 | chai.expect(transformationResult.code.trim()).not.differentFrom(fileExpected.trim()); 38 | }; 39 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/arguments-array-destructuring.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 4 | 5 | /** 6 | * @param {Number} a 7 | * @param {Number} b 8 | * @param {Number} c 9 | * @returns {Number} 10 | * @typecheck 11 | */ 12 | function test(_ref) { 13 | var _ref2 = _slicedToArray(_ref, 3), 14 | _ref2$ = _ref2[0], 15 | a = _ref2$ === undefined ? 1 : _ref2$, 16 | b = _ref2[1], 17 | c = _ref2[2]; 18 | 19 | __executeTypecheck__("test", "a", a, "\"Number\""); 20 | 21 | __executeTypecheck__("test", "b", b, "\"Number\""); 22 | 23 | __executeTypecheck__("test", "c", c, "\"Number\""); 24 | 25 | return __executeTypecheck__("test", "return", a + b + c, "\"Number\""); 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/class-return-in-statement.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | var Test = function () { 8 | function Test() { 9 | _classCallCheck(this, Test); 10 | } 11 | 12 | _createClass(Test, [{ 13 | key: "myMethod", 14 | 15 | /** 16 | * @returns {Number} 17 | * @typecheck 18 | */ 19 | value: function myMethod() { 20 | if (true) { 21 | return __executeTypecheck__("method Test.myMethod", "return", 1, "\"Number\""); 22 | } else { 23 | return __executeTypecheck__("method Test.myMethod", "return", 2, "\"Number\""); 24 | } 25 | } 26 | }]); 27 | 28 | return Test; 29 | }(); 30 | 31 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/variable-class.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | // @typecheck 8 | 9 | var variable = function () { 10 | /** 11 | * @param {Number} a 12 | */ 13 | function variable(a) { 14 | _classCallCheck(this, variable); 15 | 16 | __executeTypecheck__("Anonymous class.constructor", "a", a, "\"Number\""); 17 | 18 | this._a = a; 19 | } 20 | 21 | /** 22 | * @returns {Number} 23 | */ 24 | 25 | 26 | _createClass(variable, [{ 27 | key: "getA", 28 | value: function getA() { 29 | return __executeTypecheck__("method Anonymous class.getA", "return", this._a, "\"Number\""); 30 | } 31 | }]); 32 | 33 | return variable; 34 | }(); 35 | 36 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/class-method.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | var Test = function () { 8 | function Test() { 9 | _classCallCheck(this, Test); 10 | } 11 | 12 | _createClass(Test, [{ 13 | key: "myMethod", 14 | 15 | /** 16 | * @param {Number} a 17 | * @param {Number} b 18 | * @returns {Number} 19 | * @typecheck 20 | */ 21 | value: function myMethod(a, b) { 22 | __executeTypecheck__("method Test.myMethod", "a", a, "\"Number\""); 23 | 24 | __executeTypecheck__("method Test.myMethod", "b", b, "\"Number\""); 25 | 26 | return __executeTypecheck__("method Test.myMethod", "return", a + b, "\"Number\""); 27 | } 28 | }]); 29 | 30 | return Test; 31 | }(); 32 | 33 | -------------------------------------------------------------------------------- /test/smoke/fixtures/assertion-inject/integration/class-getter-setter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | var Test = function () { 8 | function Test() { 9 | _classCallCheck(this, Test); 10 | 11 | this._a = a; 12 | } 13 | 14 | /** 15 | * @returns {Number} 16 | * @typecheck 17 | */ 18 | 19 | 20 | _createClass(Test, [{ 21 | key: "a", 22 | get: function get() { 23 | return __executeTypecheck__("get Test.a", "return", this._a, "\"Number\""); 24 | } 25 | 26 | /** 27 | * @param {Number} a 28 | * @typecheck 29 | */ 30 | , 31 | set: function set(a) { 32 | __executeTypecheck__("set Test.a", "a", a, "\"Number\""); 33 | 34 | this._a = a; 35 | } 36 | }]); 37 | 38 | return Test; 39 | }(); 40 | -------------------------------------------------------------------------------- /src/lib/find-global-directive.js: -------------------------------------------------------------------------------- 1 | const {defaults} = require('../../shared/config.json'); 2 | 3 | function findFirstNode(node, index) { 4 | return index === 0; 5 | } 6 | 7 | function hasGlobalDirective(comment, directive) { 8 | return ( 9 | comment.value.includes(`@${directive}`) && 10 | !comment.value.includes('@param') && 11 | !comment.value.includes('@return') 12 | ); 13 | } 14 | 15 | /** 16 | * @param {NodePath} path 17 | * @returns {Array|null} 18 | */ 19 | function getHeadingComments(path) { 20 | const firstNode = path.node.body.find(findFirstNode); 21 | 22 | if (!firstNode) { 23 | return null; 24 | } 25 | 26 | return firstNode.leadingComments; 27 | } 28 | 29 | /** 30 | * @param {NodePath} path 31 | * @param {PluginPass} state 32 | * @returns {Boolean} 33 | */ 34 | module.exports = (path, state) => { 35 | let useDirective = state.opts.useDirective; 36 | 37 | if (useDirective === false) { 38 | return false; 39 | } 40 | 41 | if (!useDirective) { 42 | useDirective = defaults.useDirective; 43 | } 44 | 45 | const topComments = getHeadingComments(path); 46 | 47 | if (!topComments) { 48 | return false; 49 | } 50 | 51 | let globalDirective = false; 52 | 53 | for (let index = 0, count = topComments.length; index < count; index++) { 54 | if (hasGlobalDirective(topComments[index], useDirective)) { 55 | globalDirective = true; 56 | break; 57 | } 58 | } 59 | 60 | return globalDirective; 61 | }; 62 | -------------------------------------------------------------------------------- /src/lib/strict-mode.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | const SEPARATOR = chalk.gray('➞'); 4 | const PREFIX = chalk.bold.red.inverse('[TYPECHECK STRICT MODE]') + ' ' + SEPARATOR; 5 | 6 | function createMessage(message) { 7 | return `${PREFIX} ${chalk.underline(message)}`; 8 | } 9 | 10 | module.exports.ERROR = { 11 | NO_RETURN_IN_JSDOC: createMessage('Return statement type annotation missing.'), 12 | NO_ARGUMENT_IN_JSDOC: createMessage('Function argument type annotation missing.'), 13 | UNUSED_ARGUMENTS_IN_JSDOC: createMessage('Arguments described in type annotation doesn\'t appear in function signature.'), 14 | EMPTY_RETURN_IN_FUNCTION: createMessage('Return statement must return something.') 15 | }; 16 | 17 | /** 18 | * @param {NodePath} path 19 | * @param {String} error 20 | */ 21 | module.exports.throwException = (path, error) => { 22 | let errorAnchorPath = path; 23 | 24 | if (path.node) { 25 | if (path.isReturnStatement() && path.node.argument) { 26 | errorAnchorPath = path.get('argument'); 27 | } 28 | } 29 | 30 | throw errorAnchorPath.buildCodeFrameError(error); 31 | }; 32 | 33 | /** 34 | * @param {NodePath} path 35 | * @param {String} component 36 | * @param {String} error 37 | */ 38 | module.exports.throwSpecificException = (path, component, error) => { 39 | const componentName = chalk.bold.yellow.inverse(`[${component}]`); 40 | const normalizedError = `${PREFIX} ${componentName} ${SEPARATOR} ${chalk.underline(error)}`; 41 | 42 | module.exports.throwException(path, normalizedError); 43 | }; 44 | -------------------------------------------------------------------------------- /test/smoke/assertion-inject.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const utils = require('../utils'); 3 | const config = require('../config.json'); 4 | const compare = require('./helpers/compare'); 5 | 6 | const DATA_DIRECTORY = path.join(config.path.smokeTestData, 'assertion-inject'); 7 | const SOURCE_DIRECTORY = path.join(DATA_DIRECTORY, 'src'); 8 | const EXPECTED_DIRECTORY = path.join(DATA_DIRECTORY, 'expected'); 9 | const INTEGRATION_DIRECTORY = path.join(DATA_DIRECTORY, 'integration'); 10 | 11 | const BABEL_ES2015_CONFIG = { 12 | presets: ['es2015'] 13 | }; 14 | 15 | function createAssertionTest(filename) { 16 | it(`in '${filename}'`, () => { 17 | Promise.all([ 18 | utils.readFile(path.join(SOURCE_DIRECTORY, filename)), 19 | utils.readFile(path.join(EXPECTED_DIRECTORY, filename)) 20 | ]).then((files) => { 21 | compare(...files); 22 | }); 23 | }); 24 | } 25 | 26 | function createES2015IntegrationTest(filename) { 27 | it(`in '${filename}'`, () => { 28 | Promise.all([ 29 | utils.readFile(path.join(SOURCE_DIRECTORY, filename)), 30 | utils.readFile(path.join(INTEGRATION_DIRECTORY, filename)) 31 | ]).then((files) => { 32 | compare(...files, BABEL_ES2015_CONFIG); 33 | }); 34 | }); 35 | } 36 | 37 | utils.readDirectory(SOURCE_DIRECTORY).then((sources) => { 38 | describe('[SMOKE] Assertion inject', () => { 39 | describe('correctly adds assertions', () => { 40 | sources.forEach(createAssertionTest); 41 | }); 42 | 43 | describe('integration with es2015 preset', () => { 44 | sources.forEach(createES2015IntegrationTest); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/smoke/directive-parsing.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const config = require('../config.json'); 4 | const utils = require('../utils'); 5 | const compare = require('./helpers/compare'); 6 | 7 | const DATA_DIRECTORY = path.join(config.path.smokeTestData, 'directive-parsing'); 8 | const SOURCE_DIRECTORY = path.join(DATA_DIRECTORY, 'src'); 9 | const EXPECTED_DIRECTORY = path.join(DATA_DIRECTORY, 'expected'); 10 | 11 | const read = (file) => { 12 | return Promise.all([ 13 | utils.readFile(path.join(SOURCE_DIRECTORY, file)), 14 | utils.readFile(path.join(EXPECTED_DIRECTORY, file)) 15 | ]); 16 | }; 17 | 18 | const executeTest = (file, done) => { 19 | read(file) 20 | .then((files) => compare(...files)) 21 | .then(() => done()) 22 | .catch((exception) => done(exception)); 23 | }; 24 | 25 | describe('[SMOKE] Directive parsing', () => { 26 | describe('with default parameters', () => { 27 | it('doesn\'t transform code without directive', (done) => { 28 | executeTest('defaults-no-directive.js', done); 29 | }); 30 | 31 | it('doesn\'t transform code with global directive and without jsDoc', (done) => { 32 | executeTest('defaults-global-directive-before-function.js', done); 33 | }); 34 | 35 | it('transform code when directive in function', (done) => { 36 | executeTest('defaults-directive-in-function.js', done); 37 | }); 38 | 39 | it('transform code when directive is global', (done) => { 40 | executeTest('defaults-global-directive.js', done); 41 | }); 42 | 43 | it('transform code without collision in directives', (done) => { 44 | executeTest('defaults-collision-between-global-and-local.js', done); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-jsdoc-runtime-typecheck", 3 | "version": "1.2.1", 4 | "description": "Babel plugin, that adds typecheck, based on jsDoc.", 5 | "license": "MIT", 6 | "homepage": "https://github.com/johnthecat/babel-plugin-jsdoc-runtime-typecheck.git", 7 | "author": "Sergey Zhuravlev (https://github.com/johnthecat)", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/johnthecat/babel-plugin-jsdoc-runtime-typecheck.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/johnthecat/babel-plugin-jsdoc-runtime-typecheck/issues" 14 | }, 15 | "keywords": [ 16 | "build", 17 | "babel", 18 | "plugin", 19 | "jsdoc", 20 | "typecheck", 21 | "flow" 22 | ], 23 | "main": "dist/index.js", 24 | "scripts": { 25 | "eslint:src": "eslint -c .eslintrc src/", 26 | "eslint:test": "eslint --ignore-path test/.eslintignore -c test/.eslintrc test/", 27 | "build": "npm run eslint:src && babel src --out-dir dist", 28 | "build:watch": "babel src --watch --out-dir dist", 29 | "build:ci": "npm run test && npm run build", 30 | "test": "npm run eslint:test && node test", 31 | "test:watch": "npm run eslint:test && node test --watch" 32 | }, 33 | "pre-commit": [ 34 | "build:ci" 35 | ], 36 | "devDependencies": { 37 | "babel-cli": "^6.24.0", 38 | "babel-core": "^6.24.0", 39 | "babel-preset-env": "^1.3.2", 40 | "babel-preset-es2015": "^6.24.0", 41 | "chai": "^3.5.0", 42 | "chai-diff": "^1.0.1", 43 | "eslint": "^3.19.0", 44 | "mocha": "^3.2.0", 45 | "pre-commit": "^1.2.2", 46 | "vm2": "^3.4.6", 47 | "yargs": "^7.0.2" 48 | }, 49 | "dependencies": { 50 | "babel-template": "6.23.0", 51 | "chalk": "1.1.3", 52 | "core-js": "2.4.1", 53 | "doctrine": "2.0.0" 54 | }, 55 | "engineStrict": true, 56 | "engines": { 57 | "node": ">= 4.7.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/check-function.js: -------------------------------------------------------------------------------- 1 | const config = require('../../shared/config.json'); 2 | 3 | const ALLOWED_NODE_TYPE = 'CallExpression'; 4 | const FUNCTION_NAME = config.functionName; 5 | 6 | /** 7 | * @param {NodePath} path 8 | * @return {Boolean} 9 | */ 10 | function traverseToReturnStatement(path) { 11 | let found = false; 12 | 13 | path.traverse({ 14 | ReturnStatement(returnPath) { 15 | if ( 16 | returnPath.node.argument && 17 | returnPath.node.argument.type === ALLOWED_NODE_TYPE && 18 | returnPath.node.argument.callee.name === FUNCTION_NAME 19 | ) { 20 | found = true; 21 | path.stop(); 22 | } 23 | } 24 | }); 25 | 26 | return found; 27 | } 28 | 29 | /** 30 | * @param {NodePath} path 31 | * @returns {Boolean} 32 | */ 33 | function findInsertedExpression(path) { 34 | if (path.isExpressionStatement()) { 35 | return ( 36 | path.node.expression.type === ALLOWED_NODE_TYPE && 37 | path.node.expression.callee.name === FUNCTION_NAME 38 | ); 39 | } 40 | 41 | if (path.isReturnStatement()) { 42 | return ( 43 | path.node.argument && 44 | path.node.argument.type === ALLOWED_NODE_TYPE && 45 | path.node.argument.callee.name === FUNCTION_NAME 46 | ); 47 | } 48 | 49 | return traverseToReturnStatement(path); 50 | } 51 | 52 | /** 53 | * @param {NodePath} path 54 | * @returns {Boolean} 55 | */ 56 | module.exports = (path) => { 57 | /** 58 | * @type {Array} 59 | */ 60 | const innerPaths = path.get('body.body'); 61 | 62 | let result = true; 63 | 64 | for (let index = 0, count = innerPaths.length; index < count; index++) { 65 | if (findInsertedExpression(innerPaths[index])) { 66 | result = false; 67 | break; 68 | } 69 | } 70 | 71 | return result; 72 | }; 73 | -------------------------------------------------------------------------------- /src/visitors/return-statement.js: -------------------------------------------------------------------------------- 1 | const strictMode = require('../lib/strict-mode'); 2 | const normalizeValidator = require('../lib/normalize-validator'); 3 | 4 | const VALIDATOR_METHOD_NAME = 'return'; 5 | 6 | /** 7 | * @param {Function} typecheckFunctionCall 8 | * @param {Object} globalState 9 | * @param {Object} t 10 | * @returns {{ReturnStatement: Function}} 11 | */ 12 | module.exports = (typecheckFunctionCall, globalState, t) => { 13 | return { 14 | ReturnStatement(path) { 15 | /** 16 | * Check for valid function parent, prevents changing return statements in nested functions 17 | */ 18 | if (path.getFunctionParent().node !== this.functionPath.node) { 19 | path.stop(); 20 | return; 21 | } 22 | 23 | let statement = this.jsDoc.returnStatement; 24 | let argument = path.get('argument'); 25 | 26 | if (globalState.useStrict) { 27 | /** 28 | * Case: function doesn't return anything, but can stop self execution with return statement. 29 | */ 30 | if (statement === void(0) && argument.node) { 31 | strictMode.throwException(path, strictMode.ERROR.NO_RETURN_IN_JSDOC); 32 | } 33 | 34 | /** 35 | * Case: return in described in jsDoc, but real return statement in function is empty. 36 | */ 37 | if (statement !== void(0) && !argument.node) { 38 | strictMode.throwException(path, strictMode.ERROR.EMPTY_RETURN_IN_FUNCTION); 39 | } 40 | } 41 | 42 | if (statement) { 43 | let functionCall = typecheckFunctionCall( 44 | this.functionName, 45 | VALIDATOR_METHOD_NAME, 46 | argument.node || t.identifier('void(0)'), 47 | normalizeValidator(path, statement, t) 48 | ); 49 | 50 | argument.replaceWith(functionCall.expression); 51 | } 52 | } 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/visitors/helpers.js: -------------------------------------------------------------------------------- 1 | const config = require('../../shared/config.json'); 2 | 3 | const FUNCTION_NODE_DESCRIPTOR = 'init'; 4 | const CLASS_NODE_DESCRIPTOR = 'init'; 5 | const CONSTRUCTOR_NODE_TYPE = 'constructor'; 6 | 7 | /** 8 | * @param {Array} declarations 9 | * @returns {NodePath|null} 10 | */ 11 | module.exports.declarationIsFunction = (declarations) => { 12 | let result = null; 13 | let declaration; 14 | 15 | for (let index = 0, count = declarations.length; index < count; index++) { 16 | declaration = declarations[index].get(FUNCTION_NODE_DESCRIPTOR); 17 | 18 | if (declaration.isFunction()) { 19 | result = declaration; 20 | break; 21 | } 22 | } 23 | 24 | return result; 25 | }; 26 | 27 | /** 28 | * @param {Array} declarations 29 | * @returns {NodePath|null} 30 | */ 31 | module.exports.declarationIsClassExpression = (declarations) => { 32 | let result = null; 33 | let declaration; 34 | 35 | for (let index = 0, count = declarations.length; index < count; index++) { 36 | declaration = declarations[index].get(CLASS_NODE_DESCRIPTOR); 37 | 38 | if (declaration.isClassExpression()) { 39 | result = declaration; 40 | break; 41 | } 42 | } 43 | 44 | return result; 45 | }; 46 | 47 | /** 48 | * @param {NodePath} path 49 | * @returns {Boolean} 50 | */ 51 | module.exports.pathIsConstructor = (path) => { 52 | return path.node.kind === CONSTRUCTOR_NODE_TYPE; 53 | }; 54 | 55 | /** 56 | * @param {NodePath} path 57 | * @returns {Boolean} 58 | */ 59 | module.exports.pathIsExpression = (path) => { 60 | return path.isExpressionStatement(); 61 | }; 62 | 63 | /** 64 | * @param {NodePath} path 65 | * @returns {Boolean} 66 | */ 67 | module.exports.pathIsClassDeclaration = (path) => { 68 | return path.isClassDeclaration(); 69 | }; 70 | 71 | /** 72 | * @param {NodePath} classMethod 73 | * @param {NodePath} classExpression 74 | * @return {String} 75 | */ 76 | module.exports.createClassMethodName = (classMethod, classExpression) => { 77 | const node = classMethod.node; 78 | const className = classExpression.node.id ? classExpression.node.id.name : config.defaultClassName; 79 | const kind = node.kind === CONSTRUCTOR_NODE_TYPE ? '' : node.kind + ' '; 80 | 81 | return `${node.static ? 'static ' : ''}${kind}${className}.${node.key.name}`; 82 | }; 83 | -------------------------------------------------------------------------------- /test/smoke/strict-mode.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const babel = require('babel-core'); 3 | const chai = require('chai'); 4 | 5 | const config = require('../config.json'); 6 | const utils = require('../utils'); 7 | 8 | const DATA_DIRECTORY = path.join(config.path.smokeTestData, 'strict-mode'); 9 | const SOURCE_DIRECTORY_ERRORS = path.join(DATA_DIRECTORY, 'exception'); 10 | const SOURCE_DIRECTORY_NO_ERRORS = path.join(DATA_DIRECTORY, 'no-exception'); 11 | 12 | const EXCEPTION_NOT_THROWN = 'Cannot handle this case - exception not thrown!'; 13 | const EXCEPTION_THROWN = 'Cannot handle this case - exception thrown!'; 14 | const BABEL_CONFIG = { 15 | code: false, 16 | plugins: [ 17 | [ 18 | config.path.plugin, 19 | { 20 | useStrict: true 21 | } 22 | ] 23 | ] 24 | }; 25 | 26 | function transform (source) { 27 | let transformed = false; 28 | 29 | try { 30 | babel.transform(source, BABEL_CONFIG); 31 | transformed = true; 32 | } catch (e) { 33 | // nothing to do here 34 | } 35 | 36 | return transformed; 37 | } 38 | 39 | function createPositiveTest(filename) { 40 | it(`in '${filename}'`, (done) => { 41 | utils.readFile(path.join(SOURCE_DIRECTORY_NO_ERRORS, filename)).then((fileSource) => { 42 | const transformed = transform(fileSource); 43 | 44 | chai.assert.isOk(transformed, EXCEPTION_THROWN); 45 | done(); 46 | }); 47 | }); 48 | } 49 | 50 | function createNegativeTest(filename) { 51 | it(`in '${filename}'`, (done) => { 52 | utils.readFile(path.join(SOURCE_DIRECTORY_ERRORS, filename)).then((fileSource) => { 53 | const transformed = transform(fileSource); 54 | 55 | chai.assert.isNotOk(transformed, EXCEPTION_NOT_THROWN); 56 | done(); 57 | }); 58 | }); 59 | } 60 | 61 | Promise.all([ 62 | utils.readDirectory(SOURCE_DIRECTORY_NO_ERRORS), 63 | utils.readDirectory(SOURCE_DIRECTORY_ERRORS) 64 | ]).then(([positiveTestCases, negativeTestCases]) => { 65 | 66 | describe('[SMOKE] Strict mode', () => { 67 | 68 | describe('shouldn\'t throw exception', () => { 69 | positiveTestCases.forEach(createPositiveTest); 70 | }); 71 | 72 | describe('should throw exception', () => { 73 | negativeTestCases.forEach(createNegativeTest); 74 | }); 75 | 76 | }); 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /test/smoke/runtime.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const path = require('path'); 3 | const babel = require('babel-core'); 4 | const {VM} = require('vm2'); 5 | const config = require('../config.json'); 6 | const utils = require('../utils'); 7 | 8 | const DATA_DIRECTORY = path.join(config.path.smokeTestData, 'runtime'); 9 | const SOURCE_DIRECTORY_ERRORS = path.join(DATA_DIRECTORY, 'src', 'exception'); 10 | const SOURCE_DIRECTORY_NO_ERRORS = path.join(DATA_DIRECTORY, 'src', 'no-exception'); 11 | const EXPECTED_DIRECTORY = path.join(DATA_DIRECTORY, 'expected'); 12 | 13 | const BABEL_CONFIG = { 14 | plugins: [config.path.plugin] 15 | }; 16 | 17 | const sandbox = new VM(); 18 | 19 | function createPositiveTest(filename) { 20 | it(`in '${filename}'`, (done) => { 21 | utils.readFile(path.join(SOURCE_DIRECTORY_NO_ERRORS, filename)).then((fileSource) => { 22 | const transformedSource = babel.transform(fileSource, BABEL_CONFIG); 23 | 24 | try { 25 | sandbox.run(transformedSource.code); 26 | done(); 27 | } catch (e) { 28 | done(e); 29 | } 30 | }); 31 | }); 32 | } 33 | 34 | function createNegativeTest(filename) { 35 | it(`in '${filename}'`, (done) => { 36 | const expectedFile = filename.replace('.js', '.txt'); 37 | 38 | Promise.all([ 39 | utils.readFile(path.join(SOURCE_DIRECTORY_ERRORS, filename)), 40 | utils.readFile(path.join(EXPECTED_DIRECTORY, expectedFile)) 41 | ]) 42 | .then(([fileSource, fileExpected]) => { 43 | const transformedSource = babel.transform(fileSource, BABEL_CONFIG); 44 | 45 | try { 46 | sandbox.run(transformedSource.code); 47 | done('Exception not throwed'); 48 | } catch (exception) { 49 | chai.expect(exception.message.trim()).not.differentFrom(fileExpected.trim()); 50 | done(); 51 | } 52 | }); 53 | }); 54 | } 55 | 56 | Promise.all([ 57 | utils.readDirectory(SOURCE_DIRECTORY_NO_ERRORS), 58 | utils.readDirectory(SOURCE_DIRECTORY_ERRORS) 59 | ]).then(([positiveTestCases, negativeTestCases]) => { 60 | 61 | describe('[SMOKE] Runtime check', () => { 62 | 63 | describe('correctly pass validation', () => { 64 | positiveTestCases.forEach(createPositiveTest); 65 | }); 66 | 67 | describe('correctly throws error', () => { 68 | negativeTestCases.forEach(createNegativeTest); 69 | }); 70 | 71 | }); 72 | 73 | }); 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * How it works: 3 | * 1. It makes initialization - gets config, called template factories, sets flags to default state. 4 | * 2. Plugin traverse in 3 ways: 5 | * - basic function declarations (FunctionDeclaration, ArrowFunctionExpression, etc.) 6 | * - variable declarations with declared function 7 | * - return statement with function as argument 8 | * 3. It tries to find closest block comment, that can be relative to this function. 9 | * 4. It tries to parse it with doctrine (https://github.com/eslint/doctrine). 10 | * 5. It makes function body normalization (removes "(a) => a * a" cases, makes it multiline) 11 | * 6. It inserts type check function call for arguments. 12 | * 7. It makes traverse for all return statements, relative to this function. 13 | * 8. It adds typecheck function call into return statement. 14 | * 9. If some function were transformed by plugin, on exit of Program it adds typecheck function declaration to file. 15 | */ 16 | 17 | require('core-js/fn/object/assign'); 18 | require('core-js/fn/array/includes'); 19 | require('core-js/fn/string/includes'); 20 | 21 | const {defaults, functionName} = require('../shared/config.json'); 22 | const typecheckTemplate = require('./lib/typecheck-function-template'); 23 | const findGlobalDirective = require('./lib/find-global-directive'); 24 | const functionVisitorsFactory = require('./visitors'); 25 | 26 | 27 | /** 28 | * @param {Object} t 29 | * @returns {{visitor: Object}} 30 | */ 31 | module.exports = function ({types: t}) { 32 | const typecheckFunctionDeclaration = typecheckTemplate.function(functionName); 33 | const typecheckFunctionCall = typecheckTemplate.call(functionName, t); 34 | 35 | const globalState = Object.create(null); 36 | const basicStateControlVisitor = { 37 | Program: { 38 | /** 39 | * @param {NodePath} path 40 | * @param {PluginPass} state 41 | */ 42 | enter(path, state) { 43 | globalState.hasGlobalDirective = findGlobalDirective(path, state); 44 | globalState.shouldInjectHelperFunction = false; 45 | globalState.useStrict = state.opts.useStrict || defaults.useStrict; 46 | }, 47 | 48 | /** 49 | * @param {NodePath} path 50 | * @param {PluginPass} state 51 | */ 52 | exit(path, state) { 53 | if (!globalState.shouldInjectHelperFunction || state.opts._insertHelper === false) { 54 | return; 55 | } 56 | 57 | path.pushContainer('body', typecheckFunctionDeclaration()); 58 | } 59 | } 60 | }; 61 | 62 | return { 63 | visitor: Object.assign( 64 | basicStateControlVisitor, 65 | functionVisitorsFactory(typecheckFunctionCall, globalState, t) 66 | ) 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.2.1 4 | ### Bugfix 5 | * Fixed bug, when this case throws error: 6 | ```javascript 7 | /** 8 | * @param {{id: String}} data 9 | */ 10 | function test(data) {} 11 | 12 | test(null); 13 | ``` 14 | 15 | ### Other 16 | * Refactored some code; 17 | * Refactored some tests. 18 | 19 | ## 1.2.0 20 | ### Features 21 | * Changed behavior of record validation. Now it doesn't throw error in this case: 22 | ```javascript 23 | /** 24 | * @param {{a: Number, b: Number}} record 25 | * @returns {Number} 26 | */ 27 | function test(record) { 28 | return record.a + record.b; 29 | } 30 | 31 | test({ a: 1, b: 2, c: 3 }); 32 | ``` 33 | 34 | ### Other 35 | * Refactored comment finder; 36 | * Refactored function validator; 37 | * Minor performance increase. 38 | 39 | ## 1.1.5 40 | ### Bugfix 41 | * Fixed parsing and validation error for nested parameters 42 | 43 | ## 1.1.4 44 | ### Features 45 | * New error message 46 | ``` 47 | Uncaught TypeError: 48 | Parameter "data" in function "myFunction" has wrong type. 49 | Expected: {Object} 50 | Current: "[1, 2, 3]" 51 | ``` 52 | 53 | ### Bugfix 54 | * Fixed constructor validation; 55 | * Fixed performance of comment searching; 56 | * Fixed performance of comment parsing. 57 | 58 | 59 | ## 1.1.2 60 | ### Bugfix 61 | * Fixed adding double check to class method in some cases; 62 | * Fixed exception on return with type "any" in strict mode. 63 | 64 | ## 1.1.1 65 | ### Features 66 | * Added parsing for multiline Object definition: 67 | ```javascript 68 | /** 69 | * @param {Object} data 70 | * @param {Number} data.id 71 | * @param {String} data.name 72 | * @param {Number} data.status 73 | */ 74 | 75 | //equals to 76 | 77 | /** 78 | * @param {{id: Number, name: String, status: Number}} data 79 | */ 80 | ``` 81 | 82 | ### Bugfix 83 | * Fixed integration problem with class method parsing after es2015 transformation; 84 | * Added polyfill for es2015 features, now it's fully compatible with Node < `6.0.0`. 85 | 86 | ### Other 87 | * Added integration tests with es2015 preset; 88 | * Added a lot of smoke tests; 89 | * Increased performance; 90 | * Added Travis CI build on commit. 91 | 92 | 93 | ## 1.1.0 94 | 95 | ### Features 96 | * Added strict mode - now plugin throw compilation exception when it can find error by static analyze; 97 | * Flag - `useStrict: true` (default - false), add it to config; 98 | * Throw exception, when arguments/returns aren't equal to jsDoc. 99 | 100 | ### Bugfix 101 | * Fixed a lot of cases of wrong parsing of arguments and return; 102 | * Fixed bug, when assertion doesn't adds to empty return; 103 | * Fixed crashes on empty files with directive. 104 | 105 | ### Other 106 | * Global refactoring - better code style, easy to read; 107 | * Added a lot of tests, increased stability of library. 108 | -------------------------------------------------------------------------------- /src/lib/find-comment.js: -------------------------------------------------------------------------------- 1 | require('core-js/fn/string/includes'); 2 | 3 | const {defaults, doctrineConfig} = require('../../shared/config.json'); 4 | 5 | const DEFAULT_COMMENT_TOP_PADDING = -1; 6 | const ALLOWED_COMMENT_TYPE = 'CommentBlock'; 7 | const ALLOWED_DIRECTIVES = doctrineConfig.tags.filter(tag => tag !== 'name').map(tag => `@${tag}`); 8 | const ALLOWED_DIRECTIVES_COUNT = ALLOWED_DIRECTIVES.length; 9 | 10 | /** 11 | * @param {Object} options 12 | * @returns {String|Boolean} 13 | */ 14 | function getDirective(options) { 15 | let directive; 16 | 17 | if ('useDirective' in options && options.useDirective !== true) { 18 | directive = options.useDirective; 19 | } else { 20 | directive = defaults.useDirective; 21 | } 22 | 23 | return directive; 24 | } 25 | 26 | function getPathEnd(path) { 27 | if (path && path.node) { 28 | return path.node.end || DEFAULT_COMMENT_TOP_PADDING; 29 | } else { 30 | return DEFAULT_COMMENT_TOP_PADDING; 31 | } 32 | } 33 | 34 | /** 35 | * Fix for annoying edge case, when ObjectProperty node doesn't have start index 36 | * Workaround: find function body and get it's start index. 37 | * 38 | * @param {Path} path 39 | * @returns {Number} 40 | */ 41 | function getFunctionDecrarationStart(path) { 42 | if (path.isObjectProperty()) { 43 | return path.node.start || path.get('value.body').node.start; 44 | } else { 45 | return path.node.start; 46 | } 47 | } 48 | 49 | function findComment(comments, startPosition, endPosition) { 50 | let comment; 51 | 52 | for (let index = comments.length - 1; index >= 0; index--) { 53 | comment = comments[index]; 54 | 55 | if ( 56 | comment.type === ALLOWED_COMMENT_TYPE && 57 | comment.start > startPosition && 58 | comment.end < endPosition 59 | ) { 60 | break; 61 | } 62 | } 63 | 64 | return comment; 65 | } 66 | 67 | /** 68 | * @param {NodePath} path 69 | * @param {PluginPass} state 70 | * @param {Boolean} hasGlobalDirective 71 | * @returns {String|null} 72 | */ 73 | module.exports = (path, state, hasGlobalDirective) => { 74 | const node = path.node; 75 | const comments = node.leadingComments; 76 | 77 | let result = null; 78 | 79 | if (!comments) { 80 | return result; 81 | } 82 | 83 | const functionDeclarationStart = getFunctionDecrarationStart(path); 84 | const previousNodeEnd = path.key !== 0 ? 85 | getPathEnd(path.getPrevSibling()) : 86 | DEFAULT_COMMENT_TOP_PADDING; 87 | 88 | const comment = findComment(comments, previousNodeEnd, functionDeclarationStart); 89 | 90 | if (!comment) { 91 | return result; 92 | } 93 | 94 | const directive = getDirective(state.opts); 95 | 96 | if ( 97 | typeof directive === 'string' && 98 | !hasGlobalDirective && 99 | !comment.value.includes(`@${directive}`) 100 | ) { 101 | return result; 102 | } 103 | 104 | const commentValue = comment.value; 105 | 106 | for (let index = 0; index < ALLOWED_DIRECTIVES_COUNT; index++) { 107 | if (commentValue.includes(ALLOWED_DIRECTIVES[index])) { 108 | result = commentValue; 109 | break; 110 | } 111 | } 112 | 113 | return result; 114 | }; 115 | -------------------------------------------------------------------------------- /src/lib/insert-parameters-check.js: -------------------------------------------------------------------------------- 1 | const normalizeValidator = require('./normalize-validator'); 2 | const strictMode = require('./strict-mode'); 3 | 4 | /** 5 | * @param {String} functionName 6 | * @param {Function} functionTemplate 7 | * @param {NodePath} functionPath 8 | * @param {Object} jsDoc 9 | * @param {Boolean} useStrict 10 | * @param {Object} t 11 | */ 12 | module.exports = (functionName, functionTemplate, functionPath, jsDoc, useStrict, t) => { 13 | let parameters = jsDoc.parameters; 14 | let functionParameters = functionPath.get('params'); 15 | let functionBody = functionPath.get('body'); 16 | 17 | if (!functionBody.isBlockStatement()) return; 18 | 19 | let countOfInsertedArguments = insertAssertionBasedOn(functionParameters); 20 | 21 | if (useStrict) { 22 | let parametersKeys = Object.keys(parameters); 23 | 24 | if (parametersKeys.length > countOfInsertedArguments) { 25 | strictMode.throwException(functionPath, strictMode.ERROR.UNUSED_ARGUMENTS_IN_JSDOC); 26 | } 27 | } 28 | 29 | /** 30 | * @param {Array} nodes 31 | * @returns {Number} 32 | */ 33 | function insertAssertionBasedOn(nodes) { 34 | let shouldThrowException = true; 35 | let count = 0; 36 | let path, node, name; 37 | let type; 38 | 39 | for (let index = nodes.length - 1; index >= 0; index--) { 40 | shouldThrowException = true; 41 | path = nodes[index]; 42 | 43 | if (path.isAssignmentPattern()) { 44 | shouldThrowException = false; 45 | count += insertAssertionBasedOn( 46 | [path.get('left')] 47 | ); 48 | } else 49 | if (path.isRestElement()) { 50 | shouldThrowException = false; 51 | count += insertAssertionBasedOn( 52 | [path.get('argument')] 53 | ); 54 | } else 55 | if (path.isObjectProperty()) { 56 | shouldThrowException = false; 57 | count += insertAssertionBasedOn( 58 | [path.get('value')] 59 | ); 60 | } else 61 | if (path.isObjectPattern()) { 62 | shouldThrowException = false; 63 | count += insertAssertionBasedOn( 64 | path.get('properties') 65 | ); 66 | } else 67 | if (path.isArrayPattern()) { 68 | shouldThrowException = false; 69 | count += insertAssertionBasedOn( 70 | path.get('elements') 71 | ); 72 | } 73 | 74 | node = path.node; 75 | name = node.name; 76 | type = parameters[name]; 77 | 78 | if (!(name in parameters) || type === void(0)) { 79 | if (useStrict && shouldThrowException) { 80 | strictMode.throwException(path, strictMode.ERROR.NO_ARGUMENT_IN_JSDOC); 81 | } 82 | 83 | continue; 84 | } 85 | 86 | ++count; 87 | 88 | if (!type) { 89 | continue; 90 | } 91 | 92 | functionBody.unshiftContainer( 93 | 'body', 94 | functionTemplate( 95 | functionName, 96 | name, 97 | node, 98 | normalizeValidator(functionPath, type, t) 99 | ) 100 | ); 101 | } 102 | 103 | return count; 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /src/lib/parse-jsdoc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {{name: String, type: Object}} Parameter 3 | */ 4 | 5 | const config = require('../../shared/config.json'); 6 | const doctrine = require('doctrine'); 7 | 8 | const NESTED_PARAMETER_SEPARATOR = '.'; 9 | const WRONG_TYPES = { 10 | 'function': 'Function', 11 | 'object': 'Object', 12 | 'array': 'Array', 13 | 'number': 'Number', 14 | 'string': 'String', 15 | 'boolean': 'Boolean', 16 | 'bool': 'Boolean', 17 | 'any': '*' 18 | }; 19 | 20 | /** 21 | * @param {String} name 22 | * @returns {String} 23 | */ 24 | function normalizeConstructorName(name) { 25 | if (WRONG_TYPES[name]) { 26 | return WRONG_TYPES[name]; 27 | } 28 | 29 | return name; 30 | } 31 | 32 | /** 33 | * @param {Object|null} type 34 | * @returns {String|Array|Object|null|undefined} 35 | */ 36 | function normalizeTypes(type) { 37 | if (!type) { 38 | return void(0); 39 | } 40 | 41 | switch (type.type) { 42 | case doctrine.Syntax.AllLiteral: 43 | return null; 44 | 45 | case doctrine.Syntax.NullLiteral: 46 | return normalizeConstructorName('null'); 47 | 48 | case doctrine.Syntax.UndefinedLiteral: 49 | return normalizeConstructorName('undefined'); 50 | 51 | case doctrine.Syntax.NameExpression: 52 | return normalizeConstructorName(type.name); 53 | 54 | case doctrine.Syntax.UnionType: 55 | return type.elements.map(normalizeTypes); 56 | 57 | case doctrine.Syntax.TypeApplication: 58 | return { 59 | root: normalizeConstructorName(type.expression.name), 60 | children: type.applications.map(normalizeTypes) 61 | }; 62 | 63 | case doctrine.Syntax.OptionalType: 64 | return { 65 | parameter: normalizeTypes(type.expression), 66 | optional: true 67 | }; 68 | 69 | case doctrine.Syntax.RecordType: 70 | return { 71 | record: normalizeConstructorName('Object'), 72 | fields: type.fields.reduce((map, fieldType) => { 73 | map[fieldType.key] = normalizeTypes(fieldType); 74 | 75 | return map; 76 | }, {}) 77 | }; 78 | 79 | case doctrine.Syntax.FieldType: 80 | return normalizeTypes(type.value); 81 | 82 | case doctrine.Syntax.FunctionType: 83 | return normalizeConstructorName('Function'); 84 | 85 | case doctrine.Syntax.NonNullableType: 86 | case doctrine.Syntax.NullableType: 87 | return normalizeTypes(type.expression); 88 | 89 | default: 90 | return void(0); 91 | } 92 | } 93 | 94 | function findNameTag(tag) { 95 | return tag.title === 'name'; 96 | } 97 | 98 | function findParameterTag(tag) { 99 | return tag.title === 'param'; 100 | } 101 | 102 | function findReturnTag(tag) { 103 | return tag.title === 'return' || tag.title === 'returns'; 104 | } 105 | 106 | /** 107 | * @param {Array} paramsDescriptions 108 | * @param {Boolean} useStrict 109 | * @returns {Object} 110 | */ 111 | function convertParameters(paramsDescriptions, useStrict) { 112 | const parameters = {}; 113 | 114 | let parameter; 115 | let parameterName; 116 | let parameterRoot; 117 | let parameterField; 118 | 119 | // TODO refactor that shit 120 | for (let index = 0, count = paramsDescriptions.length; index < count; index++) { 121 | parameter = paramsDescriptions[index]; 122 | parameterName = parameter.name.split(NESTED_PARAMETER_SEPARATOR); 123 | 124 | if (parameterName.length === 1) { 125 | parameters[parameter.name] = normalizeTypes(parameter.type); 126 | continue; 127 | } 128 | 129 | parameterRoot = parameterName[0]; 130 | parameterField = parameterName[1]; 131 | 132 | if ( 133 | typeof parameters[parameterRoot] === 'object' && 134 | parameters[parameterRoot] !== null && 135 | 'fields' in parameters[parameterRoot] 136 | ) { 137 | parameters[parameterRoot].fields[parameterField] = normalizeTypes(parameter.type); 138 | } else if (parameters[parameterRoot] !== void(0) || !useStrict) { 139 | parameters[parameterRoot] = { 140 | record: parameters[parameterRoot], 141 | fields: { 142 | [parameterField]: normalizeTypes(parameter.type) 143 | } 144 | }; 145 | } else if (useStrict) { 146 | throw `Can't parse field "${parameterField}" in "${parameterRoot}" descriptor: "${parameterRoot}" is undefined.`; 147 | } 148 | } 149 | 150 | return parameters; 151 | } 152 | 153 | /** 154 | * @param {String} comment 155 | * @return {{tags: Array, description: String}} 156 | */ 157 | function parseJSDoc(comment) { 158 | return doctrine.parse(comment, config.doctrineConfig); 159 | } 160 | 161 | /** 162 | * @param {String} comment - comment with jsDoc 163 | * @param {Boolean} [useStrict] 164 | * @returns {{parameters: Object, [returnStatement]: Object, [name]: String}|null} 165 | */ 166 | module.exports.parseFunctionDeclaration = (comment, useStrict = false) => { 167 | const commentAst = parseJSDoc(comment); 168 | 169 | if (commentAst.tags.length === 0) { 170 | return null; 171 | } 172 | 173 | const tags = commentAst.tags; 174 | const nameDescription = tags.find(findNameTag); 175 | const paramsDescriptions = tags.filter(findParameterTag); 176 | const returnDescription = tags.find(findReturnTag); 177 | 178 | const result = { 179 | parameters: convertParameters(paramsDescriptions, useStrict) 180 | }; 181 | 182 | if (nameDescription) { 183 | result.name = nameDescription.name; 184 | } 185 | 186 | if (returnDescription) { 187 | result.returnStatement = normalizeTypes(returnDescription.type); 188 | } 189 | 190 | return result; 191 | }; 192 | -------------------------------------------------------------------------------- /src/visitors/index.js: -------------------------------------------------------------------------------- 1 | const config = require('../../shared/config.json'); 2 | 3 | const {parseFunctionDeclaration} = require('../lib/parse-jsdoc'); 4 | const canInjectIntoFunction = require('../lib/check-function'); 5 | const normalizeFunctionBody = require('../lib/normalize-function-body'); 6 | const insertParametersCheck = require('../lib/insert-parameters-check'); 7 | const findComment = require('../lib/find-comment'); 8 | const strictMode = require('../lib/strict-mode'); 9 | const returnStatementVisitorFactory = require('./return-statement'); 10 | const helpers = require('./helpers'); 11 | 12 | 13 | /** 14 | * @param {Function} typecheckFunctionCall 15 | * @param {Object} globalState 16 | * @param {Object} t 17 | * @returns {Object} 18 | */ 19 | module.exports = (typecheckFunctionCall, globalState, t) => { 20 | const returnTypecheckVisitor = returnStatementVisitorFactory(typecheckFunctionCall, globalState, t); 21 | 22 | /** 23 | * @param {String} comment 24 | * @param {NodePath} path 25 | * @param {String} [name] 26 | */ 27 | function executeFunctionTransformation(comment, path, name) { 28 | let jsDoc; 29 | 30 | try { 31 | jsDoc = parseFunctionDeclaration(comment, globalState.useStrict); 32 | } catch (error) { 33 | strictMode.throwSpecificException(path, 'jsDoc parser', error); 34 | } 35 | 36 | if (!jsDoc) { 37 | path.stop(); 38 | return; 39 | } 40 | 41 | const functionName = name || jsDoc.name || (path.node.id ? path.node.id.name : config.defaultFunctionName); 42 | 43 | normalizeFunctionBody(path); 44 | 45 | if (canInjectIntoFunction(path)) { 46 | insertParametersCheck(functionName, typecheckFunctionCall, path, jsDoc, globalState.useStrict, t); 47 | 48 | if (jsDoc.returnStatement || globalState.useStrict) { 49 | path.traverse(returnTypecheckVisitor, { 50 | jsDoc: jsDoc, 51 | functionPath: path, 52 | functionName: functionName 53 | }); 54 | } 55 | 56 | globalState.shouldInjectHelperFunction = true; 57 | } 58 | } 59 | 60 | /** 61 | * @param {NodePath} classDeclaration 62 | * @param {PluginPass} state 63 | */ 64 | function executeTraverseForNestedClass(classDeclaration, state) { 65 | classDeclaration.traverse({ 66 | ClassMethod(classPath) { 67 | const comment = findComment(classPath, state, globalState.hasGlobalDirective); 68 | 69 | if (!comment) return; 70 | 71 | executeFunctionTransformation( 72 | comment, classPath, 73 | helpers.createClassMethodName(classPath, classDeclaration) 74 | ); 75 | } 76 | }); 77 | } 78 | 79 | return { 80 | /** 81 | * @param {NodePath} path 82 | * @param {PluginPass} state 83 | */ 84 | VariableDeclaration(path, state) { 85 | const declarations = path.get('declarations'); 86 | const functionDeclaration = helpers.declarationIsFunction(declarations); 87 | 88 | if (functionDeclaration) { 89 | const comment = findComment(path, state, globalState.hasGlobalDirective); 90 | 91 | if (comment) { 92 | executeFunctionTransformation(comment, functionDeclaration); 93 | } 94 | } else { 95 | const classExpressionDeclaration = helpers.declarationIsClassExpression(declarations); 96 | 97 | if (classExpressionDeclaration) { 98 | executeTraverseForNestedClass(classExpressionDeclaration, state); 99 | } 100 | } 101 | }, 102 | 103 | /** 104 | * @param {NodePath} path 105 | * @param {PluginPass} state 106 | */ 107 | ReturnStatement(path, state) { 108 | const argument = path.get('argument'); 109 | 110 | if (!argument || !argument.isFunction()) return; 111 | 112 | const comment = findComment(path, state, globalState.hasGlobalDirective); 113 | 114 | if (!comment) return; 115 | 116 | executeFunctionTransformation(comment, argument); 117 | }, 118 | 119 | /** 120 | * @param {NodePath} path 121 | * @param {PluginPass} state 122 | */ 123 | AssignmentExpression(path, state) { 124 | const rightPath = path.get('right'); 125 | 126 | if (!rightPath.isFunction()) return; 127 | 128 | const expression = path.find(helpers.pathIsExpression); 129 | const comment = findComment(expression, state, globalState.hasGlobalDirective); 130 | 131 | if (!comment) return; 132 | 133 | executeFunctionTransformation(comment, rightPath); 134 | }, 135 | 136 | /** 137 | * @param {NodePath} path 138 | * @param {PluginPass} state 139 | */ 140 | ObjectProperty(path, state) { 141 | const value = path.get('value'); 142 | 143 | if (!value.isFunction()) return; 144 | 145 | const comment = findComment(path, state, globalState.hasGlobalDirective); 146 | 147 | if (!comment) return; 148 | 149 | executeFunctionTransformation(comment, value, path.node.key.name); 150 | }, 151 | 152 | /** 153 | * @param {NodePath} path 154 | * @param {PluginPass} state 155 | */ 156 | ClassMethod(path, state) { 157 | const comment = findComment(path, state, globalState.hasGlobalDirective); 158 | 159 | if (!comment) return; 160 | 161 | const classDeclaration = path.find(helpers.pathIsClassDeclaration); 162 | 163 | if (!classDeclaration) return; 164 | 165 | executeFunctionTransformation( 166 | comment, path, 167 | helpers.createClassMethodName(path, classDeclaration) 168 | ); 169 | }, 170 | 171 | /** 172 | * @param {NodePath} path 173 | * @param {PluginPass} state 174 | */ 175 | ObjectMethod(path, state) { 176 | const comment = findComment(path, state, globalState.hasGlobalDirective); 177 | 178 | if (!comment) return; 179 | 180 | executeFunctionTransformation(comment, path, path.node.key.name); 181 | }, 182 | 183 | /** 184 | * @param {NodePath} path 185 | * @param {PluginPass} state 186 | */ 187 | 'FunctionDeclaration|ArrowFunctionExpression'(path, state) { 188 | const comment = findComment(path, state, globalState.hasGlobalDirective); 189 | 190 | if (!comment) return; 191 | 192 | executeFunctionTransformation(comment, path); 193 | } 194 | }; 195 | }; 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Babel jsDoc runtime typecheck 2 | 3 | [![npm](https://img.shields.io/npm/v/babel-plugin-jsdoc-runtime-typecheck.svg)](https://www.npmjs.com/package/babel-plugin-jsdoc-runtime-typecheck) 4 | [![license](https://img.shields.io/github/license/johnthecat/babel-plugin-jsdoc-runtime-typecheck.svg)](https://github.com/johnthecat/babel-plugin-jsdoc-runtime-typecheck/blob/master/LICENSE) 5 | [![npm](https://img.shields.io/npm/dt/babel-plugin-jsdoc-runtime-typecheck.svg)](https://www.npmjs.com/package/babel-plugin-jsdoc-runtime-typecheck) 6 | [![Travis](https://img.shields.io/travis/johnthecat/babel-plugin-jsdoc-runtime-typecheck.svg)](https://travis-ci.org/johnthecat/babel-plugin-jsdoc-runtime-typecheck) 7 | 8 | ## Overview 9 | This plugin will add runtime typecheck, based on [jsDoc](http://usejsdoc.org/) annotation. 10 | It transform code like this: 11 | ```javascript 12 | // from 13 | 14 | /** 15 | * @param {Number} a 16 | * @returns {Number} 17 | * @typecheck 18 | */ 19 | function test(a) { 20 | return a; 21 | } 22 | 23 | // to 24 | 25 | function test(a) { 26 | __executeTypeCheck__('test', 'a', a, 'Number'); 27 | 28 | return __executeTypeCheck__('test', 'return', a, 'Number'); 29 | } 30 | ``` 31 | 32 | Result: 33 | Console error example 34 | 35 | 36 | **CAUTION: Use this plugin only in development, it will slow down your code (a lot of additional function calls and large helper function).** 37 | 38 | ## Motivation 39 | Flow is good solution, but it adds custom syntax to javascript code and adding it to existing project is quite hard. 40 | IDE's like Webstorm has good support of jsDoc and can add cool code completion tips, based on users comments. 41 | So, with this plugin, you can easy start to use benefits of strong typing in javascript code without any pain. 42 | Using this plugin in development also will speed up development, because it will reduce number of weird errors and behaviors. 43 | 44 | 45 | ## How to 46 | 47 | ### Install 48 | `npm install babel-plugin-jsdoc-runtime-typecheck --save-dev` 49 | 50 | ### Use 51 | _.babelrc_ 52 | ```json 53 | { 54 | "plugins": ["jsdoc-runtime-typecheck"] 55 | } 56 | ``` 57 | _js code - global directive_ 58 | ```javascript 59 | // @typecheck 60 | 61 | /** 62 | * @param {String} str 63 | * @returns {String} 64 | */ 65 | function makeMeLaugh(str) { 66 | return str + ' - ha-ha-ha!'; 67 | } 68 | ``` 69 | _js code - local directive_ 70 | ```javascript 71 | /** 72 | * @param {String} str 73 | * @returns {String} 74 | * @typecheck 75 | */ 76 | function makeMeLaugh(str) { 77 | return str + ' - ha-ha-ha!'; 78 | } 79 | ``` 80 | 81 | ### Configure 82 | 83 | #### useDirective 84 | By default, plugin will only parse docs with special directive `@typecheck`, you can change it like this: 85 | ``` 86 | { 87 | "plugins": [ 88 | ["jsdoc-runtime-typecheck", 89 | { 90 | //useDirective: 'typecheck' - this is default 91 | //useDirective: false - if you want to check all functions with jsDoc (useful for new projects) 92 | useDirective: 'makeMeHappy' - your custom directive 93 | } 94 | ] 95 | ] 96 | } 97 | ``` 98 | Then, use it: 99 | ```javascript 100 | // @makeMeHappy 101 | 102 | // or 103 | 104 | /** 105 | * @makeMeHappy 106 | * @param {Number} a 107 | * @returns {Number} 108 | */ 109 | ``` 110 | 111 | #### useStrict 112 | You can enable strict mode - in this mode plugin throw compilation exception when it can find error by static analyze. 113 | 114 | Setup: 115 | ``` 116 | { 117 | "plugins": [ 118 | ["jsdoc-runtime-typecheck", 119 | { 120 | //useStrict: false - default 121 | useStrict: true 122 | } 123 | ] 124 | ] 125 | } 126 | ``` 127 | Use: 128 | 129 | Code: 130 | ```javascript 131 | /** 132 | * @param {Number} a 133 | * @param {Number} b 134 | * @returns {Number} 135 | * @typecheck 136 | */ 137 | function test(a, b, c) { 138 | return a + b + c; 139 | } 140 | ``` 141 | Result in console: 142 | ```bash 143 | SyntaxError: input.js: [TYPECHECK STRICT MODE]: Function argument type annotation missing. 144 | 5 | * @typecheck 145 | 6 | */ 146 | > 7 | function test(a, b, c) { 147 | | ^ 148 | 8 | return a + b + c; 149 | 9 | } 150 | 10 | 151 | ``` 152 | 153 | ## Supports: 154 | 155 | ### jsDoc tags 156 | * `@params` can be optional, supported declarations: 157 | * `@param {*} name` - no check 158 | * `@param {Number=} name` - optional 159 | * `@param {Number} [name]` - optional 160 | * `@param {?Number} name` 161 | * `@param {!Number} name` 162 | * `@param {Number|String} id` 163 | * `@param {Array} collection` - check every item in array 164 | * Check defined keys in Object: 165 | ``` 166 | @param {Object} data 167 | @param {Number} data.id 168 | @param {String} data.name 169 | 170 | //or 171 | 172 | @param {{id: Number, name: String}} data 173 | ``` 174 | 175 | * `@param {function(Array)} name` - check type of function 176 | * `@returns` or `@return` - type annotation are same as in params. 177 | 178 | ### Language constructions 179 | 180 | #### Function declaration 181 | 182 | ```javascript 183 | /** 184 | * @param {Number} a 185 | * @returns {Number} 186 | */ 187 | function myDeclaredFunction(a) { 188 | return a; 189 | } 190 | 191 | /** 192 | * @param {Number} a 193 | * @returns {Number} 194 | */ 195 | let myExpressionFunction = function(a) { 196 | return a; 197 | }; 198 | 199 | /** 200 | * @param {Number} a 201 | * @returns {Number} 202 | */ 203 | let myArrowFunction = (a) => { 204 | return a; 205 | }; 206 | 207 | /** 208 | * @param {Number} a 209 | * @returns {Number} 210 | * In this case it will transform body to "{ return a; }" block 211 | */ 212 | let myArrowExpressionFunction = (a) => a; 213 | ``` 214 | 215 | #### Object method 216 | 217 | ```javascript 218 | let myObject = { 219 | /** 220 | * @param {Number} a 221 | * @returns {Number} 222 | */ 223 | myMethod(a) { 224 | return a; 225 | }, 226 | 227 | /** 228 | * @param {Number} a 229 | * @returns {Number} 230 | * Will use object field name as function name ("myField" here) 231 | */ 232 | myField: function(a) { 233 | return a; 234 | } 235 | } 236 | ``` 237 | 238 | #### Class constructor and method 239 | 240 | ```javascript 241 | class MyClass { 242 | /** 243 | * @param {Number} a 244 | */ 245 | constructor(a) { 246 | this._a = a; 247 | } 248 | 249 | /** 250 | * @param {Number} a 251 | * @returns {Number} 252 | */ 253 | myMethod(a) { 254 | return a; 255 | } 256 | 257 | /** 258 | * @param {Number} a 259 | * @returns {Number} 260 | */ 261 | static myStaticMethod(a) { 262 | return a; 263 | } 264 | 265 | /** 266 | * @param {Number} a 267 | */ 268 | set a(a) { 269 | this._a = a; 270 | } 271 | 272 | /** 273 | * @returns {Number} a 274 | */ 275 | get a() { 276 | return this._a; 277 | } 278 | } 279 | ``` 280 | -------------------------------------------------------------------------------- /shared/helper-function-declaration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CAUTION! This file should be written in ES5 syntax, because it will be included in final client code. 3 | * It also shouldn't be transformed via babel, because es2015 preset will add helpers, that breaks code. 4 | * TODO add documentation 5 | */ 6 | 7 | var babelTemplate = require('babel-template'); 8 | 9 | /** 10 | * @param {String} [functionName] 11 | * @param {String} parameterName 12 | * @param {*} parameter 13 | * @param {String} validatorSource 14 | * @returns {*} parameter 15 | */ 16 | function __TYPECHECK_HELPER_FUNCTION__(functionName, parameterName, parameter, validatorSource) { 17 | var isRootValid = false; 18 | var invalidType = null; 19 | var valid = false; 20 | var validator; 21 | 22 | if (typeof validatorSource === 'string') { 23 | validator = JSON.parse(validatorSource); 24 | 25 | valid = validateByType(parameter, validator); 26 | } else { 27 | validator = validatorSource; 28 | 29 | if (typeof validator === 'function') { 30 | valid = parameter instanceof validator; 31 | } else { 32 | valid = parameter === validator; 33 | } 34 | } 35 | 36 | if (!valid) { 37 | var ERROR_PADDING = (' ').repeat(4); 38 | var LINE_BREAK = '\n'; 39 | var argumentType = (parameterName === 'return' ? 'Return statement' : 'Parameter "' + parameterName + '"'); 40 | var message = ( 41 | LINE_BREAK + 42 | ERROR_PADDING + argumentType + ' in function "' + functionName + '" has wrong type.' + LINE_BREAK + 43 | ERROR_PADDING + 'Expected type: ' + makeTypeReadable(validator) + LINE_BREAK + 44 | ERROR_PADDING + 'Current value: "' + (invalidType || parameter) + '"' + LINE_BREAK 45 | ); 46 | var error = new TypeError(message); 47 | 48 | if (error.stack) { 49 | error.stack = error.stack.replace(/__TYPECHECK_HELPER_FUNCTION__/gm, argumentType + ' typecheck failed'); 50 | } 51 | 52 | throw error; 53 | } 54 | 55 | return parameter; 56 | 57 | // HELPERS 58 | 59 | function makeTypeReadable(validator) { 60 | let typeDeclaration; 61 | 62 | if (typeof validator === 'string') { 63 | typeDeclaration = validator; 64 | } 65 | 66 | if (Array.isArray(validator)) { 67 | typeDeclaration = validator.map(function(type) { 68 | return makeTypeReadable(type); 69 | }).join('|'); 70 | } 71 | 72 | if (typeof validator === 'object') { 73 | if ('root' in validator) { 74 | typeDeclaration = validator.root + '<' + makeTypeReadable(validator.children) + '>'; 75 | } 76 | 77 | if ('record' in validator) { 78 | typeDeclaration = JSON.stringify(validator.fields); 79 | } 80 | 81 | if ('optional' in validator) { 82 | typeDeclaration = makeTypeReadable(validator.parameter) + ' (optional)'; 83 | } 84 | } 85 | 86 | if (typeof validator === 'function') { 87 | typeDeclaration = validator.name; 88 | } 89 | 90 | return typeDeclaration; 91 | } 92 | 93 | 94 | function instanceOf(instance, constructorName) { 95 | while (instance !== null && instance !== void(0)) { 96 | if (instance.constructor.name === constructorName) { 97 | return true; 98 | } 99 | 100 | instance = Object.getPrototypeOf(instance); 101 | } 102 | 103 | return false; 104 | } 105 | 106 | 107 | function validateByType(parameter, type) { 108 | var isValid; 109 | var field; 110 | 111 | switch (type) { 112 | case void(0): 113 | return false; 114 | 115 | case null: 116 | return true; 117 | 118 | case 'null': 119 | return parameter === null; 120 | 121 | case 'undefined': 122 | return parameter === void(0); 123 | 124 | case 'Function': 125 | case 'Object': 126 | case 'String': 127 | case 'Number': 128 | return typeof parameter === type.toLowerCase(); 129 | 130 | case 'Array': 131 | return Array.isArray(parameter); 132 | } 133 | 134 | if (Array.isArray(type)) { 135 | return type.some(function (innerType) { 136 | return validateByType(parameter, innerType); 137 | }); 138 | } 139 | 140 | if (typeof type === 'object') { 141 | if ('root' in type) { 142 | isRootValid = validateByType(parameter, type.root); 143 | 144 | if (type.root === 'Promise') { 145 | return isRootValid; 146 | } 147 | 148 | var isChildrenValid = true; 149 | var isThisChildrenValid; 150 | 151 | for (field in parameter) { 152 | if (!parameter.hasOwnProperty(field)) continue; 153 | 154 | isThisChildrenValid = type.children.some(function (childType) { 155 | return validateByType(parameter[field], childType); 156 | }); 157 | 158 | if (!isThisChildrenValid) { 159 | isChildrenValid = false; 160 | } 161 | } 162 | 163 | isValid = isRootValid && isChildrenValid; 164 | 165 | if (!isValid && isRootValid) { 166 | try { 167 | invalidType = JSON.stringify(parameter); 168 | } catch (e) { 169 | invalidType = type.root + '<*>'; 170 | } 171 | } 172 | 173 | return isValid; 174 | } 175 | 176 | if ('optional' in type) { 177 | if (parameter === void(0)) { 178 | return true; 179 | } else { 180 | return validateByType(parameter, type.parameter); 181 | } 182 | } 183 | 184 | if ('record' in type) { 185 | if (parameter === null) { 186 | return false; 187 | } 188 | 189 | isRootValid = validateByType(parameter, type.record); 190 | var isRecordValid = true; 191 | var isThisRecordValid; 192 | 193 | 194 | for (field in type.fields) { 195 | if (!type.fields.hasOwnProperty(field)) continue; 196 | 197 | isThisRecordValid = validateByType(parameter[field], type.fields[field]); 198 | 199 | if (!isThisRecordValid) { 200 | isRecordValid = false; 201 | } 202 | } 203 | 204 | isValid = isRootValid && isRecordValid; 205 | 206 | if (!isValid && isRootValid) { 207 | try { 208 | invalidType = JSON.stringify(parameter); 209 | } catch (e) { 210 | invalidType = type.record + '<*>'; 211 | } 212 | } 213 | 214 | return isValid; 215 | } 216 | } 217 | 218 | return instanceOf(parameter, type); 219 | } 220 | } 221 | 222 | /** 223 | * @param {String} name 224 | * @returns {NodePath} helper AST 225 | */ 226 | module.exports = function (name) { 227 | var template = __TYPECHECK_HELPER_FUNCTION__.toString().replace(/__TYPECHECK_HELPER_FUNCTION__/gm, name); 228 | 229 | return babelTemplate(template); 230 | }; 231 | --------------------------------------------------------------------------------