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

/**
* @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;