├── cache └── .empty ├── test ├── fixtures │ ├── code │ │ ├── empty.js │ │ ├── typedef.js │ │ ├── root_scope.js │ │ ├── test.js │ │ ├── jsdoc_types.js │ │ ├── arrays_access.js │ │ ├── pass_vars.js │ │ ├── es6.js │ │ ├── arrays.js │ │ ├── unions.js │ │ ├── objects.js │ │ └── reassign.js │ ├── models │ │ ├── .empty │ │ ├── nested │ │ │ ├── human.json │ │ │ └── nested1 │ │ │ │ └── human2.json │ │ └── wrong_dir │ │ │ └── wrong.json │ ├── .simple.yml │ ├── .eslintrc.yml │ ├── redux │ │ └── reducer.js │ └── cached-models.json ├── units │ ├── redux.js │ ├── load-schemas.js │ ├── adapters.js │ └── comments.js └── code.js ├── .travis.yml ├── models ├── var.yml ├── movie.yml ├── scope.yml └── settings.yml ├── todo ├── lib ├── config.js ├── index.js ├── helpers │ └── babelrc.js ├── traverse.js ├── schemas │ ├── formats │ │ ├── redux.js │ │ └── json.js │ ├── load.js │ ├── accessor.js │ └── generator │ │ ├── type.js │ │ └── index.js ├── comments.js ├── types │ └── jsdoc.js ├── validation.js └── passing.js ├── rules ├── composite.js ├── primitive.js └── typelint.js ├── example └── index.js ├── adapters ├── to-snake-case.js └── to-camel-case.js ├── CHANGELOG.md ├── .eslintrc.yml ├── jsonSchemas.xml ├── package.json ├── LICENSE └── README.md /cache/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/code/empty.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/models/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/code/typedef.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 4 5 | -------------------------------------------------------------------------------- /models/var.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | varName: 'string' 3 | varType: 'string' 4 | kind: 'string' -------------------------------------------------------------------------------- /todo: -------------------------------------------------------------------------------- 1 | getValuesWithScreenSizes(campaign.data).totalAis // thinks that totalAis is a prop of campaign.data -------------------------------------------------------------------------------- /test/fixtures/.simple.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | node: true 4 | es6: true 5 | rules: 6 | typelint: 2 -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | var _settings; 2 | module.exports = { 3 | get settings() { 4 | return _settings; 5 | }, 6 | set settings(val) { 7 | _settings = val; 8 | } 9 | }; -------------------------------------------------------------------------------- /rules/composite.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context) { 2 | throw new Error('Composite rule is deprecated. Please use one rule typelint/typeint for everything in your eslint config'); 3 | }; 4 | -------------------------------------------------------------------------------- /rules/primitive.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context) { 2 | throw new Error('Primitive rule is deprecated. Please use one rule typelint/typeint for everything in your eslint config'); 3 | }; 4 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | typelint: require('../rules/typelint'), 4 | composite: require('../rules/composite'), 5 | primitive: require('../rules/primitive') 6 | } 7 | }; -------------------------------------------------------------------------------- /test/fixtures/code/root_scope.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test regular function with parameters 3 | * @param man 4 | * @param {Boolean} flag 5 | * @returns {number} 6 | */ 7 | var man = require('fdgd'); 8 | var zoko = man.firrstName; -------------------------------------------------------------------------------- /test/fixtures/code/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test regular function with parameters 3 | * @param man 4 | * @param {Boolean} flag 5 | * @returns {number} 6 | */ 7 | function test(man, flag) { 8 | var zoko = man.first_Name; 9 | } -------------------------------------------------------------------------------- /test/fixtures/code/jsdoc_types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {String} str 4 | * @param {Array} arr 5 | */ 6 | function abc(str, arr) { 7 | var u =str.toUpperCase(); 8 | var u2 =str.toUppeerCase(); 9 | var l = arr.length; 10 | } -------------------------------------------------------------------------------- /test/fixtures/code/arrays_access.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test regular function with parameters 3 | * @param man 4 | * @param {Boolean} flag 5 | * @returns {number} 6 | */ 7 | function test(man, flag) { 8 | var len = man.friends.name; 9 | } -------------------------------------------------------------------------------- /test/fixtures/code/pass_vars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test transfer data with variables 3 | * @param man 4 | * @param {Boolean} flag 5 | * @returns {number} 6 | */ 7 | function test(man, flag) { 8 | var zoko = man.first_name; 9 | zoko.wrong = true; 10 | } -------------------------------------------------------------------------------- /test/fixtures/code/es6.js: -------------------------------------------------------------------------------- 1 | const a = 1212; 2 | const b = 43535; 3 | const util = require('util'); 4 | /** 5 | * @param man 6 | * @param {Boolean} flag 7 | * @returns {number} 8 | */ 9 | const convertShopToClient = (man, flag) => { 10 | man.age = 34; 11 | } -------------------------------------------------------------------------------- /models/movie.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | creators: 3 | type: object 4 | properties: 5 | director: string 6 | screenwriter: string 7 | producer: string 8 | actors: 9 | type: array 10 | items: 11 | type: string 12 | title: string -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | const db = {}; // mock db 2 | 3 | function getActors(movieId) { 4 | return db.findOne({ _id: movieId }).then( 5 | /** 6 | * @param movie 7 | */ 8 | (movie) => { 9 | return movie.actors; 10 | }); 11 | } 12 | 13 | module.exports = getActors; -------------------------------------------------------------------------------- /test/fixtures/code/arrays.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test regular function with parameters 3 | * @param man 4 | * @param {Boolean} flag 5 | * @returns {number} 6 | */ 7 | function test(man, flag) { 8 | var len = man.friends.length; 9 | var map = man.friends.map(friend => friend.name); 10 | } -------------------------------------------------------------------------------- /test/fixtures/code/unions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test regular function with parameters 3 | * @param {(human|String)} man 4 | * @param {Boolean} flag 5 | * @returns {number} 6 | */ 7 | function test(man, flag) { 8 | var zoko = man.first_name; 9 | zoko = man.toUpperCase(); 10 | zoko = man.wrong; 11 | } -------------------------------------------------------------------------------- /test/fixtures/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | env: 4 | node: true 5 | es6: true 6 | plugins: 7 | - 8 | typelint 9 | rules: 10 | typelint/typelint: 2 11 | settings: 12 | typelint: 13 | models: 14 | json: 15 | dir: './test/fixtures/models' 16 | useCache: false -------------------------------------------------------------------------------- /models/scope.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | props: 3 | items: 4 | type: 'string' 5 | init: 6 | type: 'object' 7 | properties: 8 | start: 'number' 9 | end: 'number' 10 | typedVars: 11 | items: 12 | type: 'string' 13 | nativeVars: 14 | items: 15 | type: 'object' 16 | $ref: './var.yml' 17 | debug: 'boolean' 18 | functionNode: 'object' -------------------------------------------------------------------------------- /test/fixtures/code/objects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test regular function with parameters 3 | * @param {object} man 4 | * @param {object} man.education 5 | * @param {number} man.age 6 | * @param {object} man.traits 7 | * @param {number} man.education.years 8 | */ 9 | function test(man) { 10 | var education = man.education; 11 | var years = man.education.years; 12 | var wrong = man.education.wrong; 13 | } -------------------------------------------------------------------------------- /test/units/redux.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production'; 2 | var assert = require('assert'); 3 | var reduxFormater = require('../../lib/schemas/formats/redux'); 4 | 5 | describe('Reducer', () => { 6 | it('Reducer', function() { 7 | this.timeout(0); 8 | var schema = reduxFormater({ 9 | reducerPath: __dirname + '/../fixtures/redux/reducer' 10 | }); 11 | assert.equal(schema.ReduxState.properties.test2.properties.prop33.type, 'number'); 12 | }); 13 | }); -------------------------------------------------------------------------------- /test/fixtures/redux/reducer.js: -------------------------------------------------------------------------------- 1 | var combineReducers = require('redux').combineReducers; 2 | 3 | var initialState1 = { 4 | prop1: [], 5 | prop2: '', 6 | prop3: 0 7 | } 8 | 9 | var initialState2 = { 10 | prop11: [], 11 | prop22: '', 12 | prop33: 0 13 | } 14 | 15 | 16 | function test1(state, action) { 17 | return initialState1; 18 | } 19 | 20 | function test2(state, action) { 21 | return initialState2; 22 | } 23 | 24 | module.exports = combineReducers({ 25 | test1, 26 | test2 27 | }) -------------------------------------------------------------------------------- /adapters/to-snake-case.js: -------------------------------------------------------------------------------- 1 | function convertString(prop) { 2 | return prop.split(/(?=[A-Z])/).join('_').toLowerCase(); 3 | } 4 | 5 | function lookup(schema) { 6 | var newKey; 7 | if (typeof schema !== 'object') return schema; 8 | return Object.keys(schema).reduce(function (prev, key, i) { 9 | newKey = convertString(key); 10 | prev[newKey] = lookup(schema[key]); 11 | return prev; 12 | }, {}); 13 | } 14 | 15 | module.exports = function (schemas) { 16 | return lookup(schemas); 17 | } 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.3 2 | 3 | Features: 4 | 5 | * Redux state support as type 6 | 7 | # 1.0.6 8 | 9 | Changes: 10 | 11 | * One rule typelint/typelint for everything 12 | 13 | Features: 14 | 15 | * Linting union and object types in JSDoc 16 | * Support TypeLint types in JSDoc format (curly braces) 17 | 18 | # 1.0.7 19 | 20 | * Fixed bugs with wrong types detection in comments 21 | * Support of swagger schema files with definitions section 22 | * Improved documentation and added example project https://github.com/yarax/typelint-example -------------------------------------------------------------------------------- /test/units/load-schemas.js: -------------------------------------------------------------------------------- 1 | var json = require('../../lib/schemas/formats/json'); 2 | var assert = require('assert'); 3 | 4 | describe('Schema loaders', function () { 5 | 6 | it('JSON schema load directory', function () { 7 | var settings = { 8 | dir: './test/fixtures/models', 9 | exclude: 'wrong_dir' 10 | }; 11 | var schemas = json(settings); 12 | assert.equal(typeof schemas.human, 'object'); 13 | assert.equal(typeof schemas.human2, 'object'); 14 | assert.equal(typeof schemas.wrong, 'undefined'); 15 | }); 16 | 17 | }); -------------------------------------------------------------------------------- /models/settings.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | models: 3 | type: 'object' 4 | properties: 5 | json: 6 | type: 'object' 7 | proprties: 8 | dir: 'string' 9 | swagger: 'string' 10 | adapters: 11 | type: 'array' 12 | items: 13 | type: 'string' 14 | exclude: 15 | type: 'array' 16 | items: 17 | type: 'string' 18 | redux: 19 | type: 'object' 20 | properties: 21 | reducerPath: 'string' 22 | useCache: 'boolean' 23 | 24 | useCache: 'boolean' -------------------------------------------------------------------------------- /test/fixtures/code/reassign.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test transfer data with variables 3 | * @param man 4 | */ 5 | function test(man) { 6 | // reassign before using 7 | man = null; 8 | var a = man.firstName.a.b.c; 9 | } 10 | /** 11 | * Test transfer data with variables 12 | * @param man 13 | */ 14 | function test2(man) { 15 | var a = man.wrong; 16 | //reassign after using 17 | man = null; 18 | } 19 | /** 20 | * Test transfer data with variables 21 | * @items {Array} 22 | */ 23 | function test3(items) { 24 | var a = items.wrong; 25 | items = null; 26 | //reassign after using 27 | } -------------------------------------------------------------------------------- /adapters/to-camel-case.js: -------------------------------------------------------------------------------- 1 | function convertString(prop) { 2 | if (prop === '_id') return '_id'; 3 | return prop.split('_') 4 | .map(function (part, i) { 5 | if (!i) return part; 6 | return (part[0] ? part[0].toUpperCase() : '') + part.substr(1); 7 | }) 8 | .join(''); 9 | } 10 | 11 | function lookup(schema) { 12 | var newKey; 13 | if (typeof schema !== 'object') return schema; 14 | return Object.keys(schema).reduce(function (prev, key, i) { 15 | newKey = convertString(key); 16 | prev[newKey] = lookup(schema[key]); 17 | return prev; 18 | }, {}); 19 | } 20 | 21 | module.exports = function (schemas) { 22 | return lookup(schemas); 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | node: true 4 | es6: true 5 | plugins: 6 | - 7 | typelint 8 | rules: 9 | eol-last: 0 10 | no-trailing-spaces: 0 11 | func-names: 0 12 | comma-dangle: 0 13 | no-param-reassign: 0 14 | no-use-before-define: 0 15 | global-require: 0 16 | no-underscore-dangle: 0 17 | one-var: 0 18 | one-var-declaration-per-line: 0 19 | max-len: 0 20 | valid-typeof: 2 21 | typelint/typelint: 2 22 | extends: 'airbnb-base/legacy' 23 | settings: 24 | typelint: 25 | models: 26 | json: 27 | dir: 'models' 28 | adapters: ['./adapters/to-camel-case'] 29 | redux: 30 | reducerPath: '/home/roman/frontend/src/client/redux/typelintReducerWrapper.js' 31 | useCache: false -------------------------------------------------------------------------------- /test/fixtures/cached-models.json: -------------------------------------------------------------------------------- 1 | {"movie":{"properties":{"pro_creators":{"type":"object","properties":{"pro_director":"string","screenwriter":"string","producer":"string"},"actors":{"type":"array","items":{"type":"string"}}},"title":"string"}},"scope":{"properties":{"odd_props":{"items":{"type":"string"}},"init":{"type":"object","properties":{"start":"number","end":"number"}},"typedVars":{"items":{"type":"string"}},"nativeVars":{"items":{"type":"object","$ref":"./var.yml"}},"debug":"boolean","functionNode":"object"}},"settings":{"properties":{"lintNative":"boolean","modelsDir":"string","adapters":{"type":"array","items":{"type":"string"}},"excludeModelDirs":{"type":"array","items":{"type":"string"}},"useCache":"boolean"}},"var":{"properties":{"varName":"string","varType":"string","kind":"string"}}} -------------------------------------------------------------------------------- /lib/helpers/babelrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on https://github.com/babel/babel-loader/blob/9513e9b6661c94dc3c810707f185edcb6cc94990/index.js 3 | */ 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var exists = fs.existsSync; 7 | 8 | var find = function (start, rel) { 9 | var file = path.join(start, rel); 10 | var opts = {}; 11 | var up = ''; 12 | 13 | if (exists(file)) { 14 | return fs.readFileSync(file, 'utf8') 15 | } 16 | 17 | up = path.dirname(start); 18 | if (up !== start) { 19 | // Reached root 20 | return find(up, rel); 21 | } 22 | 23 | }; 24 | /** 25 | * Returns babelrc content related to file 26 | * @param loc 27 | * @param rel 28 | */ 29 | module.exports = function (loc, rel) { 30 | rel = rel || '.babelrc'; 31 | return JSON.parse(find(loc, rel)); 32 | }; -------------------------------------------------------------------------------- /test/fixtures/models/nested/human.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Human", 3 | "type": "object", 4 | "properties": { 5 | "first_name": { 6 | "type": "string" 7 | }, 8 | "last_name": { 9 | "type": "string" 10 | }, 11 | "age": { 12 | "description": "Age in years", 13 | "type": "integer", 14 | "minimum": 0 15 | }, 16 | "friends": { 17 | "items": { 18 | "type": "object", 19 | "properties": { 20 | "name": "string", 21 | "age": "number", 22 | "skills": { 23 | "type": "object", 24 | "properties": { 25 | "mafia": "boolean", 26 | "civilian": "boolean" 27 | } 28 | } 29 | } 30 | } 31 | 32 | } 33 | }, 34 | "required": ["firstName", "lastName"] 35 | } -------------------------------------------------------------------------------- /test/units/adapters.js: -------------------------------------------------------------------------------- 1 | var toCamel = require('../../adapters/to-camel-case'); 2 | var toSnake = require('../../adapters/to-snake-case'); 3 | var json = require('../fixtures/cached-models.json'); 4 | var assert = require('assert'); 5 | 6 | describe('Adapters', function () { 7 | 8 | it('To camel case adapter', function () { 9 | var newObj = toCamel(json); 10 | assert.equal(!!newObj.movie.properties.proCreators, true); 11 | assert.equal(!!newObj.movie.properties.proCreators.properties.proDirector, true); 12 | assert.equal(!!newObj.scope.properties.oddProps, true); 13 | }); 14 | 15 | it('To snake case adapter', function () { 16 | var newObj = toSnake(json); 17 | assert.equal(!!newObj.scope.properties.typed_vars, true); 18 | assert.equal(!!newObj.var.properties.var_name, true); 19 | }); 20 | 21 | }); -------------------------------------------------------------------------------- /test/fixtures/models/wrong_dir/wrong.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Human2", 3 | "type": "object", 4 | "properties": { 5 | "first_name": { 6 | "type": "string" 7 | }, 8 | "last_name": { 9 | "type": "string" 10 | }, 11 | "age": { 12 | "description": "Age in years", 13 | "type": "integer", 14 | "minimum": 0 15 | }, 16 | "friends": { 17 | "items": { 18 | "type": "object", 19 | "properties": { 20 | "name": "string", 21 | "age": "number", 22 | "skills": { 23 | "type": "object", 24 | "properties": { 25 | "mafia": "boolean", 26 | "civilian": "boolean" 27 | } 28 | } 29 | } 30 | } 31 | 32 | } 33 | }, 34 | "required": ["firstName", "lastName"] 35 | } -------------------------------------------------------------------------------- /test/fixtures/models/nested/nested1/human2.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Human", 3 | "type": "object", 4 | "properties": { 5 | "first_name": { 6 | "type": "string" 7 | }, 8 | "last_name": { 9 | "type": "string" 10 | }, 11 | "age": { 12 | "description": "Age in years", 13 | "type": "integer", 14 | "minimum": 0 15 | }, 16 | "friends": { 17 | "items": { 18 | "type": "object", 19 | "properties": { 20 | "name": "string", 21 | "age": "number", 22 | "skills": { 23 | "type": "object", 24 | "properties": { 25 | "mafia": "boolean", 26 | "civilian": "boolean" 27 | } 28 | } 29 | } 30 | } 31 | 32 | } 33 | }, 34 | "required": ["firstName", "lastName"] 35 | } -------------------------------------------------------------------------------- /jsonSchemas.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /lib/traverse.js: -------------------------------------------------------------------------------- 1 | var grabComments = require('./comments'); 2 | 3 | var functinable = ['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression']; 4 | 5 | /** 6 | * Traverses the current scope and collects declarations 7 | * @param {Object} node 8 | * @param {Object} scope 9 | * @returns {Object} scope 10 | */ 11 | function traverseScope(node, scope) { 12 | scope = grabComments(node, scope); 13 | 14 | if (functinable.indexOf(node.type) !== -1) { 15 | scope.functionNode = node.body; 16 | } 17 | 18 | if (node.type === 'MemberExpression' && node.property) { 19 | if (node.computed) { 20 | scope.props.push({computed: true, name: node.property.name}); 21 | } else { 22 | scope.props.push(node.property.name); 23 | } 24 | } 25 | if (node.parent) { 26 | return traverseScope(node.parent, scope); 27 | } 28 | return scope; 29 | } 30 | 31 | module.exports = traverseScope; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-typelint", 3 | "version": "1.0.9", 4 | "main": "lib/index", 5 | "description": "ESlint rule for optional typing in JavaScript, based on JSDoc", 6 | "keywords": [ 7 | "jsdoc", 8 | "eslint", 9 | "types", 10 | "js typing" 11 | ], 12 | "scripts": { 13 | "test": "./node_modules/.bin/mocha test/units/*" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "babel-register": "^6.16.3", 19 | "doctrine": "^1.2.2", 20 | "eslint": "^3.0.1", 21 | "js-yaml": "^3.6.1", 22 | "lodash": "^4.13.1" 23 | }, 24 | "devDependencies": { 25 | "babel-preset-es2015": "^6.9.0", 26 | "babel-preset-stage-0": "^6.5.0", 27 | "eslint": "^3.13.1", 28 | "eslint-config-airbnb": "12.0.0", 29 | "eslint-config-airbnb-base": "8.0.0", 30 | "eslint-plugin-import": "^1.8.1", 31 | "mocha": "^2.5.2", 32 | "redux": "^3.5.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/schemas/formats/redux.js: -------------------------------------------------------------------------------- 1 | var generate = require('../generator'); 2 | var nodePath = require('path'); 3 | require('babel-register')(); 4 | 5 | /** 6 | * @param settings 7 | */ 8 | function redux(settings) { 9 | // prevent using hot loaders while compiling 10 | process.env.NODE_ENV = 'production'; 11 | var reducer; 12 | var path = settings.reducerPath; 13 | 14 | if (!settings.reducerPath) { 15 | throw new Error('Found typelint models option redux, but there is no reducerPath option inside'); 16 | } 17 | 18 | if (!nodePath.isAbsolute(path)) { 19 | path = process.cwd() + '/' + path; 20 | } 21 | 22 | reducer = require(path); 23 | if (typeof reducer !== 'function') { 24 | if (typeof reducer.default !== 'function') { 25 | throw new Error('Reducer must be a function. See more http://redux.js.org/docs/basics/Reducers.html'); 26 | } else { 27 | reducer = reducer.default; 28 | } 29 | } 30 | 31 | return { 32 | ReduxState: generate(reducer({}, {type: ''})) 33 | }; 34 | } 35 | 36 | module.exports = redux; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Roman Krivtsov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /rules/typelint.js: -------------------------------------------------------------------------------- 1 | var traverseScope = require('../lib/traverse'); 2 | var validation = require('../lib/validation'); 3 | var config = require('../lib/config'); 4 | var rule; 5 | /** 6 | * @param {Object} context 7 | * @param {settings} settings 8 | * @return {Function} 9 | */ 10 | function handleMemberExpressions(context, settings) { 11 | return function (node) { 12 | var scope; 13 | if (node.object && node.object.name) { 14 | // memoize settings 15 | config.settings = settings; 16 | scope = traverseScope(node, { 17 | init: { 18 | start: node.start, 19 | end: node.end 20 | }, 21 | props: [], 22 | typedVars: [], 23 | nativeVars: [], 24 | }); 25 | 26 | if (scope.props.length && scope.typedVars.length) { 27 | scope.typedVars.some(function (param) { 28 | if (param.varName === node.object.name) { 29 | validation.validate(param, scope, node, context); 30 | return true; 31 | } 32 | return false; 33 | }); 34 | } 35 | } 36 | }; 37 | } 38 | 39 | /** 40 | * @param {Object} context 41 | * @returns {{MemberExpression: (function)}} 42 | */ 43 | rule = function (context) { 44 | return { 45 | MemberExpression: handleMemberExpressions(context, context.settings.typelint), 46 | }; 47 | }; 48 | 49 | rule.handleMemberExpressions = handleMemberExpressions; 50 | module.exports = rule; -------------------------------------------------------------------------------- /lib/comments.js: -------------------------------------------------------------------------------- 1 | var doctrine = require('doctrine'); 2 | var jsdoc = require('./types/jsdoc'); 3 | var _ = require('lodash'); 4 | 5 | function getTypeLintType(description) { 6 | if (!description) return null; 7 | var m = description.match(/<(.*?)>/); 8 | return m ? m[1] : null; 9 | } 10 | 11 | function parseWithDoctrine(commentString) { 12 | //console.time('doctrine'); 13 | var ast = doctrine.parse(commentString, { unwrap: true }); 14 | //assert.equal(scope.typedVars[0].varDefinedType.varname.properties.b.c, 'str 15 | var definedTypes = {}; 16 | //console.timeEnd('doctrine'); 17 | return ast.tags 18 | .filter(function (tag) { 19 | return tag.title === 'param'; 20 | }) 21 | .reduce(function (allItems, item) { 22 | var typeObj = { 23 | varName: item.name, 24 | definedTypes: definedTypes 25 | }; 26 | if (item.type) { 27 | // consider nested 28 | if (item.type.type === 'NameExpression') { 29 | // collecting object types properties 30 | definedTypes = jsdoc.collectSlicedTypes(item, definedTypes); 31 | if (item.name.match(/\./)) { // var name contains dot -- this is object type described in JSDoc 32 | typeObj.varType = item.name; 33 | } else if (item.type.name.toLowerCase() === 'object') { // no dots, but it's object type 34 | typeObj.varType = item.name; 35 | } else { 36 | typeObj.varType = item.type.name; 37 | } 38 | } 39 | if (item.type.type === 'RecordType' || item.type.type === 'ArrayType') { 40 | typeObj.varDefinedType = jsdoc.buildObjectType(item.type); 41 | } 42 | if (item.type.type === 'UnionType') { 43 | typeObj.unions = jsdoc.getAllUnions(item.type.elements); 44 | } 45 | 46 | allItems.push(typeObj); 47 | } 48 | var tlt = getTypeLintType(item.description); 49 | if (tlt) { 50 | allItems.push({ 51 | varName: item.name, 52 | varType: tlt, 53 | }); 54 | } 55 | return allItems; 56 | }, []); 57 | } 58 | 59 | /** 60 | * Handle nodes with leadingComments 61 | * @param {Object} node 62 | * @param {Object} scope 63 | * @returns scope 64 | */ 65 | function grabComments(node, scope) { 66 | if (node.leadingComments) { 67 | scope.typedVars = scope.typedVars.concat(parseWithDoctrine(node.leadingComments[0].value)); 68 | } 69 | return scope; 70 | } 71 | 72 | module.exports = grabComments; -------------------------------------------------------------------------------- /lib/schemas/load.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | var nodePath = require('path'); 3 | var fs = require('fs'); 4 | var _ = require('lodash'); 5 | var cachedPrimitives; 6 | var cachedComposite; 7 | 8 | function getFromCache() { 9 | try { 10 | return require('../../cache/models.json'); 11 | } catch (e) { 12 | return false; 13 | } 14 | } 15 | 16 | function cacheSchema(schema) { 17 | return fs.writeFileSync(__dirname + '/../../cache/models.json', JSON.stringify(schema)); 18 | } 19 | 20 | /** 21 | * Get all kind of schemas 22 | * @param settings 23 | * @returns {Object} collected schemas key-value 24 | */ 25 | function collectAllSchemas(settings) { 26 | var jsonSchema = {}; 27 | var reduxSchema = {}; 28 | var schemas = {}; 29 | if (settings.json) { 30 | jsonSchema = require('./formats/json')(settings.json); 31 | } 32 | if (settings.redux) { 33 | reduxSchema = require('./formats/redux')(settings.redux); 34 | } 35 | schemas = _.assign(jsonSchema, reduxSchema); 36 | 37 | return schemas; 38 | } 39 | 40 | /** 41 | * Preparing models 42 | * @param {Object} settings 43 | */ 44 | function loadComposite(settings) { 45 | if (cachedComposite) return cachedComposite; 46 | var schemas; 47 | if (!settings || !settings.models) { 48 | throw new Error('Please provide settings.typelint.models section in your eslint config'); 49 | } 50 | if (settings.useCache) { 51 | schemas = getFromCache() || cacheSchema(collectAllSchemas(settings.models)); 52 | } else { 53 | schemas = collectAllSchemas(settings.models); 54 | } 55 | cachedComposite = schemas; 56 | return schemas; 57 | } 58 | 59 | function loadPrimitive() { 60 | if (cachedPrimitives) return cachedPrimitives; 61 | var wrap = function (props, title) { 62 | return { 63 | type: 'object', 64 | title: title, 65 | properties: props.reduce(function (prev, prop) { 66 | prev[prop] = 'string'; 67 | return prev; 68 | }, {}) 69 | }; 70 | }; 71 | cachedPrimitives = { 72 | string: wrap(Object.getOwnPropertyNames(String.prototype), 'string'), 73 | array: wrap(Object.getOwnPropertyNames(Array.prototype), 'array'), 74 | number: wrap(Object.getOwnPropertyNames(Number.prototype), 'number'), 75 | boolean: wrap(Object.getOwnPropertyNames(Boolean.prototype), 'object'), 76 | }; 77 | return cachedPrimitives; 78 | } 79 | 80 | module.exports = { 81 | loadPrimitive: loadPrimitive, 82 | loadComposite: loadComposite, 83 | }; -------------------------------------------------------------------------------- /lib/types/jsdoc.js: -------------------------------------------------------------------------------- 1 | var accessor = require('../schemas/accessor'); 2 | 3 | function buildObjectType(type) { 4 | var schema = {}; 5 | if (type.type === 'NameExpression') { 6 | const endSchema = accessor.getSchemaByType(type.name); 7 | return endSchema || type.name; 8 | } else if (type.type === 'RecordType') { 9 | schema.type = 'object'; 10 | schema.properties = (type.fields || []).reduce((props, field) => { 11 | props[field.key] = buildObjectType(field.value); 12 | return props; 13 | }, {}); 14 | } else if (type.type === 'ArrayType') { 15 | schema.type = 'array'; 16 | // supported only elements of one type 17 | schema.items = buildObjectType(type.elements[0]); 18 | } else if (type.type === 'FieldType') { 19 | schema.type = 'object'; 20 | schema.properties = {}; 21 | schema.properties[type.key] = buildObjectType(type.value); 22 | } 23 | return schema; 24 | } 25 | 26 | function collectNestedTypes(item, definedTypes) { 27 | const type = {}; 28 | if (item.type.type === 'RecordType') { 29 | type.type = 'object'; 30 | } else { 31 | type.type = 'array'; 32 | // JSON Schema supports only arrays of one type 33 | type.items = buildObjectType(item.type.elements[0]); 34 | } 35 | } 36 | 37 | function getAllUnions(elements) { 38 | return elements.reduce(function (all, item) { 39 | if (item.type === 'UnionType') { 40 | all = all.concat(getAllUnions(item.elements)); 41 | } else if (item.type === 'NameExpression') { 42 | all.push(item.name); 43 | } else { 44 | console.log('Unknown type' + item.type); 45 | } 46 | return all; 47 | }, []); 48 | } 49 | 50 | function collectSlicedTypes(item, definedTypes) { 51 | var isObj = item.type.name.toLowerCase() === 'object'; 52 | if (!isObj && !item.name.match(/\./)) return; 53 | var accessorArr = item.name.split('.'); 54 | accessorArr.reduce(function (obj, propName, i) { 55 | if (i === 0) { 56 | obj[propName] = obj[propName] || {title: propName, properties: {}}; 57 | obj = obj[propName].properties; 58 | } else { 59 | if (i !== accessorArr.length - 1 || isObj) { 60 | obj[propName] = {properties: {}}; 61 | obj = obj[propName].properties; 62 | } else { 63 | obj[propName] = {type: item.type.name}; 64 | } 65 | } 66 | return obj; 67 | }, definedTypes); 68 | 69 | return definedTypes; 70 | } 71 | 72 | module.exports = { 73 | buildObjectType, 74 | collectNestedTypes, 75 | getAllUnions, 76 | collectSlicedTypes 77 | }; -------------------------------------------------------------------------------- /lib/schemas/accessor.js: -------------------------------------------------------------------------------- 1 | var loader = require('./load'); 2 | var config = require('../config'); 3 | var _ = require('lodash'); 4 | 5 | /** 6 | * Returns schema for certain native js type 7 | * @param {String} type native js type 8 | * @returns {Object} schema 9 | */ 10 | var getNativeSchema = function (type) { 11 | var schemas = loader.loadPrimitive(); 12 | return schemas[type.toLowerCase()]; 13 | }; 14 | 15 | var getSchemaByRef = function (ref) { 16 | var parts = ref.split('/'); 17 | if (parts[0] !== '#' || parts[1] !== 'definitions' || parts.length !== 3) { 18 | throw new Error("Don't know how to resolve $ref: " + ref); 19 | } 20 | var schema = getSchemaByType(parts[2]); 21 | if (!schema) { 22 | throw new Error('Schema with name ' + parts[2] + "wasn't found (via $ref " + ref +")"); 23 | } 24 | return schema; 25 | } 26 | 27 | /** 28 | * Lazy reading schemas from disk 29 | * Retrieves schema for certain complex type 30 | * @param {String} type Can be nested, using dots in type name 31 | * @param {Object} customTypes that were build in JSDoc 32 | */ 33 | var getSchemaByType = function (type, customTypes) { 34 | var settings = config.settings; 35 | if (!settings) { 36 | throw new Error('No settings were set'); 37 | } 38 | var schemas = _.assign(customTypes || {}, loader.loadComposite(settings)); 39 | var props; 40 | var schemaObj; 41 | var i; 42 | if (type.match(/\./)) { 43 | props = type.split('.'); 44 | schemaObj = schemas[props[0]]; 45 | for (i = 1; i < props.length; i++) { 46 | if (!schemaObj || !schemaObj.properties || !schemaObj.properties[props[i]]) { 47 | throw new Error("Can't access to schema " + props[0] + ' with path ' + type + '. Possible types: ' + JSON.stringify(Object.keys(schemas))); 48 | } 49 | schemaObj = schemaObj.properties[props[i]]; 50 | } 51 | return schemaObj; 52 | } 53 | return schemas[type]; 54 | } 55 | 56 | /** 57 | * @param {Array} unions 58 | * @param {Object} customTypes 59 | * @returns {Object} 60 | */ 61 | function getUnionSchema(unions, customTypes) { 62 | var props = unions.reduce(function (unionSchema, type) { 63 | var typeSchema; 64 | typeSchema = getNativeSchema(type); 65 | if (!typeSchema) { 66 | typeSchema = getSchemaByType(type, customTypes); 67 | } 68 | if (typeSchema) { 69 | unionSchema = _.assign(unionSchema, typeSchema.properties); 70 | } 71 | return unionSchema; 72 | }, {}); 73 | 74 | if (Object.keys(props)) { 75 | return { 76 | properties: props 77 | } 78 | } 79 | } 80 | 81 | module.exports = { 82 | getSchemaByType: getSchemaByType, 83 | getNativeSchema: getNativeSchema, 84 | getUnionSchema: getUnionSchema, 85 | getSchemaByRef: getSchemaByRef 86 | } -------------------------------------------------------------------------------- /test/units/comments.js: -------------------------------------------------------------------------------- 1 | var grabComments = require('../../lib/comments'); 2 | var config = require('../../lib/config'); 3 | var assert = require('assert'); 4 | 5 | describe('Comments', () => { 6 | it.skip('grab', () => { 7 | var scope = { 8 | typedVars: [] 9 | }; 10 | 11 | var expected = { typedVars: 12 | [ { varName: 'arg1', varType: 'object', kind: 'primitive' }, 13 | { varName: 'arg1', varType: 'TLTyle1', kind: 'composite' }, 14 | { varName: 'arg2', varType: 'object', kind: 'primitive' }, 15 | { varName: 'arg2', varType: 'TLTyle2', kind: 'composite' }, 16 | { varName: 'arg3', varType: 'object', kind: 'primitive' } ] }; 17 | 18 | grabComments({ 19 | leadingComments: [ 20 | { 21 | value: `/** 22 | * @param {object} arg1 description1 23 | * @param {object} arg2 description2 24 | * @param {object} arg3 description3 25 | */` 26 | } 27 | ] 28 | }, scope); 29 | 30 | assert.deepEqual(scope, expected); 31 | }); 32 | 33 | it('unions', () => { 34 | var scope = { 35 | typedVars: [] 36 | }; 37 | 38 | grabComments({ 39 | leadingComments: [ 40 | { 41 | value: `/** 42 | * Test regular function with parameters 43 | * @param {(man|woman)|animal} man 44 | * @param {Boolean} flag 45 | * @returns {number} 46 | */` 47 | } 48 | ] 49 | }, scope); 50 | 51 | assert.deepEqual(scope.typedVars[0].unions, [ 'man', 'woman', 'animal' ]); 52 | }) 53 | 54 | it('objects', () => { 55 | var scope = { 56 | typedVars: [] 57 | }; 58 | 59 | grabComments({ 60 | leadingComments: [ 61 | { 62 | value: `/** 63 | * Test regular function with parameters 64 | * @param {object} human 65 | * @param {object} human.education 66 | * @param {number} human.age 67 | * @param {object} human.traits 68 | * @param {number} human.education.years 69 | */` 70 | } 71 | ] 72 | }, scope); 73 | //console.log(require('util').inspect(scope.typedVars[0].definedTypes, {depth: null})); 74 | assert.equal(scope.typedVars[0].definedTypes.human.properties.education.properties.years.type, 'number'); 75 | }) 76 | 77 | it('nested objects', () => { 78 | var scope = { 79 | typedVars: [] 80 | }; 81 | config.settings = { 82 | models: {json: {dir: 'models'}} 83 | }; 84 | grabComments({ 85 | leadingComments: [ 86 | { 87 | value: `/** 88 | * Test regular function with parameters 89 | * @param {{a: number, b: {c: string}}} varname 90 | */` 91 | } 92 | ] 93 | }, scope); 94 | //console.log(require('util').inspect(scope.typedVars[0], {depth: null})); 95 | assert.equal(scope.typedVars[0].varDefinedType.properties.b.properties.c, 'string'); 96 | }) 97 | }); -------------------------------------------------------------------------------- /lib/validation.js: -------------------------------------------------------------------------------- 1 | var allowedForArray = Object.getOwnPropertyNames(Array.prototype); 2 | var accessor = require('./schemas/accessor'); 3 | 4 | /** 5 | * Validates chain of props according to schema 6 | * @param {Array} props 7 | * @param {Object} schema JSONSchema 8 | * @param {Number} i index of prop 9 | * @returns {*} null if ok, prop name if not valid 10 | */ 11 | var validateAccess = function(props, schema, i) { 12 | var schemaProp; 13 | if (!props[i]) return null; 14 | schemaProp = props[i]; 15 | if (typeof schema !== 'object') { // got to the end point, use native types 16 | schema = accessor.getNativeSchema(schema); 17 | } 18 | 19 | // square brackets object access 20 | if (schemaProp.computed) { 21 | if (schema.title !== 'number') { 22 | return null; 23 | } else { 24 | return props.join('.'); 25 | } 26 | } 27 | // access to Array methods and properties 28 | if (!schema.properties && schema.items) { 29 | if (allowedForArray.indexOf(props[i]) === -1) { 30 | if (schema.items.properties && schema.items.properties[schemaProp]) { 31 | return null; 32 | } 33 | return props.join('.'); 34 | } 35 | return null; 36 | } 37 | if (schema.$ref) { 38 | schema = accessor.getSchemaByRef(schema.$ref); 39 | } 40 | if (schema.properties && schema.properties[schemaProp]) { 41 | return validateAccess(props, schema.properties[schemaProp], i + 1); 42 | } 43 | return props.join('.'); 44 | } 45 | 46 | /** 47 | * Sends eslint report in case of problems 48 | * @param param 49 | * @param scope 50 | * @param {Object} node 51 | * @param {Object} context 52 | */ 53 | var validate = function (param, scope, node, context) { 54 | var schema; 55 | if (param.unions) { 56 | param.varType = param.unions.join('|'); 57 | schema = accessor.getUnionSchema(param.unions, param.definedTypes); 58 | } else if (param.varDefinedType) { 59 | schema = param.varDefinedType; 60 | } else if (param.varType) { 61 | //if (['object'].indexOf(param.varType.toLowerCase()) !== -1) return; // don't check generic object types 62 | schema = accessor.getNativeSchema(param.varType); 63 | if (!schema) { 64 | schema = accessor.getSchemaByType(param.varType, param.definedTypes); 65 | } 66 | } else { 67 | throw new Error('Broken param type ' + JSON.stringify(param)); 68 | } 69 | 70 | if (!schema) { 71 | context.report(node, 'Unknown schema and object type ' + param.varType); 72 | return; 73 | } 74 | if (!schema.properties && !schema.$ref) { 75 | context.report(node, 'Type ' + param.varType + ' has no properties. Trying to access "' + scope.props.join('.') + '"'); 76 | return; 77 | } 78 | 79 | var inValid = validateAccess(scope.props, schema, 0); 80 | if (inValid !== null) { 81 | context.report(node, 'Invalid access to property "' + inValid + '" for type ' + param.varType); 82 | } 83 | } 84 | 85 | module.exports = { 86 | validate: validate 87 | }; -------------------------------------------------------------------------------- /lib/schemas/formats/json.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | var nodePath = require('path'); 3 | var jsyaml = require('js-yaml'); 4 | var fs = require('fs'); 5 | 6 | /** 7 | * Reads yaml or js/json files and return js object 8 | * @param {String} path 9 | * @returns {Object} 10 | */ 11 | function getContentByPath(path) { 12 | var parsedPath = nodePath.parse(path); 13 | var ext = parsedPath.ext; 14 | var str; 15 | if (ext === '.yaml' || ext === '.yml') { 16 | str = fs.readFileSync(path).toString(); 17 | return jsyaml.safeLoad(str); 18 | } else if (ext === '.json' || ext === '.js') { 19 | return require(path); 20 | } 21 | } 22 | 23 | /** 24 | * Handles certain file path and populates `collected` 25 | * Supports: yaml, json, js 26 | * @param {String} path 27 | * @param {Object} collected 28 | * @returns {Object} 29 | */ 30 | function setSchema(path, collected) { 31 | var parsedPath = nodePath.parse(path); 32 | var ext = parsedPath.ext; 33 | var modelName = parsedPath.name; 34 | if (!ext) return collected; 35 | collected[modelName] = getContentByPath(path); 36 | return collected; 37 | } 38 | 39 | function getAbsolutePath(path) { 40 | return nodePath.isAbsolute(path) ? path : process.cwd() + '/' + path; 41 | } 42 | 43 | function lookUp(path, exclude, collected, fileName) { 44 | var stat; 45 | path = getAbsolutePath(path); 46 | stat = fs.statSync(path); 47 | if (stat.isFile()) { 48 | return setSchema(path, collected); 49 | } else if (fileName) { 50 | if (exclude && exclude.indexOf(fileName) !== -1) { 51 | return collected; 52 | } 53 | } 54 | return fs.readdirSync(path).reduce(function (obj, fName) { 55 | obj = lookUp(path + '/' + fName, exclude, obj, fName); 56 | return obj; 57 | }, collected); 58 | } 59 | 60 | /** 61 | * Wrap schema with adapters 62 | * @param schema {Object} 63 | * @param adapters 64 | */ 65 | function applyAdapters(schema, adapters) { 66 | if (adapters) { 67 | adapters.forEach(function (adapter) { 68 | if (!nodePath.isAbsolute(adapter)) { 69 | adapter = process.cwd() + '/' + adapter; 70 | } 71 | schema = require(adapter)(schema); 72 | }); 73 | } 74 | return schema; 75 | } 76 | 77 | function readSwagger(path) { 78 | path = getAbsolutePath(path); 79 | var swaggerConfig = getContentByPath(path); 80 | if (!swaggerConfig.definitions) { 81 | throw new Error('Was not found "definitions" section in swagger config by path ' + path); 82 | } 83 | return swaggerConfig.definitions; 84 | } 85 | 86 | function index(settings) { 87 | if (settings.dir) { 88 | var schema = lookUp(settings.dir, settings.exclude, {}); 89 | return applyAdapters(schema, settings.adapters); 90 | } else if (settings.swagger) { 91 | return readSwagger(settings.swagger); 92 | } else { 93 | throw new Error ('In typelint.models.json section in eslint config should be either "dir" or "swagger" option'); 94 | } 95 | } 96 | 97 | module.exports = index; -------------------------------------------------------------------------------- /lib/schemas/generator/type.js: -------------------------------------------------------------------------------- 1 | (function (factory) { 2 | if (typeof exports == 'object') { 3 | module.exports = factory(); 4 | } else if ((typeof define == 'function') && define.amd) { 5 | define(factory); 6 | } 7 | }(function () { 8 | 9 | var isBuiltIn = (function () { 10 | var built_ins = [ 11 | Object, 12 | Function, 13 | Array, 14 | String, 15 | Boolean, 16 | Number, 17 | Date, 18 | RegExp, 19 | Error 20 | ]; 21 | var built_ins_length = built_ins.length; 22 | 23 | return function (_constructor) { 24 | for (var i = 0; i < built_ins_length; i++) { 25 | if (built_ins[i] === _constructor) { 26 | return true; 27 | } 28 | } 29 | return false; 30 | }; 31 | })(); 32 | 33 | var stringType = (function () { 34 | var _toString = ({}).toString; 35 | 36 | return function (obj) { 37 | // For now work around this bug in PhantomJS 38 | // https://github.com/ariya/phantomjs/issues/11722 39 | if (obj === null) { 40 | return 'null'; 41 | } else if (obj === undefined) { 42 | return 'undefined'; 43 | } 44 | 45 | // [object Blah] -> Blah 46 | var stype = _toString.call(obj).slice(8, -1); 47 | 48 | // Temporarily elided see commented on line 37 above 49 | // if ((obj === null) || (obj === undefined)) { 50 | // return stype.toLowerCase(); 51 | // } 52 | 53 | var ctype = of(obj); 54 | 55 | if (ctype && !isBuiltIn(ctype)) { 56 | return ctype.name; 57 | } else { 58 | return stype; 59 | } 60 | }; 61 | })(); 62 | 63 | function of (obj) { 64 | if ((obj === null) || (obj === undefined)) { 65 | return obj; 66 | } else { 67 | return obj.constructor; 68 | } 69 | } 70 | 71 | function is (obj, test) { 72 | var typer = (of(test) === String) ? stringType : of; 73 | return (typer(obj) === test); 74 | } 75 | 76 | function instance (obj, test) { 77 | return (obj instanceof test); 78 | } 79 | 80 | function extension (_Extension, _Base) { 81 | return instance(_Extension.prototype, _Base); 82 | } 83 | 84 | function any (obj, tests) { 85 | if (!is(tests, Array)) { 86 | throw ("Second argument to .any() should be array") 87 | } 88 | for (var i = 0; i < tests.length; i++) { 89 | var test = tests[i]; 90 | if (is(obj, test)) { 91 | return true; 92 | } 93 | } 94 | return false; 95 | } 96 | 97 | var exports = function (obj, type) { 98 | if (arguments.length == 1) { 99 | return of(obj); 100 | } else { 101 | if (is(type, Array)) { 102 | return any(obj, type); 103 | } else { 104 | return is(obj, type); 105 | } 106 | } 107 | } 108 | 109 | exports.instance = instance; 110 | exports.string = stringType; 111 | exports.of = of; 112 | exports.is = is; 113 | exports.any = any; 114 | exports.extension = extension; 115 | return exports; 116 | 117 | })); -------------------------------------------------------------------------------- /lib/passing.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | var assignable = ['VariableDeclaration']; 4 | 5 | /** 6 | * @param {String} varName 7 | * @param {Object} scope 8 | * @returns {String} found type 9 | */ 10 | function isVarInScope(varName, scope) { 11 | var found = null; 12 | scope.typedVars.some(function (item) { 13 | var eq = item.varName === varName; 14 | if (eq) { 15 | found = item.varType; 16 | } 17 | return eq; 18 | }); 19 | return found; 20 | } 21 | 22 | /** 23 | * @param {String} varName 24 | * @param {Object} scope 25 | * @returns {Number} index 26 | */ 27 | function indexOfVarInScope(varName, scope) { 28 | var found = -1; 29 | scope.typedVars.some(function (item, i) { 30 | var eq = item.varName === varName; 31 | if (eq) { 32 | found = i; 33 | } 34 | return eq; 35 | }); 36 | return found; 37 | } 38 | 39 | /** 40 | * Recursively get all members 41 | * @param {Object} node 42 | * @param {Array} props 43 | * @returns {Array} props 44 | */ 45 | function traverseAllMembers(node, props) { 46 | if (node.type === 'MemberExpression' && node.property) { 47 | props.push(node.property.name); 48 | } 49 | if (node.parent) { 50 | return traverseAllMembers(node.parent, props); 51 | } 52 | return props; 53 | } 54 | 55 | /** 56 | * Handle assignments of typed vars 57 | * Goes by each node in function scope 58 | * @param node 59 | * @param scope 60 | * @returns {scope|*} 61 | */ 62 | function searchForAssignments(node, scope) { 63 | // We are at the later point, than initial statement, can't affect, skip 64 | if (node.start > scope.init.start) { 65 | return scope; 66 | } 67 | scope = grabComments(node, scope); 68 | scope = checkReAssignments(node, scope); 69 | if (assignable.indexOf(node.type) !== -1) { 70 | node.declarations.forEach(function (declaration) { 71 | var fromVar; 72 | var newVarType; 73 | var newVar; 74 | var fromVarProps; 75 | if (!declaration.init || !declaration.id) return; 76 | newVar = declaration.id.name; 77 | // var newVar = typedVar; 78 | if (declaration.init.type === 'Identifier') { 79 | fromVar = declaration.init.name; 80 | newVarType = isVarInScope(fromVar, scope) 81 | } 82 | // var newVar = typedVar.prop1.prop2; 83 | if (declaration.init.type === 'MemberExpression') { 84 | fromVar = declaration.init.object.name; 85 | newVarType = isVarInScope(fromVar, scope); 86 | if (newVarType) { 87 | fromVarProps = traverseAllMembers(declaration.init, []); 88 | newVarType = newVarType + '.' + fromVarProps.join('.'); 89 | } 90 | } 91 | if (newVar && newVarType) { 92 | scope.typedVars.push({ 93 | varName: newVar, 94 | varType: newVarType 95 | }); 96 | } 97 | }); 98 | return scope; 99 | } 100 | if (Array.isArray(node.body)) { 101 | scope = node.body.reduce(function (prevScope, tail) { 102 | return searchForAssignments(tail, prevScope); 103 | }, scope); 104 | } 105 | return scope; 106 | } 107 | 108 | function checkReAssignments(node, scope) { 109 | var index; 110 | if (node.type === 'ExpressionStatement' && node.expression && node.expression.type === 'AssignmentExpression') { 111 | index = indexOfVarInScope(node.expression.left.name, scope); 112 | if (index !== -1) { 113 | scope.typedVars.splice(index, 1); 114 | } 115 | } 116 | return scope; 117 | } -------------------------------------------------------------------------------- /test/code.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | var assert = require('assert'); 3 | 4 | const getCmd = (file, config) => { 5 | if (!config) config = '.eslintrc.yml'; 6 | const cmd = `node ${__dirname}/../node_modules/.bin/eslint -c ${__dirname}/fixtures/${config} ${__dirname}/fixtures/code/${file}.js`; 7 | console.log(cmd); 8 | return cmd; 9 | } 10 | 11 | describe('typelint', function () { 12 | 13 | it('Run with default config without settings', (done) => { 14 | exec(getCmd('empty', '.simple.yml'), (err, stdout, stderr) => { 15 | if (err || stderr) return done(new Error(stdout)); 16 | done(); 17 | }); 18 | }); 19 | 20 | it('Regular function comments/variable', (done) => { 21 | exec(getCmd('test'), (err, stdout, stderr) => { 22 | if (!err) { 23 | throw new Error('Should throw error'); 24 | } 25 | assert(!!stdout.match(/first_Name/), true); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('JSDoc typedef', (done) => { 31 | exec(getCmd('test'), (err, stdout, stderr) => { 32 | if (!err) { 33 | throw new Error('Should throw error'); 34 | } 35 | assert(!!stdout.match(/first_Name/), true); 36 | done(); 37 | }); 38 | }); 39 | 40 | it.skip('Code in the root scope', (done) => { 41 | exec(getCmd('root_scope'), (err, stdout, stderr) => { 42 | if (!err) { 43 | throw new Error('Should throw error, because wrong member access'); 44 | } 45 | done(); 46 | }); 47 | }); 48 | 49 | it.skip('Transfer variables, should throw', (done) => { 50 | exec(getCmd('pass_vars'), (err, stdout, stderr) => { 51 | if (!err) { 52 | throw new Error('Should throw error, because wrong member access'); 53 | } 54 | done(); 55 | }); 56 | }); 57 | 58 | it('Const definition before arrow function', (done) => { 59 | exec(getCmd('es6'), (err, stdout, stderr) => { 60 | if (err || stderr) return done(new Error(stdout)); 61 | done(); 62 | }); 63 | }); 64 | 65 | it('Const definition before arrow function', (done) => { 66 | exec(getCmd('arrays'), (err, stdout, stderr) => { 67 | if (err || stderr) return done(new Error(stdout)); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('Const definition before arrow function', (done) => { 73 | exec(getCmd('arrays_access'), (err, stdout, stderr) => { 74 | if (err || stderr) return done(new Error(stdout)); 75 | done(); 76 | }); 77 | }); 78 | 79 | it('JSDoc types', (done) => { 80 | exec(getCmd('jsdoc_types'), (err, stdout, stderr) => { 81 | if (!err) { 82 | throw new Error('Should throw error'); 83 | } 84 | done(); 85 | }); 86 | }); 87 | 88 | it.skip('Var reassign', (done) => { 89 | exec(getCmd('reassign'), (err, stdout, stderr) => { 90 | if (!err) { 91 | throw new Error('Should throw error'); 92 | } 93 | assert(!!stdout.match(/Invalid access to property wrong for type human/), true); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('Unions', (done) => { 99 | exec(getCmd('unions'), (err, stdout, stderr) => { 100 | if (!err) { 101 | throw new Error('Should throw error'); 102 | } 103 | assert(!!stdout.match(/Invalid access to property wrong for type human|String/), true); 104 | done(); 105 | }); 106 | }); 107 | 108 | it('Object types', (done) => { 109 | exec(getCmd('objects'), (err, stdout, stderr) => { 110 | if (!err) { 111 | throw new Error('Should throw error'); 112 | } 113 | assert(!!stdout.match(/Invalid access to property "education.wrong" for type man/), true); 114 | done(); 115 | }); 116 | }); 117 | 118 | }); -------------------------------------------------------------------------------- /lib/schemas/generator/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module from https://github.com/nijikokun/generate-schema 3 | * @type {*|exports|module.exports} 4 | */ 5 | var Type = require('./type') 6 | var DRAFT = "http://json-schema.org/draft-04/schema#" 7 | 8 | function getUniqueKeys (a, b, c) { 9 | var a = Object.keys(a) 10 | var b = Object.keys(b) 11 | var c = c || [] 12 | var value 13 | var cIndex 14 | var aIndex 15 | 16 | for (var keyIndex = 0, keyLength = b.length; keyIndex < keyLength; keyIndex++) { 17 | value = b[keyIndex] 18 | aIndex = a.indexOf(value) 19 | cIndex = c.indexOf(value) 20 | 21 | if (aIndex === -1) { 22 | if (cIndex !== -1) { 23 | // Value is optional, it doesn't exist in A but exists in B(n) 24 | c.splice(cIndex, 1) 25 | } 26 | } else if (cIndex === -1) { 27 | // Value is required, it exists in both B and A, and is not yet present in C 28 | c.push(value) 29 | } 30 | } 31 | 32 | return c 33 | } 34 | 35 | function processArray (array, output, nested) { 36 | var oneOf 37 | var type 38 | 39 | if (nested && output) { 40 | output = { 41 | items: output 42 | } 43 | } else { 44 | output = output || {} 45 | output.type = Type.string(array).toLowerCase() 46 | output.items = output.items || {} 47 | } 48 | 49 | // Determine whether each item is different 50 | for (var index = 0, length = array.length; index < length; index++) { 51 | var elementType = Type.string(array[index]).toLowerCase() 52 | 53 | if (type && elementType !== type) { 54 | output.items.oneOf = [] 55 | oneOf = true 56 | break 57 | } else { 58 | type = elementType 59 | } 60 | } 61 | 62 | // Setup type otherwise 63 | if (!oneOf) { 64 | output.items.type = type 65 | } 66 | 67 | // Process each item depending 68 | if (typeof output.items.oneOf !== 'undefined' || type === 'object') { 69 | for (var index = 0, length = array.length; index < length; index++) { 70 | var value = array[index] 71 | var itemType = Type.string(value).toLowerCase() 72 | var required = [] 73 | var processOutput 74 | 75 | switch (itemType) { 76 | case "object": 77 | if (output.items.properties) { 78 | output.items.required = getUniqueKeys(output.items.properties, value, output.items.required) 79 | } 80 | 81 | processOutput = processObject(value, oneOf ? {} : output.items.properties, true) 82 | break 83 | 84 | case "array": 85 | processOutput = processArray(value, oneOf ? {} : output.items.properties, true) 86 | break 87 | 88 | default: 89 | processOutput = { type: itemType } 90 | } 91 | 92 | if (oneOf) { 93 | output.items.oneOf.push(processOutput) 94 | } else { 95 | output.items.properties = processOutput 96 | } 97 | } 98 | } 99 | 100 | return nested ? output.items : output 101 | } 102 | 103 | function processObject (object, output, nested) { 104 | if (nested && output) { 105 | output = { 106 | properties: output 107 | } 108 | } else { 109 | output = output || {} 110 | output.type = Type.string(object).toLowerCase() 111 | output.properties = output.properties || {} 112 | } 113 | 114 | for (var key in object) { 115 | var value = object[key] 116 | var type = Type.string(value).toLowerCase() 117 | 118 | if (type === 'undefined') { 119 | type = 'null' 120 | } 121 | 122 | switch (type) { 123 | case "object": 124 | output.properties[key] = processObject(value) 125 | break 126 | 127 | case "array": 128 | output.properties[key] = processArray(value) 129 | break 130 | 131 | default: 132 | output.properties[key] = { 133 | type: type 134 | } 135 | } 136 | } 137 | 138 | return nested ? output.properties : output 139 | } 140 | 141 | module.exports = function (title, object) { 142 | var processOutput 143 | var output = { 144 | $schema: DRAFT 145 | } 146 | 147 | // Determine title exists 148 | if (typeof title !== 'string') { 149 | object = title 150 | title = undefined 151 | } else { 152 | output.title = title 153 | } 154 | 155 | // Set initial object type 156 | output.type = Type.string(object).toLowerCase() 157 | 158 | // Process object 159 | switch (output.type) { 160 | case "object": 161 | processOutput = processObject(object) 162 | output.type = processOutput.type 163 | output.properties = processOutput.properties 164 | break 165 | 166 | case "array": 167 | processOutput = processArray(object) 168 | output.type = processOutput.type 169 | output.items = processOutput.items 170 | 171 | if (output.title) { 172 | output.items.title = output.title 173 | output.title += " Set" 174 | } 175 | 176 | break 177 | } 178 | 179 | // Output 180 | return output 181 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeLint 2 | 3 | TypeLint is an [ESlint](http://eslint.org) plugin for static type checking in JavaScript, based on [JSDoc](http://usejsdoc.org/). 4 | 5 | Every application manages data, which is usually described in some way. 6 | 7 | For example: 8 | 9 | * Swagger definitions 10 | * Redux store (folded reducers) 11 | * JSON based database schemas (MongoDB, RethinkDB, CouchDB etc) 12 | * GraphQL schemas 13 | 14 | Mission of TypeLint is to provide complex type checking, based on **already described data structures** of your app. 15 | 16 | # Demo 17 | 18 | 19 | 20 | See also [Typelint example project](https://github.com/yarax/typelint-example) 21 | 22 | # Installation 23 | 24 | ``` 25 | npm i eslint eslint-plugin-typelint 26 | ``` 27 | 28 | `If you use eslint globally, you should install typelint globally as well.` 29 | 30 | Put typelint to eslint plugins array and add rule `typelint/typelint` 31 | 32 | # Supported types declarations 33 | 34 | All types are compatible with JSDoc and Closure Compiler. See [http://eslint.org/doctrine/demo/](http://eslint.org/doctrine/demo/) for examples 35 | 36 | * `@param {TypeName} varName` Where TypeName is previously defined type. See type definition section below. 37 | * `@param {[TypeName]} varName` Array of type TypeName 38 | * `@param {{a: String, b: {c: [Number]}}}` Nested record types 39 | * `@param {TypeName1|TypeName2}` Union types 40 | 41 | # Type definitions 42 | 43 | ## Redux state 44 | 45 | Redux is a popular store for React apps. 46 | 47 | It describes the state shape as a composition of reducers. 48 | 49 | Dealing with a big and deeply nested store it's easy to make mistakes with access to a wrong property. 50 | 51 | Typelint constructs the Redux schema, based on initial values of reducers, detecting types of end values. 52 | 53 | To use it, just add the redux option to settings.typelint.models section of .eslintrc with path to your root reducer: 54 | 55 | ``` 56 | "settings": { 57 | "typelint": { 58 | "models": { 59 | "redux": { 60 | "reducerPath": "./src/client/redux/reducer.js" 61 | } 62 | }, 63 | "useCache": true 64 | } 65 | } 66 | ``` 67 | 68 | After that you can use a new typelint type in JSDoc comments. 69 | 70 | ## API/Swagger 71 | 72 | TypeLint supports JSON Schema for describing data interfaces. 73 | 74 | [JSON Schema](http://json-schema.org/) is advanced, popular and well documented format for describing JSON data. 75 | 76 | For example if you use [Swagger](http://swagger.io/) for API, you already have JSON Schema definitions, that you can use. 77 | 78 | To bind your models to TypeLint, put the json option to settings.typelint.models section of your .eslintrc: 79 | 80 | ```js 81 | "settings": { 82 | "typelint": { 83 | "models": { 84 | "json": { 85 | "dir": "./models", 86 | "exclude": ["wrong_dir"] 87 | }, 88 | } 89 | } 90 | } 91 | ``` 92 | 93 | When mentioned "dir" option, models are expected as files, where each of them contains one definition. 94 | Name of the file is a name of the model. 95 | When "swagger" option is used instead of "dir", the particular path is expected as a Swagger schema with "definitions" section. 96 | See [Swagger docs](http://swagger.io/specification/#definitionsObject) for more information. 97 | 98 | Supported formats: 99 | * JSON 100 | * YAML 101 | * JS files as common.js modules (export object is a schema) 102 | 103 | # Additional features 104 | 105 | ### Adapters 106 | 107 | Adapters can be used for transforming schemas from one format to another. For example: 108 | 109 | ``` 110 | "json": { 111 | "dir": "./models", 112 | "exclude": ["wrong_dir"], 113 | "adapters": ["./node_modules/eslint-plugin-typelint/adapters/to-camel-case"] 114 | }, 115 | ``` 116 | There are two already existed adapters: to-camel-case and to-snake-case. They appropriately convert fields of schema. 117 | You can write your own adapters, using the same interface. 118 | 119 | ### Autocomplete 120 | 121 | If you use WebStorm/PHPStorm 2016, all TypeLint types are available for autocomplete. Just run in your working directory: 122 | ``` 123 | cp node_modules/eslint-plugin-typelint/jsonSchemas.xml .idea/ 124 | ``` 125 | 126 | # What it is and what it is not 127 | 128 | TypeLint is a helper, but not a full-fledged typed system for js. 129 | 130 | If you want to make your code 100% typed, please use any of existing static typed languages, which can be transpiled to JavaScript (TypeScript, Flow etc) 131 | 132 | The purpose of TypeLint is to help developer avoid `undefined` errors, but optionally and staying all the speed and flexibility of pure JavaScript developement. 133 | 134 | `BTW` TypeLint was written with help of TypeLint 😊️ 135 | 136 | # All .eslinrc typelint settings options 137 | 138 | * models.json.dir - {String} path to your models dir. Every file is a separate model 139 | * models.json.swagger - {String} path to your swagger schema file 140 | * models.json.exclude - {Array} array of paths, that models finder should ignore while loading models 141 | * models.json.adapters - {Array} array of paths to modules, which exports functions :: yourSchema -> JSONSchema. 142 | * models.redux.reducerPath - {String} Path to the root Redux reducer 143 | * useCache - {Boolean} caches all models (will work faster, but changes in models will not affect). `Default`: false 144 | 145 | # Planned features 146 | 147 | * Handle passing typed variables throw 148 | * Detect when variable was reassigned 149 | * Improve caching and performance 150 | * Support GraphQL schemas 151 | * Adapters for Mongoose and Thinky 152 | * Other types of models bindings 153 | 154 | [Changelog](https://github.com/yarax/typelint/blob/master/CHANGELOG.md) 155 | 156 | [Article "Optional typing in JavaScript without transpilers"](https://medium.com/@raxwunter/optional-typing-in-javascript-without-transpilers-1f819d622a3a) 157 | 158 | [Type features comparison: Haskell, TypeScript, Flow, JSDoc, JSON Schema](https://github.com/yarax/typescript-flow-haskell-types-comparison) 159 | 160 | # License 161 | 162 | MIT. 163 | 164 | [![Build Status](https://travis-ci.org/yarax/eslint-plugin-typelint.svg?branch=master)](https://travis-ci.org/yarax/eslint-plugin-typelint) 165 | --------------------------------------------------------------------------------