This app provides monitoring and information features for the common freifunk user and the technical stuff of a freifunk community.
Code base is taken from a TUM Practical Course project and added here to see if Freifunk Altdorf can use it.
https://www.freifunk-altdorf.de
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
410 lines
11 KiB
410 lines
11 KiB
/** |
|
* @fileoverview Utility class and functions for React components detection |
|
* @author Yannick Croissant |
|
*/ |
|
|
|
'use strict'; |
|
|
|
/** |
|
* Components |
|
* @class |
|
*/ |
|
function Components() { |
|
this.list = {}; |
|
this.getId = function (node) { |
|
return node && node.range.join(':'); |
|
}; |
|
} |
|
|
|
/** |
|
* Add a node to the components list, or update it if it's already in the list |
|
* |
|
* @param {ASTNode} node The AST node being added. |
|
* @param {Number} confidence Confidence in the component detection (0=banned, 1=maybe, 2=yes) |
|
*/ |
|
Components.prototype.add = function (node, confidence) { |
|
const id = this.getId(node); |
|
if (this.list[id]) { |
|
if (confidence === 0 || this.list[id].confidence === 0) { |
|
this.list[id].confidence = 0; |
|
} else { |
|
this.list[id].confidence = Math.max(this.list[id].confidence, confidence); |
|
} |
|
return; |
|
} |
|
this.list[id] = { |
|
node: node, |
|
confidence: confidence, |
|
}; |
|
}; |
|
|
|
/** |
|
* Find a component in the list using its node |
|
* |
|
* @param {ASTNode} node The AST node being searched. |
|
* @returns {Object} Component object, undefined if the component is not found |
|
*/ |
|
Components.prototype.get = function (node) { |
|
const id = this.getId(node); |
|
return this.list[id]; |
|
}; |
|
|
|
/** |
|
* Update a component in the list |
|
* |
|
* @param {ASTNode} node The AST node being updated. |
|
* @param {Object} props Additional properties to add to the component. |
|
*/ |
|
Components.prototype.set = function (node, props) { |
|
let currentNode = node; |
|
while (currentNode && !this.list[this.getId(currentNode)]) { |
|
currentNode = node.parent; |
|
} |
|
if (!currentNode) { |
|
return; |
|
} |
|
const id = this.getId(currentNode); |
|
this.list[id] = Object.assign({}, this.list[id], props); |
|
}; |
|
|
|
/** |
|
* Return the components list |
|
* Components for which we are not confident are not returned |
|
* |
|
* @returns {Object} Components list |
|
*/ |
|
Components.prototype.all = function () { |
|
const list = {}; |
|
Object.keys(this.list).forEach((i) => { |
|
if ({}.hasOwnProperty.call(this.list, i) && this.list[i].confidence >= 2) { |
|
list[i] = this.list[i]; |
|
} |
|
}); |
|
return list; |
|
}; |
|
|
|
/** |
|
* Return the length of the components list |
|
* Components for which we are not confident are not counted |
|
* |
|
* @returns {Number} Components list length |
|
*/ |
|
Components.prototype.length = function () { |
|
let length = 0; |
|
Object.keys(this.list).forEach((i) => { |
|
if ({}.hasOwnProperty.call(this.list, i) && this.list[i].confidence >= 2) { |
|
length += 1; |
|
} |
|
}); |
|
return length; |
|
}; |
|
|
|
function componentRule(rule, context) { |
|
const sourceCode = context.getSourceCode(); |
|
const components = new Components(); |
|
|
|
// Utilities for component detection |
|
const utils = { |
|
|
|
/** |
|
* Check if the node is a React ES5 component |
|
* |
|
* @param {ASTNode} node The AST node being checked. |
|
* @returns {Boolean} True if the node is a React ES5 component, false if not |
|
*/ |
|
isES5Component: function (node) { |
|
if (!node.parent) { |
|
return false; |
|
} |
|
return /^(React\.)?createClass$/.test(sourceCode.getText(node.parent.callee)); |
|
}, |
|
|
|
/** |
|
* Check if the node is a React ES6 component |
|
* |
|
* @param {ASTNode} node The AST node being checked. |
|
* @returns {Boolean} True if the node is a React ES6 component, false if not |
|
*/ |
|
isES6Component: function (node) { |
|
if (!node.superClass) { |
|
return false; |
|
} |
|
return /^(React\.)?(Pure)?Component$/.test(sourceCode.getText(node.superClass)); |
|
}, |
|
|
|
/** |
|
* Check if the node is returning JSX |
|
* |
|
* @param {ASTNode} node The AST node being checked (must be a ReturnStatement). |
|
* @returns {Boolean} True if the node is returning JSX, false if not |
|
*/ |
|
isReturningJSX: function (node) { |
|
let property; |
|
switch (node.type) { |
|
case 'ReturnStatement': |
|
property = 'argument'; |
|
break; |
|
case 'ArrowFunctionExpression': |
|
property = 'body'; |
|
break; |
|
default: |
|
return false; |
|
} |
|
|
|
const returnsJSX = |
|
node[property] && |
|
node[property].type === 'JSXElement' |
|
; |
|
const returnsReactCreateElement = |
|
node[property] && |
|
node[property].callee && |
|
node[property].callee.property && |
|
node[property].callee.property.name === 'createElement' |
|
; |
|
|
|
return Boolean(returnsJSX || returnsReactCreateElement); |
|
}, |
|
|
|
/** |
|
* Get the parent component node from the current scope |
|
* |
|
* @returns {ASTNode} component node, null if we are not in a component |
|
*/ |
|
getParentComponent: function () { |
|
return ( |
|
utils.getParentES6Component() || |
|
utils.getParentES5Component() || |
|
utils.getParentStatelessComponent() |
|
); |
|
}, |
|
|
|
/** |
|
* Get the parent ES5 component node from the current scope |
|
* |
|
* @returns {ASTNode} component node, null if we are not in a component |
|
*/ |
|
getParentES5Component: function () { |
|
let scope = context.getScope(); |
|
while (scope) { |
|
const node = scope.block && scope.block.parent && scope.block.parent.parent; |
|
if (node && utils.isES5Component(node)) { |
|
return node; |
|
} |
|
scope = scope.upper; |
|
} |
|
return null; |
|
}, |
|
|
|
/** |
|
* Get the parent ES6 component node from the current scope |
|
* |
|
* @returns {ASTNode} component node, null if we are not in a component |
|
*/ |
|
getParentES6Component: function () { |
|
let scope = context.getScope(); |
|
while (scope && scope.type !== 'class') { |
|
scope = scope.upper; |
|
} |
|
const node = scope && scope.block; |
|
if (!node || !utils.isES6Component(node)) { |
|
return null; |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* Get the parent stateless component node from the current scope |
|
* |
|
* @returns {ASTNode} component node, null if we are not in a component |
|
*/ |
|
getParentStatelessComponent: function () { |
|
let scope = context.getScope(); |
|
while (scope) { |
|
const node = scope.block; |
|
// Ignore non functions |
|
const isFunction = /Function/.test(node.type); |
|
// Ignore classes methods |
|
const isNotMethod = !node.parent || node.parent.type !== 'MethodDefinition'; |
|
// Ignore arguments (callback, etc.) |
|
const isNotArgument = !node.parent || node.parent.type !== 'CallExpression'; |
|
if (isFunction && isNotMethod && isNotArgument) { |
|
return node; |
|
} |
|
scope = scope.upper; |
|
} |
|
return null; |
|
}, |
|
|
|
/** |
|
* Get the related component from a node |
|
* |
|
* @param {ASTNode} node The AST node being checked (must be a MemberExpression). |
|
* @returns {ASTNode} component node, null if we cannot find the component |
|
*/ |
|
getRelatedComponent: function (node) { |
|
let currentNode = node; |
|
let i; |
|
let j; |
|
let k; |
|
let l; |
|
// Get the component path |
|
const componentPath = []; |
|
while (currentNode) { |
|
if (currentNode.property && currentNode.property.type === 'Identifier') { |
|
componentPath.push(currentNode.property.name); |
|
} |
|
if (currentNode.object && currentNode.object.type === 'Identifier') { |
|
componentPath.push(currentNode.object.name); |
|
} |
|
currentNode = currentNode.object; |
|
} |
|
componentPath.reverse(); |
|
|
|
// Find the variable in the current scope |
|
const variableName = componentPath.shift(); |
|
if (!variableName) { |
|
return null; |
|
} |
|
let variableInScope; |
|
const variables = context.getScope().variables; |
|
for (i = 0, j = variables.length; i < j; i++) { // eslint-disable-line no-plusplus |
|
if (variables[i].name === variableName) { |
|
variableInScope = variables[i]; |
|
break; |
|
} |
|
} |
|
if (!variableInScope) { |
|
return null; |
|
} |
|
|
|
// Find the variable declaration |
|
let defInScope; |
|
const defs = variableInScope.defs; |
|
for (i = 0, j = defs.length; i < j; i++) { // eslint-disable-line no-plusplus |
|
if ( |
|
defs[i].type === 'ClassName' || |
|
defs[i].type === 'FunctionName' || |
|
defs[i].type === 'Variable' |
|
) { |
|
defInScope = defs[i]; |
|
break; |
|
} |
|
} |
|
if (!defInScope) { |
|
return null; |
|
} |
|
currentNode = defInScope.node.init || defInScope.node; |
|
|
|
// Traverse the node properties to the component declaration |
|
for (i = 0, j = componentPath.length; i < j; i++) { // eslint-disable-line no-plusplus |
|
if (!currentNode.properties) { |
|
continue; // eslint-disable-line no-continue |
|
} |
|
for (k = 0, l = currentNode.properties.length; k < l; k++) { // eslint-disable-line no-plusplus, max-len |
|
if (currentNode.properties[k].key.name === componentPath[i]) { |
|
currentNode = currentNode.properties[k]; |
|
break; |
|
} |
|
} |
|
if (!currentNode) { |
|
return null; |
|
} |
|
currentNode = currentNode.value; |
|
} |
|
|
|
// Return the component |
|
return components.get(currentNode); |
|
}, |
|
}; |
|
|
|
// Component detection instructions |
|
const detectionInstructions = { |
|
ClassDeclaration: function (node) { |
|
if (!utils.isES6Component(node)) { |
|
return; |
|
} |
|
components.add(node, 2); |
|
}, |
|
|
|
ClassProperty: function () { |
|
const node = utils.getParentComponent(); |
|
if (!node) { |
|
return; |
|
} |
|
components.add(node, 2); |
|
}, |
|
|
|
ObjectExpression: function (node) { |
|
if (!utils.isES5Component(node)) { |
|
return; |
|
} |
|
components.add(node, 2); |
|
}, |
|
|
|
FunctionExpression: function () { |
|
const node = utils.getParentComponent(); |
|
if (!node) { |
|
return; |
|
} |
|
components.add(node, 1); |
|
}, |
|
|
|
FunctionDeclaration: function () { |
|
const node = utils.getParentComponent(); |
|
if (!node) { |
|
return; |
|
} |
|
components.add(node, 1); |
|
}, |
|
|
|
ArrowFunctionExpression: function () { |
|
const node = utils.getParentComponent(); |
|
if (!node) { |
|
return; |
|
} |
|
if (node.expression && utils.isReturningJSX(node)) { |
|
components.add(node, 2); |
|
} else { |
|
components.add(node, 1); |
|
} |
|
}, |
|
|
|
ThisExpression: function () { |
|
const node = utils.getParentComponent(); |
|
if (!node || !/Function/.test(node.type)) { |
|
return; |
|
} |
|
// Ban functions with a ThisExpression |
|
components.add(node, 0); |
|
}, |
|
|
|
ReturnStatement: function (node) { |
|
if (!utils.isReturningJSX(node)) { |
|
return; |
|
} |
|
const parentNode = utils.getParentComponent(); |
|
if (!parentNode) { |
|
return; |
|
} |
|
components.add(parentNode, 2); |
|
}, |
|
}; |
|
|
|
// Update the provided rule instructions to add the component detection |
|
const ruleInstructions = rule(context, components, utils); |
|
const updatedRuleInstructions = Object.assign({}, ruleInstructions); |
|
Object.keys(detectionInstructions).forEach((instruction) => { |
|
updatedRuleInstructions[instruction] = (node) => { |
|
detectionInstructions[instruction](node); |
|
return ruleInstructions[instruction] ? ruleInstructions[instruction](node) : undefined; |
|
}; |
|
}); |
|
// Return the updated rule instructions |
|
return updatedRuleInstructions; |
|
} |
|
|
|
Components.detect = function (rule) { |
|
return componentRule.bind(this, rule); |
|
}; |
|
|
|
module.exports = Components;
|
|
|