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.
411 lines
11 KiB
411 lines
11 KiB
6 years ago
|
/**
|
||
|
* @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;
|