250 lines
8.5 KiB
JavaScript
250 lines
8.5 KiB
JavaScript
/* eslint-disable no-shadow, no-param-reassign */
|
|
const pathLib = require('path');
|
|
const t = require('@babel/types');
|
|
const { default: traverse } = require('@babel/traverse');
|
|
const { Analyzer } = require('./helpers/Analyzer.js');
|
|
const { trackDownIdentifierFromScope } = require('./helpers/track-down-identifier.js');
|
|
const { aForEach } = require('../utils/async-array-utils.js');
|
|
|
|
/** @typedef {import('./types').FindClassesAnalyzerOutput} FindClassesAnalyzerOutput */
|
|
/** @typedef {import('./types').FindClassesAnalyzerOutputEntry} FindClassesAnalyzerOutputEntry */
|
|
/** @typedef {import('./types').FindClassesConfig} FindClassesConfig */
|
|
|
|
/**
|
|
* @desc Finds import specifiers and sources
|
|
* @param {BabelAst} ast
|
|
* @param {string} relativePath the file being currently processed
|
|
*/
|
|
async function findMembersPerAstEntry(ast, fullCurrentFilePath, projectPath) {
|
|
// The transformed entry
|
|
const classesFound = [];
|
|
/**
|
|
* @desc Detects private/publicness based on underscores. Checks '$' as well
|
|
* @returns {'public|protected|private'}
|
|
*/
|
|
function computeAccessType(name) {
|
|
if (name.startsWith('_') || name.startsWith('$')) {
|
|
// (at least) 2 prefixes
|
|
if (name.startsWith('__') || name.startsWith('$$')) {
|
|
return 'private';
|
|
}
|
|
return 'protected';
|
|
}
|
|
return 'public';
|
|
}
|
|
|
|
function isStaticProperties({ node }) {
|
|
return node.static && node.kind === 'get' && node.key.name === 'properties';
|
|
}
|
|
|
|
// function isBlacklisted({ node }) {
|
|
// // Handle static getters
|
|
// const sgBlacklistPlatform = ['attributes'];
|
|
// const sgBlacklistLitEl = ['properties', 'styles'];
|
|
// const sgBlacklistLion = ['localizeNamespaces'];
|
|
// const sgBlacklist = [...sgBlacklistPlatform, ...sgBlacklistLitEl, ...sgBlacklistLion];
|
|
// if (node.kind === 'get' && node.static && sgBlacklist.includes(node.key.name)) {
|
|
// return true;
|
|
// }
|
|
// // Handle getters
|
|
// const gBlacklistLitEl = ['updateComplete'];
|
|
// const gBlacklistLion = ['slots'];
|
|
// const gBlacklist = [...gBlacklistLion, ...gBlacklistLitEl];
|
|
// if (node.kind === 'get' && !node.static && gBlacklist.includes(node.key.name)) {
|
|
// return true;
|
|
// }
|
|
// // Handle methods
|
|
// const mBlacklistPlatform = ['constructor', 'connectedCallback', 'disconnectedCallback'];
|
|
// const mBlacklistLitEl = [
|
|
// 'requestUpdateInternal',
|
|
// 'createRenderRoot',
|
|
// 'render',
|
|
// 'updated',
|
|
// 'firstUpdated',
|
|
// 'update',
|
|
// 'shouldUpdate',
|
|
// ];
|
|
// const mBlacklistLion = ['onLocaleUpdated'];
|
|
// const mBlacklist = [...mBlacklistPlatform, ...mBlacklistLitEl, ...mBlacklistLion];
|
|
// if (!node.static && mBlacklist.includes(node.key.name)) {
|
|
// return true;
|
|
// }
|
|
// return false;
|
|
// }
|
|
|
|
async function traverseClass(path, { isMixin } = {}) {
|
|
const classRes = {};
|
|
classRes.name = path.node.id && path.node.id.name;
|
|
classRes.isMixin = Boolean(isMixin);
|
|
if (path.node.superClass) {
|
|
const superClasses = [];
|
|
|
|
// Add all Identifier names
|
|
let parent = path.node.superClass;
|
|
while (parent.type === 'CallExpression') {
|
|
superClasses.push({ name: parent.callee.name, isMixin: true });
|
|
// As long as we are a CallExpression, we will have a parent
|
|
[parent] = parent.arguments;
|
|
}
|
|
// At the end of the chain, we find type === Identifier
|
|
superClasses.push({ name: parent.name, isMixin: false });
|
|
|
|
// For all found superclasses, track down their root location.
|
|
// This will either result in a local, relative path in the project,
|
|
// or an external path like '@lion/overlays'. In the latter case,
|
|
// tracking down will halt and should be done when there is access to
|
|
// the external repo... (similar to how 'match-imports' analyzer works)
|
|
await aForEach(superClasses, async classObj => {
|
|
// Finds the file that holds the declaration of the import
|
|
classObj.rootFile = await trackDownIdentifierFromScope(
|
|
path,
|
|
classObj.name,
|
|
fullCurrentFilePath,
|
|
projectPath,
|
|
);
|
|
});
|
|
classRes.superClasses = superClasses;
|
|
}
|
|
|
|
classRes.members = {};
|
|
classRes.members.props = []; // meta: private, public, getter/setter, (found in static get properties)
|
|
classRes.members.methods = []; // meta: private, public, getter/setter
|
|
path.traverse({
|
|
ClassMethod(path) {
|
|
// if (isBlacklisted(path)) {
|
|
// return;
|
|
// }
|
|
if (isStaticProperties(path)) {
|
|
let hasFoundTopLvlObjExpr = false;
|
|
path.traverse({
|
|
ObjectExpression(path) {
|
|
if (hasFoundTopLvlObjExpr) return;
|
|
hasFoundTopLvlObjExpr = true;
|
|
path.node.properties.forEach(objectProperty => {
|
|
if (!t.isProperty(objectProperty)) {
|
|
// we can also have a SpreadElement
|
|
return;
|
|
}
|
|
const propRes = {};
|
|
const { name } = objectProperty.key;
|
|
propRes.name = name;
|
|
propRes.accessType = computeAccessType(name);
|
|
propRes.kind = [...(propRes.kind || []), objectProperty.kind];
|
|
classRes.members.props.push(propRes);
|
|
});
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const methodRes = {};
|
|
const { name } = path.node.key;
|
|
methodRes.name = name;
|
|
methodRes.accessType = computeAccessType(name);
|
|
|
|
if (path.node.kind === 'set' || path.node.kind === 'get') {
|
|
if (path.node.static) {
|
|
methodRes.static = true;
|
|
}
|
|
methodRes.kind = [...(methodRes.kind || []), path.node.kind];
|
|
// Merge getter/setters into one
|
|
const found = classRes.members.props.find(p => p.name === name);
|
|
if (found) {
|
|
found.kind = [...(found.kind || []), path.node.kind];
|
|
} else {
|
|
classRes.members.props.push(methodRes);
|
|
}
|
|
} else {
|
|
classRes.members.methods.push(methodRes);
|
|
}
|
|
},
|
|
});
|
|
|
|
classesFound.push(classRes);
|
|
}
|
|
|
|
const classesToTraverse = [];
|
|
traverse(ast, {
|
|
ClassDeclaration(path) {
|
|
classesToTraverse.push({ path, isMixin: false });
|
|
},
|
|
ClassExpression(path) {
|
|
classesToTraverse.push({ path, isMixin: true });
|
|
},
|
|
});
|
|
|
|
await aForEach(classesToTraverse, async klass => {
|
|
await traverseClass(klass.path, { isMixin: klass.isMixin });
|
|
});
|
|
|
|
return classesFound;
|
|
}
|
|
|
|
// // TODO: split up and make configurable
|
|
// function _flattenedFormsPostProcessor(queryOutput) {
|
|
// // Temp: post process, so that we, per category, per file, get all public props
|
|
// queryOutput[0].entries = queryOutput[0].entries
|
|
// .filter(entry => {
|
|
// // contains only forms (and thus is not a test or demo)
|
|
// return entry.meta.categories.includes('forms') && entry.meta.categories.length === 1;
|
|
// })
|
|
// .map(entry => {
|
|
// const newResult = entry.result.map(({ name, props, methods }) => {
|
|
// return {
|
|
// name,
|
|
// props: props.filter(p => p.meta.accessType === 'public').map(p => p.name),
|
|
// methods: methods.filter(m => m.meta.accessType === 'public').map(m => m.name),
|
|
// };
|
|
// });
|
|
// return { file: entry.file, result: newResult };
|
|
// });
|
|
// }
|
|
|
|
class FindClassesAnalyzer extends Analyzer {
|
|
constructor() {
|
|
super();
|
|
this.name = 'find-classes';
|
|
}
|
|
|
|
/**
|
|
* @desc Will find all public members (properties (incl. getter/setters)/functions) of a class and
|
|
* will make a distinction between private, public and protected methods
|
|
* @param {FindClassesConfig} customConfig
|
|
*/
|
|
async execute(customConfig = {}) {
|
|
/** @type {FindClassesConfig} */
|
|
const cfg = {
|
|
gatherFilesConfig: {},
|
|
targetProjectPath: null,
|
|
metaConfig: null,
|
|
...customConfig,
|
|
};
|
|
|
|
/**
|
|
* Prepare
|
|
*/
|
|
const analyzerResult = this._prepare(cfg);
|
|
if (analyzerResult) {
|
|
return analyzerResult;
|
|
}
|
|
|
|
/**
|
|
* Traverse
|
|
*/
|
|
/** @type {FindClassesAnalyzerOutput} */
|
|
const queryOutput = await this._traverse(async (ast, { relativePath }) => {
|
|
const projectPath = cfg.targetProjectPath;
|
|
const fullPath = pathLib.resolve(projectPath, relativePath);
|
|
const transformedEntry = await findMembersPerAstEntry(ast, fullPath, projectPath);
|
|
return { result: transformedEntry };
|
|
});
|
|
// _flattenedFormsPostProcessor();
|
|
|
|
/**
|
|
* Finalize
|
|
*/
|
|
return this._finalize(queryOutput, cfg);
|
|
}
|
|
}
|
|
|
|
module.exports = FindClassesAnalyzer;
|