UNPKG

zelda-ast

Version:

zelda static analysis based on javascript ast.

298 lines (265 loc) 7.96 kB
import j from 'jscodeshift'; import once from 'lodash.once'; import assert from 'assert'; import Helper from './Helper'; import * as utils from '../utils/index'; Helper.register(); const _check = (node, name) => { return (node.type === 'Identifier' && node.name === name) || (node.type === 'Literal' && node.value === name); }; const methods = { findModels(namespace) { return this.find(j.ObjectExpression, node => { const props = node.properties.reduce((memo, prop) => { if (j.Property.check(prop)) { if (prop.key.name === 'namespace') { if (!j.Literal.check(prop.value)) { return {}; } memo[prop.key.name] = prop.value.value; } else { memo[prop.key.name] = true; } } return memo; }, {}); if (!props.namespace) return false; if (namespace && props.namespace !== namespace) return false; // state 不是必须的 // 但为增加准确性, 还需要声明除 namespace 以外的其他任一项 return props.state || props.reducers || props.effects || props.subscriptions; }); }, updateNamespace(newNamespace) { return this.forEach(path => { path.node.properties.forEach(prop => { if (j.Property.check(prop) && prop.key.name === 'namespace') { prop.value = j.literal(newNamespace); } }); }); }, updateState(source) { return this.forEach(path => { path.node.properties.forEach(prop => { if (j.Property.check(prop) && prop.key.name === 'state') { prop.value = utils.getExpression(source); } }); }); }, addState(name, source) { this._addModelItem(name, source, { itemsKey: 'state', defaultSource: '{}' }); }, addReducer(name, source) { this._addModelItem(name, source, { itemsKey: 'reducers', defaultSource: 'function(state) {\n return state;\n}', }); }, addEffect(name, source) { this._addModelItem(name, source, { itemsKey: 'effects', defaultSource: '*function(action, { call, put, select }) {\n}', }); }, addSubscription(name, source) { this._addModelItem(name, source, { itemsKey: 'subscriptions', defaultSource: 'function({ dispatch, history }) {\n}', }); }, /** * @private */ _addModelItem(name, source, { itemsKey, defaultSource }) { return this.forEach(path => { let items = null; path.node.properties.forEach(prop => { if (j.Property.check(prop) && prop.key.name === itemsKey) { assert( j.ObjectExpression.check(prop.value), `_addModelItem: ${itemsKey} should be ObjectExpression, but got ${prop.value.type}` ); items = prop; } }); if (!items) { items = j.property( 'init', j.identifier(itemsKey), j.objectExpression([]) ); path.node.properties.push(items); } const item = j.property( 'init', j.identifier(name), utils.getExpression(source || defaultSource) ); items.value.properties.push(item); }); }, updateReducer(name, source) { this._updateModelItem(name, source, { itemsKey: 'reducers', }); }, updateEffect(name, source) { this._updateModelItem(name, source, { itemsKey: 'effects', }); }, updateSubscription(name, source) { this._updateModelItem(name, source, { itemsKey: 'subscriptions', }); }, /** * @private */ _updateModelItem(name, source, { itemsKey }) { return this.forEach(path => { let items = null; path.node.properties.forEach(prop => { if (j.Property.check(prop) && prop.key.name === itemsKey) { assert( j.ObjectExpression.check(prop.value), `_updateModelItem: ${itemsKey} should be ObjectExpression, but got ${prop.value.type}` ); items = prop; } }); assert(items, `_updateModelItem: ${itemsKey} not found`); let updated = false; items.value.properties.forEach(prop => { if (j.Property.check(prop) && _check(prop.key, name)) { updated = true; prop.value = utils.getExpression(source); } }); assert(updated, `_updateModelItem: ${itemsKey}.${name} not found`); }); }, removeReducer(name) { this._removeModelItem(name, { itemsKey: 'reducers', }); }, removeEffect(name) { this._removeModelItem(name, { itemsKey: 'effects', }); }, removeSubscription(name) { this._removeModelItem(name, { itemsKey: 'subscriptions', }); }, /** * @private */ _removeModelItem(name, { itemsKey }) { return this.forEach(path => { let items = null; path.node.properties.forEach(prop => { if (j.Property.check(prop) && prop.key.name === itemsKey) { assert( j.ObjectExpression.check(prop.value), `_removeModelItem: ${itemsKey} should be ObjectExpression, but got ${prop.value.type}` ); items = prop; } }); assert(items, `_removeModelItem: ${itemsKey} not found`); let removed = false; items.value.properties = items.value.properties.filter(prop => { if (j.Property.check(prop) && _check(prop.key, name)) { removed = true; return false; } return true; }); assert(removed, `_removeModelItem: ${itemsKey}.${name} not found`); }); }, getModelInfo() { const defaultModel = { reducers: [], effects: [], subscriptions: [], }; return this.simpleMap(path => { const node = path.node; const result = node.properties.reduce((memo, prop) => { const name = prop.key.name; switch (name) { case 'namespace': memo[name] = prop.value.value; return memo; case 'state': memo[name] = utils.recursiveParse(prop.value); return memo; case 'reducers': memo[name] = parseReducers(prop.value); return memo; case 'effects': memo[name] = parseEffects(prop.value); return memo; case 'subscriptions': memo[name] = parseSubscriptions(prop.value); return memo; default: throw new Error(`getModelProperties: unrecognized property of dva model: ${name}`); } }, defaultModel); // TODO: reducers 等的解析支持 VariableDeclaraction // TODO: reducers 等的解析支持通过 import 从外部引入 // TODO: add id for reducers, effects and subscriptions return result; }); function parseBasic(node, parseType, extra) { assert( node.type === 'ObjectExpression', `getModelProperties: ${parseType} should be ObjectExpression, but got ${node.type}` ); return node.properties.map(prop => { const name = utils.getPropertyValue(prop.key); const result = { name, source: j(prop.value).toSource(), }; if (extra) { extra(result, prop.value); } return result; }); } function parseReducers(node) { return parseBasic(node, 'reducers'); } function parseEffects(node) { return parseBasic(node, 'effects', (result, node) => { result.dispatches = j(node).findDispatchCalls().getActionTypeFromCall(); }); } function parseSubscriptions(node) { return parseBasic(node, 'subscriptions', (result, node) => { result.dispatches = j(node).findDispatchCalls().getActionTypeFromCall(); }); } }, }; function register(jscodeshift = j) { jscodeshift.registerMethods(methods); } export default { register: once(register), };