+ valueRe = `((${value})|("${value} .*)|(.* ${value}")|(.* ${value} .*))`;
+ }
+ }
+ regex = `<${potentialTag} .*${name}="${valueRe}".+>`;
+ } else {
+ regex = `<${potentialTag} .*${name}(>|( |=).+>)`;
+ }
+ } else if (tag) {
+ regex = `<${potentialTag} .+>`;
+ } else {
+ LogService.error('Please provide a proper Feature');
+ }
+
+ return regex;
+ }
+
+ static _performGrep(searchPath, regex, customConfig) {
+ const cfg = deepmerge(
+ {
+ count: false,
+ gatherFilesConfig: {},
+ hasDebugEnabled: false,
+ },
+ customConfig,
+ );
+
+ const /** @type {string[]} */ ext = cfg.gatherFilesConfig.extensions;
+ const include = ext ? `--include="\\.(${ext.map(e => e.slice(1)).join('|')})" ` : '';
+ const count = cfg.count ? ' | wc -l' : '';
+
+ // TODO: test on Linux (only tested on Mac)
+ const cmd = `pcregrep -ornM ${include} '${regex}' ${searchPath} ${count}`;
+
+ if (cfg.hasDebugEnabled) {
+ LogService.debug(cmd, 'grep command');
+ }
+
+ return new Promise(resolve => {
+ child_process.exec(cmd, { maxBuffer: 200000000 }, (err, stdout) => {
+ resolve(stdout);
+ });
+ });
+ }
+}
+QueryService.cacheDisabled = false;
+
+module.exports = { QueryService };
diff --git a/packages/providence-analytics/src/program/services/ReportService.js b/packages/providence-analytics/src/program/services/ReportService.js
new file mode 100644
index 000000000..c00560b57
--- /dev/null
+++ b/packages/providence-analytics/src/program/services/ReportService.js
@@ -0,0 +1,102 @@
+// @ts-ignore-next-line
+require('../types/index.js');
+
+const fs = require('fs');
+const pathLib = require('path');
+const getHash = require('../utils/get-hash.js');
+
+/**
+ * @desc Should be used to write results to and read results from the file system.
+ * Creates a unique identifier based on searchP, refP (optional) and an already created
+ * @param {object} searchP search target project meta
+ * @param {object} cfg configuration used for analyzer
+ * @param {object} [refP] reference project meta
+ * @returns {string} identifier
+ */
+function createResultIdentifier(searchP, cfg, refP) {
+ // why encodeURIComponent: filters out slashes for path names for stuff like @lion/button
+ const format = p =>
+ `${encodeURIComponent(p.name)}_${p.version || (p.commitHash && p.commitHash.slice(0, 5))}`;
+ const cfgHash = getHash(cfg);
+ return `${format(searchP)}${refP ? `_+_${format(refP)}` : ''}__${cfgHash}`;
+}
+
+class ReportService {
+ /**
+ * @desc
+ * Prints queryResult report to console
+ * @param {QueryResult} queryResult
+ */
+ static printToConsole(queryResult) {
+ /* eslint-disable no-console */
+ console.log('== QUERY: =========');
+ console.log(JSON.stringify(queryResult.meta, null, 2));
+ console.log('\n== RESULT: =========');
+ console.log(JSON.stringify(queryResult.queryOutput, null, 2));
+ console.log('\n----------------------------------------\n');
+ /* eslint-enable no-console */
+ }
+
+ /**
+ * @desc
+ * Prints queryResult report as JSON to outputPath
+ * @param {QueryResult} queryResult
+ * @param {string} [identifier]
+ * @param {string} [outputPath]
+ */
+ static writeToJson(
+ queryResult,
+ identifier = new Date().getTime() / 1000,
+ outputPath = this.outputPath,
+ ) {
+ const output = JSON.stringify(queryResult, null, 2);
+ if (!fs.existsSync(outputPath)) {
+ fs.mkdirSync(outputPath);
+ }
+ const { name } = queryResult.meta.analyzerMeta;
+ const filePath = this._getResultFileNameAndPath(name, identifier);
+ fs.writeFileSync(filePath, output, { flag: 'w' });
+ }
+
+ static set outputPath(p) {
+ this.__outputPath = p;
+ }
+
+ static get outputPath() {
+ return this.__outputPath || pathLib.join(process.cwd(), '/providence-output');
+ }
+
+ static createIdentifier({ targetProject, referenceProject, analyzerConfig }) {
+ return createResultIdentifier(targetProject, analyzerConfig, referenceProject);
+ }
+
+ static getCachedResult({ analyzerName, identifier }) {
+ let cachedResult;
+ try {
+ cachedResult = JSON.parse(
+ fs.readFileSync(this._getResultFileNameAndPath(analyzerName, identifier), 'utf-8'),
+ );
+ // eslint-disable-next-line no-empty
+ } catch (_) {}
+ return cachedResult;
+ }
+
+ static _getResultFileNameAndPath(name, identifier) {
+ return pathLib.join(this.outputPath, `${name || 'query'}_-_${identifier}.json`);
+ }
+
+ static writeEntryToSearchTargetDepsFile(depProj, rootProjectMeta) {
+ const rootProj = `${rootProjectMeta.name}#${rootProjectMeta.version}`;
+ const filePath = pathLib.join(this.outputPath, 'search-target-deps-file.json');
+ let file = {};
+ try {
+ file = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
+ // eslint-disable-next-line no-empty
+ } catch (_) {}
+ const deps = [...(file[rootProj] || []), depProj];
+ file[rootProj] = [...new Set(deps)];
+ fs.writeFileSync(filePath, JSON.stringify(file, null, 2), { flag: 'w' });
+ }
+}
+
+module.exports = { ReportService };
diff --git a/packages/providence-analytics/src/program/types/index.js b/packages/providence-analytics/src/program/types/index.js
new file mode 100644
index 000000000..186e5ed2e
--- /dev/null
+++ b/packages/providence-analytics/src/program/types/index.js
@@ -0,0 +1,57 @@
+/**
+ * @typedef {Object} Feature
+ * @property {string} [name] the name of the feature. For instance 'size'
+ * @property {string} [value] the value of the feature. For instance 'xl'
+ * @property {string} [memberOf] the name of the object this feature belongs to.
+ *
+ * @property {string} [tag] the HTML element it belongs to. Will be used in html
+ * queries. This option will take precedence over 'memberOf' when configured
+ * @property {boolean} [isAttribute] useful for HTML queries explicitly looking for attribute
+ * name instead of property name. When false(default), query searches for properties
+ * @property {boolean} [usesValueContains] when the attribute value is not an exact match
+ * @property {boolean} [usesValuePartialMatch] when looking for a partial match:
+ * div[class*=foo*] ->
+ * @property {boolean} [usesTagPartialMatch] when looking for an exact match inside a space
+ * separated list within an attr: div[class*=foo] ->
+ */
+
+/**
+ * @typedef {Object} QueryResult result of a query. For all projects and files, gives the
+ * result of the query.
+ * @property {Object} QueryResult.meta
+ * @property {'ast'|'grep'} QueryResult.meta.searchType
+ * @property {QueryConfig} QueryResult.meta.query
+ * @property {Object[]} QueryResult.results
+ * @property {string} QueryResult.queryOutput[].project project name as determined by InputDataService (based on folder name)
+ * @property {number} QueryResult.queryOutput[].count
+ * @property {Object[]} [QueryResult.queryOutput[].files]
+ * @property {string} QueryResult.queryOutput[].files[].file
+ * @property {number} QueryResult.queryOutput[].files[].line
+ * @property {string} QueryResult.queryOutput[].files[].match
+ */
+
+/**
+ * @typedef {object} QueryConfig an object containing keys name, value, term, tag
+ * @property {string} QueryConfig.type the type of the tag we are searching for.
+ * A certain type has an additional property with more detailed information about the type
+ * @property {Feature} feature query details for a feature search
+ */
+
+/**
+ * @typedef {Object} InputDataProject - all files found that are queryable
+ * @property {string} InputDataProject.project - the project name
+ * @property {string} InputDataProject.path - the path to the project
+ * @property {string[]} InputDataProject.entries - array of paths that are found within 'project' that
+ * comply to the rules as configured in 'gatherFilesConfig'
+ */
+
+/**
+ * @typedef {InputDataProject[]} InputData - all files found that are queryable
+ */
+
+/**
+ * @typedef {Object} GatherFilesConfig
+ * @property {string[]} [extensions] file extension like ['.js', '.html']
+ * @property {string[]} [excludeFiles] file names filtered out
+ * @property {string[]} [excludeFolders] folder names filtered outs
+ */
diff --git a/packages/providence-analytics/src/program/utils/async-array-utils.js b/packages/providence-analytics/src/program/utils/async-array-utils.js
new file mode 100644
index 000000000..27312e8f6
--- /dev/null
+++ b/packages/providence-analytics/src/program/utils/async-array-utils.js
@@ -0,0 +1,41 @@
+/**
+ * @desc Readable way to do an async forEach
+ * Since predictability mathers, all array items will be handled in a queue;
+ * one after anotoher
+ * @param {array} array
+ * @param {function} callback
+ */
+async function aForEach(array, callback) {
+ for (let i = 0; i < array.length; i += 1) {
+ // eslint-disable-next-line no-await-in-loop
+ await callback(array[i], i);
+ }
+}
+/**
+ * @desc Readable way to do an async forEach
+ * Since predictability mathers, all array items will be handled in a queue;
+ * one after anotoher
+ * @param {array} array
+ * @param {function} callback
+ */
+async function aForEachNonSequential(array, callback) {
+ return Promise.all(array.map(callback));
+}
+/**
+ * @desc Readable way to do an async map
+ * Since predictability is crucial for a map, all array items will be handled in a queue;
+ * one after anotoher
+ * @param {array} array
+ * @param {function} callback
+ */
+async function aMap(array, callback) {
+ const mappedResults = [];
+ for (let i = 0; i < array.length; i += 1) {
+ // eslint-disable-next-line no-await-in-loop
+ const resolvedCb = await callback(array[i], i);
+ mappedResults.push(resolvedCb);
+ }
+ return mappedResults;
+}
+
+module.exports = { aForEach, aMap, aForEachNonSequential };
diff --git a/packages/providence-analytics/src/program/utils/get-file-path-relative-from-root.js b/packages/providence-analytics/src/program/utils/get-file-path-relative-from-root.js
new file mode 100644
index 000000000..ee87ca090
--- /dev/null
+++ b/packages/providence-analytics/src/program/utils/get-file-path-relative-from-root.js
@@ -0,0 +1,12 @@
+/**
+ * @desc relative path of analyzed file, realtive to project root of analyzed project
+ * - from: '/my/machine/details/analyzed-project/relevant/file.js'
+ * - to: './relevant/file.js'
+ * @param {string} absolutePath
+ * @param {string} projectRoot
+ */
+function getFilePathRelativeFromRoot(absolutePath, projectRoot) {
+ return absolutePath.replace(projectRoot, '.');
+}
+
+module.exports = { getFilePathRelativeFromRoot };
diff --git a/packages/providence-analytics/src/program/utils/get-hash.js b/packages/providence-analytics/src/program/utils/get-hash.js
new file mode 100644
index 000000000..c1f13c718
--- /dev/null
+++ b/packages/providence-analytics/src/program/utils/get-hash.js
@@ -0,0 +1,19 @@
+/**
+ *
+ * @param {string|object} inputValue
+ * @returns {number}
+ */
+function getHash(inputValue) {
+ if (typeof inputValue === 'object') {
+ // eslint-disable-next-line no-param-reassign
+ inputValue = JSON.stringify(inputValue);
+ }
+ return inputValue.split('').reduce(
+ (prevHash, currVal) =>
+ // eslint-disable-next-line no-bitwise
+ ((prevHash << 5) - prevHash + currVal.charCodeAt(0)) | 0,
+ 0,
+ );
+}
+
+module.exports = getHash;
diff --git a/packages/providence-analytics/src/program/utils/jsdoc-comment-parser.js b/packages/providence-analytics/src/program/utils/jsdoc-comment-parser.js
new file mode 100644
index 000000000..85d4bf069
--- /dev/null
+++ b/packages/providence-analytics/src/program/utils/jsdoc-comment-parser.js
@@ -0,0 +1,125 @@
+/* eslint-disable */
+
+/**
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 Ryo Maruyama
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+// From: https://github.com/esdoc/esdoc/blob/master/src/Parser/CommentParser.js
+
+/**
+ * Doc Comment Parser class.
+ *
+ * @example
+ * for (let comment of node.leadingComments) {
+ * let tags = CommentParser.parse(comment);
+ * console.log(tags);
+ * }
+ */
+class JsdocCommentParser {
+ /**
+ * parse comment to tags.
+ * @param {ASTNode} commentNode - comment node.
+ * @param {string} commentNode.value - comment body.
+ * @param {string} commentNode.type - CommentBlock or CommentLine.
+ * @returns {Tag[]} parsed comment.
+ */
+ static parse(commentNode) {
+ if (!this.isESDoc(commentNode)) return [];
+
+ let comment = commentNode.value;
+
+ // TODO: refactor
+ comment = comment.replace(/\r\n/gm, '\n'); // for windows
+ comment = comment.replace(/^[\t ]*/gm, ''); // remove line head space
+ comment = comment.replace(/^\*[\t ]?/, ''); // remove first '*'
+ comment = comment.replace(/[\t ]$/, ''); // remove last space
+ comment = comment.replace(/^\*[\t ]?/gm, ''); // remove line head '*'
+ if (comment.charAt(0) !== '@') comment = `@desc ${comment}`; // auto insert @desc
+ comment = comment.replace(/[\t ]*$/, ''); // remove tail space.
+ comment = comment.replace(/```[\s\S]*?```/g, match => match.replace(/@/g, '\\ESCAPED_AT\\')); // escape code in descriptions
+ comment = comment.replace(/^[\t ]*(@\w+)$/gm, '$1 \\TRUE'); // auto insert tag text to non-text tag (e.g. @interface)
+ comment = comment.replace(/^[\t ]*(@\w+)[\t ](.*)/gm, '\\Z$1\\Z$2'); // insert separator (\\Z@tag\\Ztext)
+ const lines = comment.split('\\Z');
+
+ let tagName = '';
+ let tagValue = '';
+ const tags = [];
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ if (line.charAt(0) === '@') {
+ tagName = line;
+ const nextLine = lines[i + 1];
+ if (nextLine.charAt(0) === '@') {
+ tagValue = '';
+ } else {
+ tagValue = nextLine;
+ i++;
+ }
+ tagValue = tagValue
+ .replace('\\TRUE', '')
+ .replace(/\\ESCAPED_AT\\/g, '@')
+ .replace(/^\n/, '')
+ .replace(/\n*$/, '');
+ tags.push({ tagName, tagValue });
+ }
+ }
+ return tags;
+ }
+
+ /**
+ * parse node to tags.
+ * @param {ASTNode} node - node.
+ * @returns {{tags: Tag[], commentNode: CommentNode}} parsed comment.
+ */
+ static parseFromNode(node) {
+ if (!node.leadingComments) node.leadingComments = [{ type: 'CommentBlock', value: '' }];
+ const commentNode = node.leadingComments[node.leadingComments.length - 1];
+ const tags = this.parse(commentNode);
+
+ return { tags, commentNode };
+ }
+
+ /**
+ * judge doc comment or not.
+ * @param {ASTNode} commentNode - comment node.
+ * @returns {boolean} if true, this comment node is doc comment.
+ */
+ static isESDoc(commentNode) {
+ if (commentNode.type !== 'CommentBlock') return false;
+ return commentNode.value.charAt(0) === '*';
+ }
+
+ /**
+ * build comment from tags
+ * @param {Tag[]} tags
+ * @returns {string} block comment value.
+ */
+ static buildComment(tags) {
+ return tags.reduce((comment, tag) => {
+ const line = tag.tagValue.replace(/\n/g, '\n * ');
+ return `${comment} * ${tag.tagName} \n * ${line} \n`;
+ }, '*\n');
+ }
+}
+
+module.exports = JsdocCommentParser;
diff --git a/packages/providence-analytics/src/program/utils/lit-to-obj.js b/packages/providence-analytics/src/program/utils/lit-to-obj.js
new file mode 100644
index 000000000..9e4dfae09
--- /dev/null
+++ b/packages/providence-analytics/src/program/utils/lit-to-obj.js
@@ -0,0 +1,23 @@
+// import htm from 'htm';
+const htm = require('htm');
+
+function convertToObj(type, props, ...children) {
+ return { type, props, children };
+}
+
+/**
+ * @desc
+ * Used for parsing lit-html templates inside ASTs
+ * @returns {type, props, children}
+ *
+ * @example
+ * litToObj`
Hello world!
`;
+ * // {
+ * // type: 'h1',
+ * // props: { .id: 'hello' },
+ * // children: ['Hello world!']
+ * // }
+ */
+const litToObj = htm.bind(convertToObj);
+
+module.exports = litToObj;
diff --git a/packages/providence-analytics/src/program/utils/memoize.js b/packages/providence-analytics/src/program/utils/memoize.js
new file mode 100644
index 000000000..faf5b8317
--- /dev/null
+++ b/packages/providence-analytics/src/program/utils/memoize.js
@@ -0,0 +1,34 @@
+function memoize(func, externalStorage) {
+ const storage = externalStorage || {};
+ // eslint-disable-next-line func-names
+ return function () {
+ // eslint-disable-next-line prefer-rest-params
+ const args = [...arguments];
+ if (args in storage) {
+ return storage[args];
+ }
+ const outcome = func.apply(this, args);
+ storage[args] = outcome;
+ return outcome;
+ };
+}
+
+function memoizeAsync(func, externalStorage) {
+ const storage = externalStorage || {};
+ // eslint-disable-next-line func-names
+ return async function () {
+ // eslint-disable-next-line prefer-rest-params
+ const args = [...arguments];
+ if (args in storage) {
+ return storage[args];
+ }
+ const outcome = await func.apply(this, args);
+ storage[args] = outcome;
+ return outcome;
+ };
+}
+
+module.exports = {
+ memoize,
+ memoizeAsync,
+};
diff --git a/packages/providence-analytics/src/program/utils/read-package-tree-with-bower-support.js b/packages/providence-analytics/src/program/utils/read-package-tree-with-bower-support.js
new file mode 100644
index 000000000..5fae5891e
--- /dev/null
+++ b/packages/providence-analytics/src/program/utils/read-package-tree-with-bower-support.js
@@ -0,0 +1,222 @@
+/* eslint-disable */
+/**
+ * This is a modified version of https://github.com/npm/read-package-tree/blob/master/rpt.js
+ * The original is meant for npm dependencies only. In our (rare) case, we have a hybrid landscape
+ * where we also want to look for npm dependencies inside bower dependencies (bower_components folder).
+ *
+ * Original: https://github.com/npm/read-package-tree
+ *
+ * The ISC License
+ *
+ * Copyright (c) Isaac Z. Schlueter and Contributors
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+ * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+const fs = require('fs');
+/* istanbul ignore next */
+const promisify = require('util').promisify || require('util-promisify');
+const { resolve, basename, dirname, join } = require('path');
+const rpj = promisify(require('read-package-json'));
+const readdir = promisify(require('readdir-scoped-modules'));
+const realpath = require('read-package-tree/realpath.js');
+
+let ID = 0;
+class Node {
+ constructor(pkg, logical, physical, er, cache) {
+ // should be impossible.
+ const cached = cache.get(physical);
+ /* istanbul ignore next */
+ if (cached && !cached.then) throw new Error('re-creating already instantiated node');
+
+ cache.set(physical, this);
+
+ const parent = basename(dirname(logical));
+ if (parent.charAt(0) === '@') this.name = `${parent}/${basename(logical)}`;
+ else this.name = basename(logical);
+ this.path = logical;
+ this.realpath = physical;
+ this.error = er;
+ this.id = ID++;
+ this.package = pkg || {};
+ this.parent = null;
+ this.isLink = false;
+ this.children = [];
+ }
+}
+
+class Link extends Node {
+ constructor(pkg, logical, physical, realpath, er, cache) {
+ super(pkg, logical, physical, er, cache);
+
+ // if the target has started, but not completed, then
+ // a Promise will be in the cache to indicate this.
+ const cachedTarget = cache.get(realpath);
+ if (cachedTarget && cachedTarget.then)
+ cachedTarget.then(node => {
+ this.target = node;
+ this.children = node.children;
+ });
+
+ this.target = cachedTarget || new Node(pkg, logical, realpath, er, cache);
+ this.realpath = realpath;
+ this.isLink = true;
+ this.error = er;
+ this.children = this.target.children;
+ }
+}
+
+// this is the way it is to expose a timing issue which is difficult to
+// test otherwise. The creation of a Node may take slightly longer than
+// the creation of a Link that targets it. If the Node has _begun_ its
+// creation phase (and put a Promise in the cache) then the Link will
+// get a Promise as its cachedTarget instead of an actual Node object.
+// This is not a problem, because it gets resolved prior to returning
+// the tree or attempting to load children. However, it IS remarkably
+// difficult to get to happen in a test environment to verify reliably.
+// Hence this kludge.
+const newNode = (pkg, logical, physical, er, cache) =>
+ process.env._TEST_RPT_SLOW_LINK_TARGET_ === '1'
+ ? new Promise(res => setTimeout(() => res(new Node(pkg, logical, physical, er, cache)), 10))
+ : new Node(pkg, logical, physical, er, cache);
+
+const loadNode = (logical, physical, cache, rpcache, stcache) => {
+ // cache temporarily holds a promise placeholder so we
+ // don't try to create the same node multiple times.
+ // this is very rare to encounter, given the aggressive
+ // caching on fs.realpath and fs.lstat calls, but
+ // it can happen in theory.
+ const cached = cache.get(physical);
+ /* istanbul ignore next */
+ if (cached) return Promise.resolve(cached);
+
+ const p = realpath(physical, rpcache, stcache, 0).then(
+ real =>
+ rpj(join(real, 'package.json'))
+ .then(
+ pkg => [pkg, null],
+ er => [null, er],
+ )
+ .then(([pkg, er]) =>
+ physical === real
+ ? newNode(pkg, logical, physical, er, cache)
+ : new Link(pkg, logical, physical, real, er, cache),
+ ),
+ // if the realpath fails, don't bother with the rest
+ er => new Node(null, logical, physical, er, cache),
+ );
+
+ cache.set(physical, p);
+ return p;
+};
+
+const loadChildren = (node, cache, filterWith, rpcache, stcache, mode) => {
+ // if a Link target has started, but not completed, then
+ // a Promise will be in the cache to indicate this.
+ //
+ // XXX When we can one day loadChildren on the link *target* instead of
+ // the link itself, to match real dep resolution, then we may end up with
+ // a node target in the cache that isn't yet done resolving when we get
+ // here. For now, though, this line will never be reached, so it's hidden
+ //
+ // if (node.then)
+ // return node.then(node => loadChildren(node, cache, filterWith, rpcache, stcache))
+
+ let depFolder = 'node_modules';
+ if (mode === 'bower') {
+ // TODO: if people rename their bower_components folder to smth like "lib", please handle
+ depFolder = 'bower_components';
+ try {
+ const bowerrc = JSON.parse(fs.readFileSync(join(node.path, '.bowerrc')));
+ if (bowerrc && bowerrc.directory) {
+ depFolder = bowerrc.directory;
+ }
+ } catch (_) {}
+ }
+ const nm = join(node.path, depFolder);
+ // const nm = join(node.path, 'bower_components')
+ return realpath(nm, rpcache, stcache, 0)
+ .then(rm => readdir(rm).then(kids => [rm, kids]))
+ .then(([rm, kids]) =>
+ Promise.all(
+ kids
+ .filter(kid => kid.charAt(0) !== '.' && (!filterWith || filterWith(node, kid)))
+ .map(kid => loadNode(join(nm, kid), join(rm, kid), cache, rpcache, stcache)),
+ ),
+ )
+ .then(kidNodes => {
+ kidNodes.forEach(k => (k.parent = node));
+ node.children.push.apply(
+ node.children,
+ kidNodes.sort((a, b) =>
+ (a.package.name ? a.package.name.toLowerCase() : a.path).localeCompare(
+ b.package.name ? b.package.name.toLowerCase() : b.path,
+ ),
+ ),
+ );
+ return node;
+ })
+ .catch(() => node);
+};
+
+const loadTree = (node, did, cache, filterWith, rpcache, stcache, mode) => {
+ // impossible except in pathological ELOOP cases
+ /* istanbul ignore next */
+ if (did.has(node.realpath)) return Promise.resolve(node);
+
+ did.add(node.realpath);
+
+ // load children on the target, not the link
+ return loadChildren(node, cache, filterWith, rpcache, stcache, mode)
+ .then(node =>
+ Promise.all(
+ node.children
+ .filter(kid => !did.has(kid.realpath))
+ .map(kid => loadTree(kid, did, cache, filterWith, rpcache, stcache, mode)),
+ ),
+ )
+ .then(() => node);
+};
+
+// XXX Drop filterWith and/or cb in next semver major bump
+/**
+ *
+ * @param {*} root
+ * @param {*} filterWith
+ * @param {*} cb
+ * @param {'npm'|'bower'} [mode='npm'] if mode is 'bower', will look in 'bower_components' instead
+ * of 'node_modules'
+ */
+const rpt = (root, filterWith, cb, mode = 'npm') => {
+ if (!cb && typeof filterWith === 'function') {
+ cb = filterWith;
+ filterWith = null;
+ }
+
+ const cache = new Map();
+ // we can assume that the cwd is real enough
+ const cwd = process.cwd();
+ const rpcache = new Map([[cwd, cwd]]);
+ const stcache = new Map();
+ const p = realpath(root, rpcache, stcache, 0)
+ .then(realRoot => loadNode(root, realRoot, cache, rpcache, stcache))
+ .then(node => loadTree(node, new Set(), cache, filterWith, rpcache, stcache, mode));
+
+ if (typeof cb === 'function') p.then(tree => cb(null, tree), cb);
+
+ return p;
+};
+
+rpt.Node = Node;
+rpt.Link = Link;
+module.exports = rpt;
diff --git a/packages/providence-analytics/src/program/utils/relative-source-path.js b/packages/providence-analytics/src/program/utils/relative-source-path.js
new file mode 100644
index 000000000..b7cc8a1b6
--- /dev/null
+++ b/packages/providence-analytics/src/program/utils/relative-source-path.js
@@ -0,0 +1,24 @@
+/**
+ * @desc determines for a source path of an import- or export specifier, whether
+ * it is relative (an internal import/export) or absolute (external)
+ * - relative: './helpers', './helpers.js', '../helpers.js'
+ * - not relative: '@open-wc/helpers', 'project-x/helpers'
+ * @param {string} source source path of an import- or export specifier
+ * @returns {boolean}
+ */
+function isRelativeSourcePath(source) {
+ return source.startsWith('.');
+}
+
+/**
+ * @desc Simple helper te make code a bit more readable.
+ * - from '/path/to/repo/my/file.js';
+ * - to './my/file.js'
+ * @param {string} fullPath like '/path/to/repo/my/file.js'
+ * @param {string} rootPath like '/path/to/repo'
+ */
+function toRelativeSourcePath(fullPath, rootPath) {
+ return fullPath.replace(rootPath, '.');
+}
+
+module.exports = { isRelativeSourcePath, toRelativeSourcePath };
diff --git a/packages/providence-analytics/src/program/utils/resolve-import-path.js b/packages/providence-analytics/src/program/utils/resolve-import-path.js
new file mode 100644
index 000000000..48fc8089d
--- /dev/null
+++ b/packages/providence-analytics/src/program/utils/resolve-import-path.js
@@ -0,0 +1,50 @@
+/**
+ * Solution inspired by es-dev-server:
+ * https://github.com/open-wc/open-wc/blob/master/packages/es-dev-server/src/utils/resolve-module-imports.js
+ */
+
+const pathLib = require('path');
+const nodeResolvePackageJson = require('@rollup/plugin-node-resolve/package.json');
+const createRollupResolve = require('@rollup/plugin-node-resolve');
+const { LogService } = require('../services/LogService.js');
+
+const fakePluginContext = {
+ meta: {
+ rollupVersion: nodeResolvePackageJson.peerDependencies.rollup,
+ },
+ warn(...msg) {
+ LogService.warn('[resolve-import-path]: ', ...msg);
+ },
+};
+
+/**
+ * @desc based on importee (in a statement "import {x} from '@lion/core'", "@lion/core" is an
+ * importee), which can be a bare module specifier, a filename without extension, or a folder
+ * name without an extension.
+ * @param {string} importee source like '@lion/core'
+ * @param {string} importer importing file, like '/my/project/importing-file.js'
+ * @returns {string} the resolved file system path, like '/my/project/node_modules/@lion/core/index.js'
+ */
+async function resolveImportPath(importee, importer, opts = {}) {
+ const rollupResolve = createRollupResolve({
+ rootDir: pathLib.dirname(importer),
+ // allow resolving polyfills for nodejs libs
+ preferBuiltins: false,
+ // extensions: ['.mjs', '.js', '.json', '.node'],
+ ...opts,
+ });
+
+ const preserveSymlinks =
+ (opts && opts.customResolveOptions && opts.customResolveOptions.preserveSymlinks) || false;
+ rollupResolve.buildStart.call(fakePluginContext, { preserveSymlinks });
+
+ const result = await rollupResolve.resolveId.call(fakePluginContext, importee, importer);
+ if (!result || !result.id) {
+ // throw new Error(`importee ${importee} not found in filesystem.`);
+ LogService.warn(`importee ${importee} not found in filesystem for importer '${importer}'.`);
+ return null;
+ }
+ return result.id;
+}
+
+module.exports = { resolveImportPath };
diff --git a/packages/providence-analytics/src/program/utils/traverse-html.js b/packages/providence-analytics/src/program/utils/traverse-html.js
new file mode 100644
index 000000000..ba673ad80
--- /dev/null
+++ b/packages/providence-analytics/src/program/utils/traverse-html.js
@@ -0,0 +1,28 @@
+/**
+ * @param {ASTNode} curNode Node to start from. Will loop over its children
+ * @param {object} processObject Will be executed for every node
+ * @param {ASTNode} [parentNode] parent of curNode
+ */
+function traverseHtml(curNode, processObject) {
+ function pathify(node) {
+ return {
+ node,
+ traverse(obj) {
+ traverseHtml(node, obj);
+ },
+ };
+ }
+
+ // let done = processFn(curNode, parentNode);
+ if (processObject[curNode.nodeName]) {
+ processObject[curNode.nodeName](pathify(curNode));
+ }
+
+ if (curNode.childNodes) {
+ curNode.childNodes.forEach(childNode => {
+ traverseHtml(childNode, processObject, curNode);
+ });
+ }
+}
+
+module.exports = traverseHtml;
diff --git a/packages/providence-analytics/test-helpers/mock-log-service-helpers.js b/packages/providence-analytics/test-helpers/mock-log-service-helpers.js
new file mode 100644
index 000000000..948f1ab6e
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/mock-log-service-helpers.js
@@ -0,0 +1,57 @@
+const { LogService } = require('../src/program/services/LogService.js');
+
+const originalWarn = LogService.warn;
+function suppressWarningLogs() {
+ LogService.warn = () => {};
+}
+function restoreSuppressWarningLogs() {
+ LogService.warn = originalWarn;
+}
+
+const originalInfo = LogService.info;
+function suppressInfoLogs() {
+ LogService.info = () => {};
+}
+function restoreSuppressInfoLogs() {
+ LogService.info = originalInfo;
+}
+
+const originalDebug = LogService.debug;
+function suppressDebugLogs() {
+ LogService.debug = () => {};
+}
+function restoreSuppressDebugLogs() {
+ LogService.debug = originalDebug;
+}
+
+const originalSuccess = LogService.success;
+function suppressSuccessLogs() {
+ LogService.success = () => {};
+}
+function restoreSuppressSuccessLogs() {
+ LogService.success = originalSuccess;
+}
+
+function suppressNonCriticalLogs() {
+ suppressInfoLogs();
+ suppressWarningLogs();
+ suppressDebugLogs();
+ suppressSuccessLogs();
+}
+
+function restoreSuppressNonCriticalLogs() {
+ restoreSuppressInfoLogs();
+ restoreSuppressWarningLogs();
+ restoreSuppressDebugLogs();
+ restoreSuppressSuccessLogs();
+}
+
+module.exports = {
+ suppressWarningLogs,
+ restoreSuppressWarningLogs,
+ suppressInfoLogs,
+ restoreSuppressInfoLogs,
+
+ suppressNonCriticalLogs,
+ restoreSuppressNonCriticalLogs,
+};
diff --git a/packages/providence-analytics/test-helpers/mock-project-helpers.js b/packages/providence-analytics/test-helpers/mock-project-helpers.js
new file mode 100644
index 000000000..c07339c09
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/mock-project-helpers.js
@@ -0,0 +1,125 @@
+// eslint-disable-next-line import/no-extraneous-dependencies
+const mockFs = require('mock-fs');
+const path = require('path');
+
+/**
+ * @desc Makes sure that, whenever the main program (providence) calls
+ * "InputDataService.createDataObject", it gives back a mocked response.
+ * @param {string[]|object} files all the code that will be run trhough AST
+ * @param {object} [cfg]
+ * @param {string} [cfg.project='fictional-project']
+ * @param {string} [cfg.projectPath='/fictional/project']
+ * @param {string[]} [cfg.filePath=`/fictional/project/test-file-${i}.js`] The indexes of the file
+ * paths match with the indexes of the files
+ * @param {object} existingMock config for mock-fs, so the previous config is not overridden
+ */
+function mockProject(files, cfg = {}, existingMock = {}) {
+ const projName = cfg.projectName || 'fictional-project';
+ const projPath = cfg.projectPath || '/fictional/project';
+
+ // Create obj structure for mock-fs
+ // eslint-disable-next-line no-shadow
+ function createFilesObjForFolder(files) {
+ let projFilesObj = {};
+ if (Array.isArray(files)) {
+ projFilesObj = files.reduce((res, code, i) => {
+ const fileName = (cfg.filePaths && cfg.filePaths[i]) || `./test-file-${i}.js`;
+ const localFileName = path.resolve(projPath, fileName);
+ res[localFileName] = code;
+ return res;
+ }, {});
+ } else {
+ Object.keys(files).forEach(f => {
+ const localFileName = path.resolve(projPath, f);
+ projFilesObj[localFileName] = files[f];
+ });
+ }
+ return projFilesObj;
+ }
+
+ const optionalPackageJson = {};
+ const hasPackageJson = cfg.filePaths && cfg.filePaths.includes('./package.json');
+ if (!hasPackageJson) {
+ optionalPackageJson[projPath] = {
+ 'package.json': `{ "name": "${projName}" , "version": "${cfg.version || '0.1.0-mock'}" }`,
+ };
+ }
+
+ const totalMock = {
+ ...existingMock, // can only add to mock-fs, not expand existing config?
+ ...optionalPackageJson,
+ ...createFilesObjForFolder(files),
+ };
+
+ mockFs(totalMock);
+ return totalMock;
+}
+
+function restoreMockedProjects() {
+ mockFs.restore();
+}
+
+function getEntry(queryResult, index = 0) {
+ return queryResult.queryOutput[index];
+}
+
+function getEntries(queryResult) {
+ return queryResult.queryOutput;
+}
+
+/**
+ * Requires two config objects (see match-imports and match-subclasses tests)
+ * and based on those, will use mock-fs package to mock them in the file system.
+ * All missing information (like target depending on ref, version numbers, project names
+ * and paths will be auto generated when not specified.)
+ * When a non imported ref dependency or a wrong version of a dev dependency needs to be
+ * tested, please explicitly provide a ./package.json that does so.
+ */
+function mockTargetAndReferenceProject(searchTargetProject, referenceProject) {
+ const targetProjectName = searchTargetProject.name || 'fictional-target-project';
+ const refProjectName = referenceProject.name || 'fictional-ref-project';
+
+ const targetcodeSnippets = searchTargetProject.files.map(f => f.code);
+ const targetFilePaths = searchTargetProject.files.map(f => f.file);
+ const refVersion = referenceProject.version || '1.0.0';
+
+ const targetHasPackageJson = targetFilePaths.includes('./package.json');
+ // Make target depend on ref
+ if (!targetHasPackageJson) {
+ targetcodeSnippets.push(`{
+ "name": "${targetProjectName}" ,
+ "version": "1.0.0",
+ "dependencies": {
+ "${refProjectName}": "${refVersion}"
+ }
+ }`);
+ targetFilePaths.push('./package.json');
+ }
+
+ // Create target mock
+ const targetMock = mockProject(targetcodeSnippets, {
+ filePaths: targetFilePaths,
+ projectName: targetProjectName,
+ projectPath: searchTargetProject.path || 'fictional/target/project',
+ });
+
+ // Append ref mock
+ mockProject(
+ referenceProject.files.map(f => f.code),
+ {
+ filePaths: referenceProject.files.map(f => f.file),
+ projectName: refProjectName,
+ projectPath: referenceProject.path || 'fictional/ref/project',
+ version: refVersion,
+ },
+ targetMock,
+ );
+}
+
+module.exports = {
+ mockProject,
+ restoreMockedProjects,
+ getEntry,
+ getEntries,
+ mockTargetAndReferenceProject,
+};
diff --git a/packages/providence-analytics/test-helpers/mock-report-service-helpers.js b/packages/providence-analytics/test-helpers/mock-report-service-helpers.js
new file mode 100644
index 000000000..8f7d39440
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/mock-report-service-helpers.js
@@ -0,0 +1,21 @@
+const { ReportService } = require('../src/program/services/ReportService.js');
+
+const originalWriteToJson = ReportService.writeToJson;
+
+function mockWriteToJson(queryResults) {
+ ReportService.writeToJson = queryResult => {
+ queryResults.push(queryResult);
+ };
+}
+
+function restoreWriteToJson(queryResults) {
+ ReportService.writeToJson = originalWriteToJson;
+ while (queryResults && queryResults.length) {
+ queryResults.pop();
+ }
+}
+
+module.exports = {
+ mockWriteToJson,
+ restoreWriteToJson,
+};
diff --git a/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/find-classes.json b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/find-classes.json
new file mode 100644
index 000000000..28ea62592
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/find-classes.json
@@ -0,0 +1,219 @@
+{
+ "meta": {
+ "searchType": "ast-analyzer",
+ "analyzerMeta": {
+ "name": "find-classes",
+ "requiredAst": "babel",
+ "identifier": "importing-target-project_0.0.2-target-mock__-297820780",
+ "targetProject": {
+ "mainEntry": "./target-src/match-imports/root-level-imports.js",
+ "name": "importing-target-project",
+ "version": "0.0.2-target-mock",
+ "commitHash": "[not-a-git-root]"
+ },
+ "configuration": {
+ "gatherFilesConfig": {},
+ "metaConfig": null
+ }
+ }
+ },
+ "queryOutput": [
+ {
+ "file": "./target-src/find-customelements/multiple.js",
+ "result": [
+ {
+ "name": null,
+ "isMixin": true,
+ "superClasses": [
+ {
+ "name": "HTMLElement",
+ "isMixin": false,
+ "rootFile": {
+ "file": "[current]",
+ "specifier": "HTMLElement"
+ }
+ }
+ ],
+ "members": {
+ "props": [],
+ "methods": []
+ }
+ },
+ {
+ "name": "ExtendedOnTheFly",
+ "isMixin": false,
+ "superClasses": [
+ {
+ "isMixin": true,
+ "rootFile": {
+ "file": "[current]"
+ }
+ },
+ {
+ "isMixin": false,
+ "rootFile": {
+ "file": "[current]"
+ }
+ }
+ ],
+ "members": {
+ "props": [],
+ "methods": []
+ }
+ }
+ ]
+ },
+ {
+ "file": "./target-src/match-subclasses/ExtendedComp.js",
+ "result": [
+ {
+ "name": "ExtendedComp",
+ "isMixin": false,
+ "superClasses": [
+ {
+ "name": "MyCompMixin",
+ "isMixin": true,
+ "rootFile": {
+ "file": "exporting-ref-project",
+ "specifier": "[default]"
+ }
+ },
+ {
+ "name": "RefClass",
+ "isMixin": false,
+ "rootFile": {
+ "file": "exporting-ref-project",
+ "specifier": "RefClass"
+ }
+ }
+ ],
+ "members": {
+ "props": [
+ {
+ "name": "getterSetter",
+ "accessType": "public",
+ "kind": [
+ "get",
+ "set"
+ ]
+ },
+ {
+ "name": "staticGetterSetter",
+ "accessType": "public",
+ "static": true,
+ "kind": [
+ "get",
+ "set"
+ ]
+ },
+ {
+ "name": "attributes",
+ "accessType": "public",
+ "static": true,
+ "kind": [
+ "get"
+ ]
+ },
+ {
+ "name": "styles",
+ "accessType": "public",
+ "static": true,
+ "kind": [
+ "get"
+ ]
+ },
+ {
+ "name": "updateComplete",
+ "accessType": "public",
+ "kind": [
+ "get"
+ ]
+ },
+ {
+ "name": "localizeNamespaces",
+ "accessType": "public",
+ "static": true,
+ "kind": [
+ "get"
+ ]
+ },
+ {
+ "name": "slots",
+ "accessType": "public",
+ "kind": [
+ "get"
+ ]
+ }
+ ],
+ "methods": [
+ {
+ "name": "method",
+ "accessType": "public"
+ },
+ {
+ "name": "_protectedMethod",
+ "accessType": "protected"
+ },
+ {
+ "name": "__privateMethod",
+ "accessType": "private"
+ },
+ {
+ "name": "$protectedMethod",
+ "accessType": "protected"
+ },
+ {
+ "name": "$$privateMethod",
+ "accessType": "private"
+ },
+ {
+ "name": "constructor",
+ "accessType": "public"
+ },
+ {
+ "name": "connectedCallback",
+ "accessType": "public"
+ },
+ {
+ "name": "disconnectedCallback",
+ "accessType": "public"
+ },
+ {
+ "name": "_requestUpdate",
+ "accessType": "protected"
+ },
+ {
+ "name": "createRenderRoot",
+ "accessType": "public"
+ },
+ {
+ "name": "render",
+ "accessType": "public"
+ },
+ {
+ "name": "updated",
+ "accessType": "public"
+ },
+ {
+ "name": "firstUpdated",
+ "accessType": "public"
+ },
+ {
+ "name": "update",
+ "accessType": "public"
+ },
+ {
+ "name": "shouldUpdate",
+ "accessType": "public"
+ },
+ {
+ "name": "onLocaleUpdated",
+ "accessType": "public"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/find-customelements.json b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/find-customelements.json
new file mode 100644
index 000000000..480035016
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/find-customelements.json
@@ -0,0 +1,50 @@
+{
+ "meta": {
+ "searchType": "ast-analyzer",
+ "analyzerMeta": {
+ "name": "find-customelements",
+ "requiredAst": "babel",
+ "identifier": "importing-target-project_0.0.2-target-mock__-2006922104",
+ "targetProject": {
+ "mainEntry": "./target-src/match-imports/root-level-imports.js",
+ "name": "importing-target-project",
+ "version": "0.0.2-target-mock",
+ "commitHash": "[not-a-git-root]"
+ },
+ "configuration": {
+ "gatherFilesConfig": {}
+ }
+ }
+ },
+ "queryOutput": [
+ {
+ "file": "./target-src/find-customelements/multiple.js",
+ "result": [
+ {
+ "tagName": "ref-class",
+ "constructorIdentifier": "RefClass",
+ "rootFile": {
+ "file": "exporting-ref-project",
+ "specifier": "RefClass"
+ }
+ },
+ {
+ "tagName": "extended-comp",
+ "constructorIdentifier": "ExtendedComp",
+ "rootFile": {
+ "file": "./target-src/match-subclasses/ExtendedComp.js",
+ "specifier": "ExtendedComp"
+ }
+ },
+ {
+ "tagName": "on-the-fly",
+ "constructorIdentifier": "[inline]",
+ "rootFile": {
+ "file": "[current]",
+ "specifier": "[inline]"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/find-exports.json b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/find-exports.json
new file mode 100644
index 000000000..b65494aa8
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/find-exports.json
@@ -0,0 +1,195 @@
+{
+ "meta": {
+ "searchType": "ast-analyzer",
+ "analyzerMeta": {
+ "name": "find-exports",
+ "requiredAst": "babel",
+ "identifier": "exporting-ref-project_1.0.0__-1083884764",
+ "targetProject": {
+ "mainEntry": "./index.js",
+ "name": "exporting-ref-project",
+ "version": "1.0.0",
+ "commitHash": "[not-a-git-root]"
+ },
+ "configuration": {
+ "metaConfig": null,
+ "gatherFilesConfig": {}
+ }
+ }
+ },
+ "queryOutput": [
+ {
+ "file": "./index.js",
+ "result": [
+ {
+ "exportSpecifiers": [
+ "[default]"
+ ],
+ "source": "refConstImported",
+ "normalizedSource": "refConstImported",
+ "rootFileMap": [
+ {
+ "currentFileSpecifier": "[default]",
+ "rootFile": {
+ "file": "refConstImported",
+ "specifier": "[default]"
+ }
+ }
+ ]
+ },
+ {
+ "exportSpecifiers": [
+ "RefClass",
+ "RefRenamedClass"
+ ],
+ "localMap": [
+ {
+ "local": "RefClass",
+ "exported": "RefRenamedClass"
+ }
+ ],
+ "source": "./ref-src/core.js",
+ "normalizedSource": "./ref-src/core.js",
+ "rootFileMap": [
+ {
+ "currentFileSpecifier": "RefClass",
+ "rootFile": {
+ "file": "./ref-src/core.js",
+ "specifier": "RefClass"
+ }
+ },
+ {
+ "currentFileSpecifier": "RefRenamedClass",
+ "rootFile": {
+ "file": "./ref-src/core.js",
+ "specifier": "RefClass"
+ }
+ }
+ ]
+ },
+ {
+ "exportSpecifiers": [
+ "[file]"
+ ],
+ "rootFileMap": [
+ null
+ ]
+ }
+ ]
+ },
+ {
+ "file": "./not-imported.js",
+ "result": [
+ {
+ "exportSpecifiers": [
+ "notImported"
+ ],
+ "localMap": [],
+ "source": null,
+ "rootFileMap": [
+ {
+ "currentFileSpecifier": "notImported",
+ "rootFile": {
+ "file": "[current]",
+ "specifier": "notImported"
+ }
+ }
+ ]
+ },
+ {
+ "exportSpecifiers": [
+ "[file]"
+ ],
+ "rootFileMap": [
+ null
+ ]
+ }
+ ]
+ },
+ {
+ "file": "./ref-component.js",
+ "result": [
+ {
+ "exportSpecifiers": [
+ "[file]"
+ ],
+ "rootFileMap": [
+ null
+ ]
+ }
+ ]
+ },
+ {
+ "file": "./ref-src/core.js",
+ "result": [
+ {
+ "exportSpecifiers": [
+ "RefClass"
+ ],
+ "localMap": [],
+ "source": null,
+ "rootFileMap": [
+ {
+ "currentFileSpecifier": "RefClass",
+ "rootFile": {
+ "file": "[current]",
+ "specifier": "RefClass"
+ }
+ }
+ ]
+ },
+ {
+ "exportSpecifiers": [
+ "[default]"
+ ],
+ "rootFileMap": [
+ {
+ "currentFileSpecifier": "[default]",
+ "rootFile": {
+ "file": "[current]",
+ "specifier": "[default]"
+ }
+ }
+ ]
+ },
+ {
+ "exportSpecifiers": [
+ "[file]"
+ ],
+ "rootFileMap": [
+ null
+ ]
+ }
+ ]
+ },
+ {
+ "file": "./ref-src/folder/index.js",
+ "result": [
+ {
+ "exportSpecifiers": [
+ "resolvePathCorrect"
+ ],
+ "localMap": [],
+ "source": null,
+ "rootFileMap": [
+ {
+ "currentFileSpecifier": "resolvePathCorrect",
+ "rootFile": {
+ "file": "[current]",
+ "specifier": "resolvePathCorrect"
+ }
+ }
+ ]
+ },
+ {
+ "exportSpecifiers": [
+ "[file]"
+ ],
+ "rootFileMap": [
+ null
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/find-imports.json b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/find-imports.json
new file mode 100644
index 000000000..f6ac9d3ce
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/find-imports.json
@@ -0,0 +1,202 @@
+{
+ "meta": {
+ "searchType": "ast-analyzer",
+ "analyzerMeta": {
+ "name": "find-imports",
+ "requiredAst": "babel",
+ "identifier": "importing-target-project_0.0.2-target-mock__139587347",
+ "targetProject": {
+ "mainEntry": "./target-src/match-imports/root-level-imports.js",
+ "name": "importing-target-project",
+ "version": "0.0.2-target-mock",
+ "commitHash": "[not-a-git-root]"
+ },
+ "configuration": {
+ "keepInternalSources": false,
+ "gatherFilesConfig": {}
+ }
+ }
+ },
+ "queryOutput": [
+ {
+ "file": "./target-src/find-customelements/multiple.js",
+ "result": [
+ {
+ "importSpecifiers": [
+ "RefClass"
+ ],
+ "source": "exporting-ref-project",
+ "normalizedSource": "exporting-ref-project"
+ }
+ ]
+ },
+ {
+ "file": "./target-src/find-imports/all-notations.js",
+ "result": [
+ {
+ "importSpecifiers": [
+ "[file]"
+ ],
+ "source": "imported/source",
+ "normalizedSource": "imported/source"
+ },
+ {
+ "importSpecifiers": [
+ "[default]"
+ ],
+ "source": "imported/source-a",
+ "normalizedSource": "imported/source-a"
+ },
+ {
+ "importSpecifiers": [
+ "b"
+ ],
+ "source": "imported/source-b",
+ "normalizedSource": "imported/source-b"
+ },
+ {
+ "importSpecifiers": [
+ "c",
+ "d"
+ ],
+ "source": "imported/source-c",
+ "normalizedSource": "imported/source-c"
+ },
+ {
+ "importSpecifiers": [
+ "[default]",
+ "f",
+ "g"
+ ],
+ "source": "imported/source-d",
+ "normalizedSource": "imported/source-d"
+ },
+ {
+ "importSpecifiers": [
+ "[default]"
+ ],
+ "source": "my/source-e",
+ "normalizedSource": "my/source-e"
+ },
+ {
+ "importSpecifiers": [
+ "[default]"
+ ],
+ "source": "[variable]",
+ "normalizedSource": "[variable]"
+ },
+ {
+ "importSpecifiers": [
+ "[*]"
+ ],
+ "source": "imported/source-g",
+ "normalizedSource": "imported/source-g"
+ }
+ ]
+ },
+ {
+ "file": "./target-src/match-imports/deep-imports.js",
+ "result": [
+ {
+ "importSpecifiers": [
+ "RefClass"
+ ],
+ "source": "exporting-ref-project/ref-src/core.js",
+ "normalizedSource": "exporting-ref-project/ref-src/core.js"
+ },
+ {
+ "importSpecifiers": [
+ "[default]"
+ ],
+ "source": "exporting-ref-project/ref-src/core.js",
+ "normalizedSource": "exporting-ref-project/ref-src/core.js"
+ },
+ {
+ "importSpecifiers": [
+ "nonMatched"
+ ],
+ "source": "unknown-project/xyz.js",
+ "normalizedSource": "unknown-project/xyz.js"
+ },
+ {
+ "importSpecifiers": [
+ "[file]"
+ ],
+ "source": "exporting-ref-project/ref-component",
+ "normalizedSource": "exporting-ref-project/ref-component"
+ },
+ {
+ "importSpecifiers": [
+ "resolvePathCorrect"
+ ],
+ "source": "exporting-ref-project/ref-src/folder",
+ "normalizedSource": "exporting-ref-project/ref-src/folder"
+ },
+ {
+ "importSpecifiers": [
+ "[*]"
+ ],
+ "source": "exporting-ref-project/ref-src/core.js",
+ "normalizedSource": "exporting-ref-project/ref-src/core.js"
+ }
+ ]
+ },
+ {
+ "file": "./target-src/match-imports/root-level-imports.js",
+ "result": [
+ {
+ "importSpecifiers": [
+ "RefClass"
+ ],
+ "source": "exporting-ref-project",
+ "normalizedSource": "exporting-ref-project"
+ },
+ {
+ "importSpecifiers": [
+ "RefRenamedClass"
+ ],
+ "source": "exporting-ref-project",
+ "normalizedSource": "exporting-ref-project"
+ },
+ {
+ "importSpecifiers": [
+ "[default]"
+ ],
+ "source": "exporting-ref-project",
+ "normalizedSource": "exporting-ref-project"
+ },
+ {
+ "importSpecifiers": [
+ "nonMatched"
+ ],
+ "source": "unknown-project",
+ "normalizedSource": "unknown-project"
+ }
+ ]
+ },
+ {
+ "file": "./target-src/match-subclasses/ExtendedComp.js",
+ "result": [
+ {
+ "importSpecifiers": [
+ "RefClass"
+ ],
+ "source": "exporting-ref-project",
+ "normalizedSource": "exporting-ref-project"
+ }
+ ]
+ },
+ {
+ "file": "./target-src/match-subclasses/internalProxy.js",
+ "result": [
+ {
+ "importSpecifiers": [
+ "[default]"
+ ],
+ "source": "exporting-ref-project",
+ "normalizedSource": "exporting-ref-project"
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/match-imports.json b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/match-imports.json
new file mode 100644
index 000000000..edc2aa1c1
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/match-imports.json
@@ -0,0 +1,158 @@
+{
+ "meta": {
+ "searchType": "ast-analyzer",
+ "analyzerMeta": {
+ "name": "match-imports",
+ "requiredAst": "babel",
+ "identifier": "importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__453069400",
+ "targetProject": {
+ "mainEntry": "./target-src/match-imports/root-level-imports.js",
+ "name": "importing-target-project",
+ "version": "0.0.2-target-mock",
+ "commitHash": "[not-a-git-root]"
+ },
+ "referenceProject": {
+ "mainEntry": "./index.js",
+ "name": "exporting-ref-project",
+ "version": "1.0.0",
+ "commitHash": "[not-a-git-root]"
+ },
+ "configuration": {
+ "gatherFilesConfig": {}
+ }
+ }
+ },
+ "queryOutput": [
+ {
+ "exportSpecifier": {
+ "name": "[default]",
+ "project": "exporting-ref-project",
+ "filePath": "./index.js",
+ "id": "[default]::./index.js::exporting-ref-project"
+ },
+ "matchesPerProject": [
+ {
+ "project": "importing-target-project",
+ "files": [
+ "./target-src/match-imports/root-level-imports.js",
+ "./target-src/match-subclasses/internalProxy.js"
+ ]
+ }
+ ]
+ },
+ {
+ "exportSpecifier": {
+ "name": "RefClass",
+ "project": "exporting-ref-project",
+ "filePath": "./index.js",
+ "id": "RefClass::./index.js::exporting-ref-project"
+ },
+ "matchesPerProject": [
+ {
+ "project": "importing-target-project",
+ "files": [
+ "./target-src/find-customelements/multiple.js",
+ "./target-src/match-imports/root-level-imports.js",
+ "./target-src/match-subclasses/ExtendedComp.js"
+ ]
+ }
+ ]
+ },
+ {
+ "exportSpecifier": {
+ "name": "RefRenamedClass",
+ "project": "exporting-ref-project",
+ "filePath": "./index.js",
+ "id": "RefRenamedClass::./index.js::exporting-ref-project"
+ },
+ "matchesPerProject": [
+ {
+ "project": "importing-target-project",
+ "files": [
+ "./target-src/match-imports/root-level-imports.js"
+ ]
+ }
+ ]
+ },
+ {
+ "exportSpecifier": {
+ "name": "[file]",
+ "project": "exporting-ref-project",
+ "filePath": "./ref-component.js",
+ "id": "[file]::./ref-component.js::exporting-ref-project"
+ },
+ "matchesPerProject": [
+ {
+ "project": "importing-target-project",
+ "files": [
+ "./target-src/match-imports/deep-imports.js"
+ ]
+ }
+ ]
+ },
+ {
+ "exportSpecifier": {
+ "name": "RefClass",
+ "project": "exporting-ref-project",
+ "filePath": "./ref-src/core.js",
+ "id": "RefClass::./ref-src/core.js::exporting-ref-project"
+ },
+ "matchesPerProject": [
+ {
+ "project": "importing-target-project",
+ "files": [
+ "./target-src/match-imports/deep-imports.js"
+ ]
+ }
+ ]
+ },
+ {
+ "exportSpecifier": {
+ "name": "[default]",
+ "project": "exporting-ref-project",
+ "filePath": "./ref-src/core.js",
+ "id": "[default]::./ref-src/core.js::exporting-ref-project"
+ },
+ "matchesPerProject": [
+ {
+ "project": "importing-target-project",
+ "files": [
+ "./target-src/match-imports/deep-imports.js"
+ ]
+ }
+ ]
+ },
+ {
+ "exportSpecifier": {
+ "name": "[file]",
+ "project": "exporting-ref-project",
+ "filePath": "./ref-src/core.js",
+ "id": "[file]::./ref-src/core.js::exporting-ref-project"
+ },
+ "matchesPerProject": [
+ {
+ "project": "importing-target-project",
+ "files": [
+ "./target-src/match-imports/deep-imports.js"
+ ]
+ }
+ ]
+ },
+ {
+ "exportSpecifier": {
+ "name": "resolvePathCorrect",
+ "project": "exporting-ref-project",
+ "filePath": "./ref-src/folder/index.js",
+ "id": "resolvePathCorrect::./ref-src/folder/index.js::exporting-ref-project"
+ },
+ "matchesPerProject": [
+ {
+ "project": "importing-target-project",
+ "files": [
+ "./target-src/match-imports/deep-imports.js"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/match-paths.json b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/match-paths.json
new file mode 100644
index 000000000..3f7500878
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/match-paths.json
@@ -0,0 +1,92 @@
+{
+ "meta": {
+ "searchType": "ast-analyzer",
+ "analyzerMeta": {
+ "name": "match-paths",
+ "requiredAst": "babel",
+ "identifier": "importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__-238486383",
+ "targetProject": {
+ "mainEntry": "./target-src/match-imports/root-level-imports.js",
+ "name": "importing-target-project",
+ "version": "0.0.2-target-mock",
+ "commitHash": "[not-a-git-root]"
+ },
+ "referenceProject": {
+ "mainEntry": "./index.js",
+ "name": "exporting-ref-project",
+ "version": "1.0.0",
+ "commitHash": "[not-a-git-root]"
+ },
+ "configuration": {
+ "gatherFilesConfig": {},
+ "prefix": null
+ }
+ }
+ },
+ "queryOutput": [
+ {
+ "name": "[default]",
+ "variable": {
+ "from": "[default]",
+ "to": "ExtendedComp",
+ "paths": [
+ {
+ "from": "./index.js",
+ "to": "./target-src/match-subclasses/ExtendedComp.js"
+ },
+ {
+ "from": "./ref-src/core.js",
+ "to": "./target-src/match-subclasses/ExtendedComp.js"
+ },
+ {
+ "from": "exporting-ref-project/index.js",
+ "to": "./target-src/match-subclasses/ExtendedComp.js"
+ },
+ {
+ "from": "exporting-ref-project/ref-src/core.js",
+ "to": "./target-src/match-subclasses/ExtendedComp.js"
+ }
+ ]
+ }
+ },
+ {
+ "name": "RefClass",
+ "variable": {
+ "from": "RefClass",
+ "to": "ExtendedComp",
+ "paths": [
+ {
+ "from": "./index.js",
+ "to": "./target-src/match-subclasses/ExtendedComp.js"
+ },
+ {
+ "from": "./ref-src/core.js",
+ "to": "./target-src/match-subclasses/ExtendedComp.js"
+ },
+ {
+ "from": "exporting-ref-project/index.js",
+ "to": "./target-src/match-subclasses/ExtendedComp.js"
+ },
+ {
+ "from": "exporting-ref-project/ref-src/core.js",
+ "to": "./target-src/match-subclasses/ExtendedComp.js"
+ }
+ ]
+ },
+ "tag": {
+ "from": "ref-component",
+ "to": "extended-comp",
+ "paths": [
+ {
+ "from": "./ref-component.js",
+ "to": "./target-src/find-customelements/multiple.js"
+ },
+ {
+ "from": "exporting-ref-project/ref-component.js",
+ "to": "./target-src/find-customelements/multiple.js"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/match-subclasses.json b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/match-subclasses.json
new file mode 100644
index 000000000..28da97b5e
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks-analyzer-outputs/match-subclasses.json
@@ -0,0 +1,65 @@
+{
+ "meta": {
+ "searchType": "ast-analyzer",
+ "analyzerMeta": {
+ "name": "match-subclasses",
+ "requiredAst": "babel",
+ "identifier": "importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__453069400",
+ "targetProject": {
+ "mainEntry": "./target-src/match-imports/root-level-imports.js",
+ "name": "importing-target-project",
+ "version": "0.0.2-target-mock",
+ "commitHash": "[not-a-git-root]"
+ },
+ "referenceProject": {
+ "mainEntry": "./index.js",
+ "name": "exporting-ref-project",
+ "version": "1.0.0",
+ "commitHash": "[not-a-git-root]"
+ },
+ "configuration": {
+ "gatherFilesConfig": {}
+ }
+ }
+ },
+ "queryOutput": [
+ {
+ "exportSpecifier": {
+ "name": "[default]",
+ "project": "exporting-ref-project",
+ "filePath": "./index.js",
+ "id": "[default]::./index.js::exporting-ref-project"
+ },
+ "matchesPerProject": [
+ {
+ "project": "importing-target-project",
+ "files": [
+ {
+ "file": "./target-src/match-subclasses/ExtendedComp.js",
+ "identifier": "ExtendedComp"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "exportSpecifier": {
+ "name": "RefClass",
+ "project": "exporting-ref-project",
+ "filePath": "./index.js",
+ "id": "RefClass::./index.js::exporting-ref-project"
+ },
+ "matchesPerProject": [
+ {
+ "project": "importing-target-project",
+ "files": [
+ {
+ "file": "./target-src/match-subclasses/ExtendedComp.js",
+ "identifier": "ExtendedComp"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/providence-analytics/test-helpers/project-mocks/README.md b/packages/providence-analytics/test-helpers/project-mocks/README.md
new file mode 100644
index 000000000..c0ecc933e
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/README.md
@@ -0,0 +1,12 @@
+# Project mocks
+
+The number of project-mocks is kept to a minimum:
+
+- one target project: "./importing-target-project"
+- one reference project: "./importing-target-project/node_modules/exporting-ref-project"
+
+Whenever new Analyzers are added, please make sure the needed ingredients for a proper
+end to end test are added to one of the above projects (or both).
+
+Be sure to update 'test-helpers/project-mocks-analyzer-output'.
+This can be done by running `npm run test:e2e -- --generate-e2e-mode` once.
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/.gitignore b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/.gitignore
new file mode 100644
index 000000000..ddf342489
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/.gitignore
@@ -0,0 +1 @@
+!node_modules/
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/index.js b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/index.js
new file mode 100644
index 000000000..011a650a7
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/index.js
@@ -0,0 +1,8 @@
+/* eslint-disable */
+
+// re-exported default specifier
+import refConstImported from './ref-src/core.js';
+
+export default refConstImported;
+// re-exported specifier
+export { RefClass, RefClass as RefRenamedClass } from './ref-src/core.js';
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/not-imported.js b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/not-imported.js
new file mode 100644
index 000000000..acea8e767
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/not-imported.js
@@ -0,0 +1,2 @@
+// this file will not be included by "importing-target-project" defined below
+export const notImported = null;
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/package.json b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/package.json
new file mode 100644
index 000000000..b5249e982
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "exporting-ref-project",
+ "version": "1.0.0",
+ "main": "./index.js"
+}
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/ref-component.js b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/ref-component.js
new file mode 100644
index 000000000..2ea4ca838
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/ref-component.js
@@ -0,0 +1,4 @@
+// global effects
+import { RefClass } from './ref-src/core.js';
+
+customElements.define('ref-component', RefClass);
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/ref-src/core.js b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/ref-src/core.js
new file mode 100644
index 000000000..a9d174b2e
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/ref-src/core.js
@@ -0,0 +1,11 @@
+/* eslint-disable */
+
+// named specifier
+export class RefClass extends HTMLElement {
+ methodToInherit() {}
+};
+
+// default specifier
+export default superclass => class MyMixin extends superclass {
+ mixinMethodToInherit() {}
+};
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/ref-src/folder/index.js b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/ref-src/folder/index.js
new file mode 100644
index 000000000..2845dc7f3
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project/ref-src/folder/index.js
@@ -0,0 +1,3 @@
+// this file (and thus this export) should be resolved via
+// [import 'exporting-ref-project/ref-src/folder']
+export const resolvePathCorrect = null;
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/package.json b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/package.json
new file mode 100644
index 000000000..87a90deec
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "importing-target-project",
+ "version": "0.0.2-target-mock",
+ "main": "./target-src/match-imports/root-level-imports.js",
+ "dependencies": {
+ "exporting-ref-project": "^1.0.0"
+ }
+}
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/find-customelements/multiple.js b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/find-customelements/multiple.js
new file mode 100644
index 000000000..41e533558
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/find-customelements/multiple.js
@@ -0,0 +1,18 @@
+/* eslint-disable max-classes-per-file */
+import { RefClass } from 'exporting-ref-project';
+import { ExtendedComp } from '../match-subclasses/ExtendedComp.js';
+
+// external
+customElements.define('ref-class', RefClass);
+
+// internal (+ via window and inside CallExpression)
+(() => {
+ window.customElements.define('extended-comp', ExtendedComp);
+})();
+
+// direct class (not supported atm)
+// To connect this to a constructor, we should also detect customElements.get()
+customElements.define('on-the-fly', class extends HTMLElement {});
+
+// eslint-disable-next-line no-unused-vars
+class ExtendedOnTheFly extends customElements.get('on-the-fly') {}
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/find-imports/all-notations.js b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/find-imports/all-notations.js
new file mode 100644
index 000000000..b247d0b47
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/find-imports/all-notations.js
@@ -0,0 +1,24 @@
+/* eslint-disable */
+// ImportDeclaration without specifiers
+import 'imported/source';
+// ImportDeclaration with default specifier
+import a from 'imported/source-a';
+// ImportDeclaration with named specifier
+import { b } from 'imported/source-b';
+// ImportDeclaration with multiple named specifiers
+import { c, d } from 'imported/source-c';
+// ImportDeclaration with default and named specifiers
+import e, { f, g } from 'imported/source-d';
+
+// Internal file import
+import '../match-imports/deep-imports'; // Notice extension is missing, will be auto resolved
+
+// Dynamic import
+import('my/source-e');
+
+// Dynamic import with variables. TODO: how to handle?
+const variable = 'f';
+import(`my/source${variable}`);
+
+// namespaced
+import * as all from 'imported/source-g';
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/match-imports/deep-imports.js b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/match-imports/deep-imports.js
new file mode 100644
index 000000000..4cba7f0cc
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/match-imports/deep-imports.js
@@ -0,0 +1,27 @@
+/* eslint-disable */
+
+// a direct named import
+import { RefClass } from 'exporting-ref-project/ref-src/core.js';
+
+// a direct default import
+import refConst from 'exporting-ref-project/ref-src/core.js';
+
+// should not be found
+import { nonMatched } from 'unknown-project/xyz.js';
+
+/**
+ * Examples below should be resolved to the proper filepath (filename + extension)
+ * (direct or indirect is not relevant in this case, it is about the source and not the
+ * specifier)
+ */
+
+// Two things:
+// - a file with side effects
+// - should resolve "as file", to 'exporting-ref-project/ref-component.js'
+import 'exporting-ref-project/ref-component';
+
+// - should resolve "as folder", to 'exporting-ref-project/ref-src/folder/index.js'
+import { resolvePathCorrect } from 'exporting-ref-project/ref-src/folder';
+
+// should match all exportSpecifiers from 'exporting-ref-project/ref-src/core.js'
+import * as all from 'exporting-ref-project/ref-src/core.js';
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/match-imports/root-level-imports.js b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/match-imports/root-level-imports.js
new file mode 100644
index 000000000..7849dbe64
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/match-imports/root-level-imports.js
@@ -0,0 +1,13 @@
+/* eslint-disable */
+
+// named import (indirect, needs transitivity check)
+import { RefClass } from 'exporting-ref-project';
+
+// renamed import (indirect, needs transitivity check)
+import { RefRenamedClass } from 'exporting-ref-project';
+
+// default (indirect, needs transitivity check)
+import refConstImported from 'exporting-ref-project';
+
+// should not be found
+import { nonMatched } from 'unknown-project';
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/match-subclasses/ExtendedComp.js b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/match-subclasses/ExtendedComp.js
new file mode 100644
index 000000000..62cb318b9
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/match-subclasses/ExtendedComp.js
@@ -0,0 +1,46 @@
+/* eslint-disable */
+
+import { RefClass } from 'exporting-ref-project';
+import MyCompMixin from './internalProxy.js';
+
+export class ExtendedComp extends MyCompMixin(RefClass) {
+ /**
+ * Whitelisted members
+ */
+ get getterSetter() {}
+ set getterSetter(v) {}
+ static get staticGetterSetter() {}
+ static set staticGetterSetter(v) {}
+ method() {}
+ _protectedMethod() {}
+ __privateMethod() {}
+ $protectedMethod() {}
+ $$privateMethod() {}
+
+ /**
+ * Blacklisted platform methods ands props by find-classes
+ */
+ static get attributes() {}
+ constructor() {}
+ connectedCallback() {}
+ disconnectedCallback() {}
+ /**
+ * Blacklisted LitElement methods ands props by find-classes
+ */
+ static get properties() {}
+ static get styles() {}
+ get updateComplete() {}
+ _requestUpdate() {}
+ createRenderRoot() {}
+ render() {}
+ updated() {}
+ firstUpdated() {}
+ update() {}
+ shouldUpdate() {}
+ /**
+ * Blacklisted Lion methods and props by find-classes
+ */
+ static get localizeNamespaces() {}
+ get slots() {}
+ onLocaleUpdated() {}
+}
diff --git a/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/match-subclasses/internalProxy.js b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/match-subclasses/internalProxy.js
new file mode 100644
index 000000000..3e351fecd
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/project-mocks/importing-target-project/target-src/match-subclasses/internalProxy.js
@@ -0,0 +1,4 @@
+/* eslint-disable */
+import MyCompMixin from 'exporting-ref-project';
+
+export default MyCompMixin;
diff --git a/packages/providence-analytics/test-helpers/templates/analyzer-template.js b/packages/providence-analytics/test-helpers/templates/analyzer-template.js
new file mode 100644
index 000000000..86f8ffacd
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/templates/analyzer-template.js
@@ -0,0 +1,107 @@
+const { Analyzer } = require('../../src/program/analyzers/helpers/Analyzer.js');
+
+/**
+ * This file outlines the minimum required functionality for an analyzer.
+ * Whenever a new analyzer is created, this file can serve as a guideline on how to do this.
+ * For 'match-analyzers' (having requiresReference: true), please look in the analyzers folder for
+ * an example
+ */
+
+/**
+ * Everything that is configured via {AnalyzerConfig} [customConfig] in the execute
+ * function, should be configured here
+ */
+const options = {
+ optionA(entryResult) {
+ // here, we perform a transformation on the entryResult
+ return entryResult;
+ },
+};
+
+/**
+ * This file takes the output of one AST (or 'program'), which
+ * corresponds to one file.
+ * The contents of this function should be designed in such a way that they
+ * can be directly pasted and edited in https://astexplorer.net/
+ * @param {BabelAST} ast
+ * @returns {TransformedEntry}
+ */
+// eslint-disable-next-line no-unused-vars
+function myAnalyzerPerAstEntry(ast) {
+ // Visit AST...
+ const transformedEntryResult = [];
+ // Do the traverse: https://babeljs.io/docs/en/babel-traverse
+ // Inside of ypur traverse function, add when there is a match wrt intended analysis
+ transformedEntryResult.push({ matched: 'entry' });
+ return transformedEntryResult;
+}
+
+class MyAnalyzer extends Analyzer {
+ constructor() {
+ super();
+ /**
+ * This must match with the name in file-system (will be used for reporting)
+ */
+ this.name = 'my-analyzer';
+ /**
+ * The ast format that the execute function expects
+ * Compatible with formats supported by AstService.getAst()
+ */
+ this.requiredAst = 'babel';
+ /**
+ * Not all analyzers require a references. Those that do, (usually 'match analyzers'),
+ * must explicitly state so with `requiresReference: true`
+ */
+ }
+
+ /**
+ * @param {AstDataProject[]} astDataProjects
+ * @param {AnalyzerConfig} [customConfig]
+ * @returns {QueryResult}
+ */
+ async execute(customConfig = {}) {
+ const cfg = {
+ targetProjectPaths: null,
+ optionA: false,
+ optionB: '',
+ ...customConfig,
+ };
+
+ /**
+ * Prepare
+ */
+ const analyzerResult = this._prepare(cfg);
+ if (analyzerResult) {
+ return analyzerResult;
+ }
+
+ /**
+ * Traverse
+ */
+ const queryOutput = await this._traverse((ast, astContext) => {
+ // Run the traversel per entry
+ let transformedEntryResult = myAnalyzerPerAstEntry(ast);
+ const meta = {};
+
+ // (optional): Post processors on TransformedEntry
+ if (cfg.optionA) {
+ // Run entry transformation based on option A
+ transformedEntryResult = options.optionA(astContext);
+ }
+
+ return { result: transformedEntryResult, meta };
+ });
+
+ // (optional): Post processors on TransformedQueryResult
+ if (cfg.optionB) {
+ // Run your QueryResult transformation based on option B
+ }
+
+ /**
+ * Finalize
+ */
+ return this._finalize(queryOutput, cfg);
+ }
+}
+
+module.exports = MyAnalyzer;
diff --git a/packages/providence-analytics/test-helpers/templates/post-processor-template.js b/packages/providence-analytics/test-helpers/templates/post-processor-template.js
new file mode 100644
index 000000000..5e895afbe
--- /dev/null
+++ b/packages/providence-analytics/test-helpers/templates/post-processor-template.js
@@ -0,0 +1,44 @@
+const /** @type {PostProcessorOptions} */ options = {
+ optionA(transformedResult) {
+ return transformedResult;
+ },
+ };
+
+/**
+ *
+ * @param {AnalyzerResult} analyzerResult
+ * @param {FindImportsConfig} customConfig
+ * @returns {AnalyzerResult}
+ */
+function myPostProcessor(analyzerResult, customConfig) {
+ const cfg = {
+ optionFoo: null,
+ ...customConfig,
+ };
+
+ let transformedResult = analyzerResult.map(({ entries, project }) => {
+ // eslint-disable-next-line no-unused-vars
+ const projectName = project.name;
+ return entries.map(entry =>
+ entry.result.map(resultForEntry => ({
+ transformed: resultForEntry.foo,
+ output: resultForEntry.bar,
+ })),
+ );
+ });
+
+ if (cfg.optionA) {
+ transformedResult = options.optionA(transformedResult);
+ }
+
+ return /** @type {AnalyzerResult} */ transformedResult;
+}
+
+module.exports = {
+ name: 'my-post-processor',
+ execute: myPostProcessor,
+ compatibleAnalyzers: ['analyzer-template'],
+ // This means it transforms the result output of an analyzer, and multiple
+ // post processors cannot be chained after this one
+ modifiesOutputStructure: true,
+};
diff --git a/packages/providence-analytics/test-node/cli/cli.testx.js b/packages/providence-analytics/test-node/cli/cli.testx.js
new file mode 100644
index 000000000..dcf8b2ed1
--- /dev/null
+++ b/packages/providence-analytics/test-node/cli/cli.testx.js
@@ -0,0 +1,98 @@
+const sinon = require('sinon');
+const pathLib = require('path');
+const { expect } = require('chai');
+const {
+ mockProject,
+ // restoreMockedProjects,
+} = require('../../test-helpers/mock-project-helpers.js');
+const {
+ mockWriteToJson,
+ restoreWriteToJson,
+} = require('../../test-helpers/mock-report-service-helpers.js');
+const {
+ suppressNonCriticalLogs,
+ restoreSuppressNonCriticalLogs,
+} = require('../../test-helpers/mock-log-service-helpers.js');
+
+const { spawnProcess } = require('../../src/cli/cli-helpers.js');
+const { QueryService } = require('../../src/program/services/QueryService.js');
+const providenceModule = require('../../src/program/providence.js');
+const dummyAnalyzer = require('../../test-helpers/templates/analyzer-template.js');
+
+const queryResults = [];
+
+describe('Providence CLI', () => {
+ before(() => {
+ suppressNonCriticalLogs();
+ mockWriteToJson(queryResults);
+ });
+
+ after(() => {
+ restoreSuppressNonCriticalLogs();
+ restoreWriteToJson();
+ });
+
+ mockProject(
+ {
+ './src/OriginalComp.js': `export class OriginalComp {}`,
+ './src/inbetween.js': `export { OriginalComp as InBetweenComp } from './OriginalComp.js'`,
+ './index.js': `export { InBetweenComp as MyComp } from './src/inbetween.js'`,
+ },
+ {
+ project: 'example-project',
+ path: '/mocked/path',
+ },
+ );
+
+ const rootDir = pathLib.resolve(__dirname, '../../');
+ async function cli(args) {
+ return spawnProcess(`node ./src/cli/index.js ${args}`, { cwd: rootDir });
+ }
+
+ async function cliAnalyze(args) {
+ return spawnProcess(`node ./src/cli/index.js analyze find-exports ${args}`, { cwd: rootDir });
+ }
+
+ it('creates a QueryConfig', async () => {
+ const stub = sinon.stub(QueryService, 'getQueryConfigFromAnalyzer');
+ await cliAnalyze('-t "/mocked/path/example-project"');
+ expect(stub.args[0]).to.equal('find-exports');
+ });
+
+ it('calls providence', async () => {
+ const providenceStub = sinon.stub(providenceModule, 'providence');
+ await cliAnalyze('-t "/mocked/path/example-project"');
+ expect(providenceStub).to.have.been.called;
+ });
+
+ describe('Global options', () => {
+ it('"-e --extensions"', async () => {
+ const providenceStub = sinon.stub(providenceModule, 'providence');
+ await cli('--extensions ".bla, .blu"');
+ expect(providenceStub.args[1].gatherFilesConfig.extensions).to.eql(['bla', 'blu']);
+ });
+
+ it('"-t", "--search-target-paths"', async () => {});
+ it('"-r", "--reference-paths"', async () => {});
+ it('"--search-target-collection"', async () => {});
+ it('"--reference-collection"', async () => {});
+
+ it.skip('"-R --verbose-report"', async () => {});
+ it.skip('"-D", "--debug"', async () => {});
+ });
+
+ describe('Commands', () => {
+ describe('Analyze', () => {
+ it('calls providence', async () => {
+ expect(typeof dummyAnalyzer.name).to.equal('string');
+ });
+ describe('Options', () => {
+ it('"-o", "--prompt-optional-config"', async () => {});
+ it('"-c", "--config"', async () => {});
+ });
+ });
+ describe('Query', () => {});
+ describe('Search', () => {});
+ describe('Manage', () => {});
+ });
+});
diff --git a/packages/providence-analytics/test-node/program/Analyzer.testx.js b/packages/providence-analytics/test-node/program/Analyzer.testx.js
new file mode 100644
index 000000000..c2b2a2b6d
--- /dev/null
+++ b/packages/providence-analytics/test-node/program/Analyzer.testx.js
@@ -0,0 +1,228 @@
+const { expect } = require('chai');
+const {
+ // mockTargetAndReferenceProject,
+ mockProject,
+ restoreMockedProjects,
+} = require('../../test-helpers/mock-project-helpers.js');
+const {
+ mockWriteToJson,
+ restoreWriteToJson,
+} = require('../../test-helpers/mock-report-service-helpers.js');
+const {
+ suppressNonCriticalLogs,
+ restoreSuppressNonCriticalLogs,
+} = require('../../test-helpers/mock-log-service-helpers.js');
+
+const { QueryService } = require('../../src/program/services/QueryService.js');
+const { providence } = require('../../src/program/providence.js');
+const dummyAnalyzer = require('../../test-helpers/templates/analyzer-template.js');
+
+const queryResults = [];
+
+describe('Analyzer', () => {
+ before(() => {
+ suppressNonCriticalLogs();
+ mockWriteToJson(queryResults);
+ });
+
+ after(() => {
+ restoreSuppressNonCriticalLogs();
+ restoreWriteToJson(queryResults);
+ });
+
+ describe('Public api', () => {
+ it('has a "name" string', async () => {
+ expect(typeof dummyAnalyzer.name).to.equal('string');
+ });
+
+ it('has an "execute" function', async () => {
+ expect(typeof dummyAnalyzer.execute).to.equal('function');
+ });
+
+ it('has a "requiredAst" string', async () => {
+ expect(typeof dummyAnalyzer.requiredAst).to.equal('string');
+ const allowedAsts = ['babel', 'typescript', 'es-module-lexer'];
+ expect(allowedAsts).to.include(dummyAnalyzer.requiredAst);
+ });
+
+ it('has a "requiresReference" boolean', async () => {
+ expect(typeof dummyAnalyzer.requiresReference).to.equal('boolean');
+ });
+ });
+
+ describe('Find Analyzers', async () => {
+ afterEach(() => {
+ restoreMockedProjects();
+ });
+
+ // Our configuration object
+ const myQueryConfigObject = QueryService.getQueryConfigFromAnalyzer(dummyAnalyzer);
+ mockProject([`const validJs = true;`, `let invalidJs = false;`], {
+ projectName: 'my-project',
+ projectPath: '/path/to/my-project',
+ filePaths: ['./test-file1.js', './test-file2.js'],
+ });
+
+ await providence(myQueryConfigObject, {
+ targetProjectPaths: ['/path/to/my-project'],
+ });
+
+ describe('Prepare phase', () => {
+ it('looks for a cached result', async () => {});
+
+ it('exposes a ".targetMeta" object', async () => {});
+
+ it('exposes a ".targetData" object', async () => {});
+
+ it('exposes a ".identifier" string', async () => {});
+ });
+
+ describe('Traverse phase', () => {});
+
+ describe('Finalize phase', () => {
+ it('returns an AnalyzerResult', async () => {
+ const queryResult = queryResults[0];
+ const { queryOutput, meta } = queryResult;
+
+ expect(queryOutput[0]).to.eql({
+ file: './test-file1.js',
+ meta: {},
+ result: [{ matched: 'entry' }],
+ });
+ expect(queryOutput[1]).to.eql({
+ file: './test-file2.js',
+ meta: {},
+ result: [{ matched: 'entry' }],
+ });
+ // Local machine info needs to be deleted, so that results are always 'machine agnostic'
+ // (which is needed to share cached json results via git)
+ expect(meta).to.eql({
+ searchType: 'ast-analyzer',
+ analyzerMeta: {
+ name: 'my-analyzer',
+ requiredAst: 'babel',
+ identifier: 'my-project_0.1.0-mock__542516121',
+ targetProject: {
+ name: 'my-project',
+ commitHash: '[not-a-git-repo]',
+ version: '0.1.0-mock',
+ },
+ configuration: {
+ targetProjectPaths: null,
+ optionA: false,
+ optionB: '',
+ debugEnabled: false,
+ gatherFilesConfig: {},
+ },
+ },
+ });
+ });
+ });
+
+ // TODO: think of exposing the ast traversal part in a distinct method "traverse", so we can
+ // create integrations with (a local version of) https://astexplorer.net
+ });
+
+ // describe.skip('Match Analyzers', () => {
+ // const referenceProject = {
+ // path: '/exporting/ref/project',
+ // name: 'exporting-ref-project',
+ // files: [
+ // {
+ // file: './package.json',
+ // code: `{
+ // "name": "importing-target-project",
+ // "version": "2.20.3",
+ // "dependencies": {
+ // "exporting-ref-project": "^2.3.0"
+ // }
+ // }`,
+ // },
+ // ],
+ // };
+
+ // const matchingTargetProject = {
+ // path: '/importing/target/project/v10',
+ // files: [
+ // {
+ // file: './package.json',
+ // code: `{
+ // "name": "importing-target-project",
+ // "version": "10.1.2",
+ // "dependencies": {
+ // "exporting-ref-project": "^2.3.0"
+ // }
+ // }`,
+ // },
+ // ],
+ // };
+
+ // const matchingDevDepTargetProject = {
+ // path: '/importing/target/project/v10',
+ // files: [
+ // {
+ // file: './package.json',
+ // code: `{
+ // "name": "importing-target-project",
+ // "version": "10.1.2",
+ // "devDependencies": {
+ // "exporting-ref-project": "^2.3.0"
+ // }
+ // }`,
+ // },
+ // ],
+ // };
+
+ // // A previous version that does not match our reference version
+ // const nonMatchingVersionTargetProject = {
+ // path: '/importing/target/project/v8',
+ // files: [
+ // {
+ // file: './package.json',
+ // code: `{
+ // "name": "importing-target-project",
+ // "version": "8.1.2",
+ // "dependencies": {
+ // "exporting-ref-project": "^1.9.0"
+ // }
+ // }`,
+ // },
+ // ],
+ // };
+
+ // const nonMatchingDepTargetProject = {
+ // path: '/importing/target/project/v8',
+ // files: [
+ // {
+ // file: './package.json',
+ // code: `{
+ // "name": "importing-target-project",
+ // "version": "8.1.2",
+ // "dependencies": {
+ // "some-other-project": "^0.1.0"
+ // }
+ // }`,
+ // },
+ // ],
+ // };
+
+ // it('has a "requiresReference" boolean', async () => {
+ // expect(dummyAnalyzer.requiresReference).to.equal(true);
+ // });
+
+ // describe('Prepare phase', () => {
+ // it('halts non-compatible reference + target combinations', async () => {
+ // mockTargetAndReferenceProject(referenceProject, nonMatchingVersionTargetProject);
+ // // Check stubbed LogService.info with reason 'no-matched-version'
+ // mockTargetAndReferenceProject(referenceProject, nonMatchingDepTargetProject);
+ // // Check stubbed LogService.info with reason 'no-dependency'
+ // });
+
+ // it('starts analysis for compatible reference + target combinations', async () => {
+ // mockTargetAndReferenceProject(referenceProject, matchingTargetProject);
+ // mockTargetAndReferenceProject(referenceProject, matchingDevDepTargetProject);
+ // // _prepare: startAnalysis: true
+ // });
+ // });
+ // });
+});
diff --git a/packages/providence-analytics/test-node/program/analyzers/e2e/all-analyzers.e2e.js b/packages/providence-analytics/test-node/program/analyzers/e2e/all-analyzers.e2e.js
new file mode 100644
index 000000000..ff61c3cfd
--- /dev/null
+++ b/packages/providence-analytics/test-node/program/analyzers/e2e/all-analyzers.e2e.js
@@ -0,0 +1,130 @@
+const pathLib = require('path');
+const { expect } = require('chai');
+const { providence } = require('../../../../src/program/providence.js');
+const { QueryService } = require('../../../../src/program/services/QueryService.js');
+const { ReportService } = require('../../../../src/program/services/ReportService.js');
+const { LogService } = require('../../../../src/program/services/LogService.js');
+
+const {
+ mockWriteToJson,
+ restoreWriteToJson,
+} = require('../../../../test-helpers/mock-report-service-helpers.js');
+const {
+ suppressNonCriticalLogs,
+ restoreSuppressNonCriticalLogs,
+} = require('../../../../test-helpers/mock-log-service-helpers.js');
+
+describe('Analyzers file-system integration', () => {
+ before(() => {
+ suppressNonCriticalLogs();
+ });
+
+ after(() => {
+ restoreSuppressNonCriticalLogs();
+ });
+
+ const generateE2eMode = process.argv.includes('--generate-e2e-mode');
+
+ const queryResults = [];
+ const targetPath = pathLib.resolve(
+ __dirname,
+ '../../../../test-helpers/project-mocks/importing-target-project',
+ );
+ const referencePath = pathLib.resolve(
+ __dirname,
+ `../../../../test-helpers/project-mocks/importing-target-project/node_modules/exporting-ref-project`,
+ );
+
+ const originalGetResultFileNameAndPath = ReportService._getResultFileNameAndPath;
+ const originalOutputPath = ReportService.outputPath;
+
+ after(() => {
+ ReportService._getResultFileNameAndPath = originalGetResultFileNameAndPath;
+ ReportService.outputPath = originalOutputPath;
+ });
+
+ if (generateE2eMode) {
+ ReportService.outputPath = pathLib.resolve(
+ __dirname,
+ '../../../../test-helpers/project-mocks-analyzer-outputs',
+ );
+ // eslint-disable-next-line func-names
+ ReportService._getResultFileNameAndPath = function (name) {
+ return pathLib.join(this.outputPath, `${name}.json`);
+ };
+ } else {
+ ReportService.outputPath = __dirname; // prevents cache to fail the test
+
+ beforeEach(() => {
+ mockWriteToJson(queryResults);
+ });
+
+ afterEach(() => {
+ restoreWriteToJson(queryResults);
+ });
+ }
+ const analyzers = [
+ {
+ analyzerName: 'find-customelements',
+ providenceConfig: {
+ targetProjectPaths: [targetPath],
+ },
+ },
+ {
+ analyzerName: 'find-imports',
+ providenceConfig: {
+ targetProjectPaths: [targetPath],
+ },
+ },
+ {
+ analyzerName: 'find-exports',
+ providenceConfig: {
+ targetProjectPaths: [referencePath],
+ },
+ },
+ {
+ analyzerName: 'find-classes',
+ providenceConfig: {
+ targetProjectPaths: [targetPath],
+ },
+ },
+ {
+ analyzerName: 'match-imports',
+ providenceConfig: {
+ targetProjectPaths: [targetPath],
+ referenceProjectPaths: [referencePath],
+ },
+ },
+ {
+ analyzerName: 'match-subclasses',
+ providenceConfig: {
+ targetProjectPaths: [targetPath],
+ referenceProjectPaths: [referencePath],
+ },
+ },
+ {
+ analyzerName: 'match-paths',
+ providenceConfig: {
+ targetProjectPaths: [targetPath],
+ referenceProjectPaths: [referencePath],
+ },
+ },
+ ];
+
+ for (const { analyzerName, providenceConfig } of analyzers) {
+ it(`"${analyzerName}" analyzer`, async () => {
+ const findExportsQueryConfig = QueryService.getQueryConfigFromAnalyzer(analyzerName);
+ await providence(findExportsQueryConfig, providenceConfig);
+ if (generateE2eMode) {
+ LogService.info(
+ 'Successfully created mocks. Do not forget to rerun tests now without "--generate-e2e-mode"',
+ );
+ return;
+ }
+ // eslint-disable-next-line import/no-dynamic-require, global-require
+ const expectedOutput = require(`../../../../test-helpers/project-mocks-analyzer-outputs/${analyzerName}.json`);
+ const queryResult = JSON.parse(JSON.stringify(queryResults[0])).queryOutput;
+ expect(queryResult).to.eql(expectedOutput.queryOutput);
+ });
+ }
+});
diff --git a/packages/providence-analytics/test-node/program/analyzers/find-classes.test.js b/packages/providence-analytics/test-node/program/analyzers/find-classes.test.js
new file mode 100644
index 000000000..f50ac9415
--- /dev/null
+++ b/packages/providence-analytics/test-node/program/analyzers/find-classes.test.js
@@ -0,0 +1,247 @@
+const { expect } = require('chai');
+const { providence } = require('../../../src/program/providence.js');
+const { QueryService } = require('../../../src/program/services/QueryService.js');
+const {
+ mockProject,
+ restoreMockedProjects,
+ getEntry,
+} = require('../../../test-helpers/mock-project-helpers.js');
+const {
+ mockWriteToJson,
+ restoreWriteToJson,
+} = require('../../../test-helpers/mock-report-service-helpers.js');
+const {
+ suppressNonCriticalLogs,
+ restoreSuppressNonCriticalLogs,
+} = require('../../../test-helpers/mock-log-service-helpers.js');
+
+const findClassesQueryConfig = QueryService.getQueryConfigFromAnalyzer('find-classes');
+
+describe('Analyzer "find-classes"', () => {
+ const queryResults = [];
+ const _providenceCfg = {
+ targetProjectPaths: ['/fictional/project'], // defined in mockProject
+ };
+
+ const cacheDisabledInitialValue = QueryService.cacheDisabled;
+
+ before(() => {
+ QueryService.cacheDisabled = true;
+ });
+
+ after(() => {
+ QueryService.cacheDisabled = cacheDisabledInitialValue;
+ });
+
+ beforeEach(() => {
+ suppressNonCriticalLogs();
+ mockWriteToJson(queryResults);
+ });
+
+ afterEach(() => {
+ restoreSuppressNonCriticalLogs();
+ restoreWriteToJson(queryResults);
+ restoreMockedProjects();
+ });
+
+ it(`finds class definitions`, async () => {
+ mockProject([`class EmptyClass {}`]);
+ await providence(findClassesQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result).to.eql([
+ {
+ name: 'EmptyClass',
+ isMixin: false,
+ members: {
+ methods: [],
+ props: [],
+ },
+ },
+ ]);
+ });
+
+ it(`finds mixin definitions`, async () => {
+ mockProject([`const m = superclass => class MyMixin extends superclass {}`]);
+ await providence(findClassesQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result).to.eql([
+ {
+ name: 'MyMixin',
+ superClasses: [
+ {
+ isMixin: false,
+ name: 'superclass',
+ rootFile: { file: '[current]', specifier: 'superclass' },
+ },
+ ],
+ isMixin: true,
+ members: {
+ methods: [],
+ props: [],
+ },
+ },
+ ]);
+ });
+
+ it(`stores superClasses`, async () => {
+ mockProject({
+ './index.js': `
+ import { Mixin } from '@external/source';
+
+ class OtherClass {}
+ export class EmptyClass extends Mixin(OtherClass) {}
+ `,
+ './internal.js': '',
+ });
+ await providence(findClassesQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[1].superClasses).to.eql([
+ {
+ isMixin: true,
+ name: 'Mixin',
+ rootFile: { file: '@external/source', specifier: 'Mixin' },
+ },
+ {
+ isMixin: false,
+ name: 'OtherClass',
+ rootFile: { file: '[current]', specifier: 'OtherClass' },
+ },
+ ]);
+ });
+
+ it(`handles multiple classes per file`, async () => {
+ mockProject([
+ ` const m = superclass => class MyMixin extends superclass {}
+ class EmptyClass extends Mixin(OtherClass) {}`,
+ ]);
+ await providence(findClassesQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result.length).to.equal(2);
+ });
+
+ describe('Members', () => {
+ it(`stores methods`, async () => {
+ mockProject([
+ `class MyClass {
+ method() {}
+ _protectedMethod() {}
+ __privateMethod() {}
+ $protectedMethod() {}
+ $$privateMethod() {}
+ }`,
+ ]);
+ await providence(findClassesQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].members.methods).to.eql([
+ {
+ accessType: 'public',
+ name: 'method',
+ },
+ {
+ accessType: 'protected',
+ name: '_protectedMethod',
+ },
+ {
+ accessType: 'private',
+ name: '__privateMethod',
+ },
+ {
+ accessType: 'protected',
+ name: '$protectedMethod',
+ },
+ {
+ accessType: 'private',
+ name: '$$privateMethod',
+ },
+ ]);
+ });
+
+ it(`stores props`, async () => {
+ mockProject([
+ `class MyClass {
+ get getterSetter() {}
+ set getterSetter(v) {}
+
+ static get _staticGetterSetter() {}
+ static set _staticGetterSetter(v) {}
+ }`,
+ ]);
+ await providence(findClassesQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].members.props).to.eql([
+ {
+ accessType: 'public',
+ kind: ['get', 'set'],
+ name: 'getterSetter',
+ },
+ {
+ accessType: 'protected',
+ kind: ['get', 'set'],
+ name: '_staticGetterSetter',
+ static: true,
+ },
+ ]);
+ });
+
+ // Options below are disabled by default for now.
+ // TODO: provide as options
+ it.skip(`filters out platform members`, async () => {
+ mockProject([
+ `class MyClass {
+ static get attributes() {}
+ constructor() {}
+ connectedCallback() {}
+ disconnectedCallback() {}
+ }`,
+ ]);
+ await providence(findClassesQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].members.methods.length).to.equal(0);
+ expect(firstEntry.result[0].members.props.length).to.equal(0);
+ });
+
+ it.skip(`filters out LitElement members`, async () => {
+ mockProject([
+ `class MyClass {
+ static get properties() {}
+ static get styles() {}
+ get updateComplete() {}
+ _requestUpdate() {}
+ createRenderRoot() {}
+ render() {}
+ updated() {}
+ firstUpdated() {}
+ update() {}
+ shouldUpdate() {}
+ }`,
+ ]);
+ await providence(findClassesQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].members.methods.length).to.equal(0);
+ expect(firstEntry.result[0].members.props.length).to.equal(0);
+ });
+
+ it.skip(`filters out Lion members`, async () => {
+ mockProject([
+ `class MyClass {
+ static get localizeNamespaces() {}
+ get slots() {}
+ onLocaleUpdated() {}
+ }`,
+ ]);
+ await providence(findClassesQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].members.methods.length).to.equal(0);
+ expect(firstEntry.result[0].members.props.length).to.equal(0);
+ });
+ });
+});
diff --git a/packages/providence-analytics/test-node/program/analyzers/find-customelements.test.js b/packages/providence-analytics/test-node/program/analyzers/find-customelements.test.js
new file mode 100644
index 000000000..66ae3b0ca
--- /dev/null
+++ b/packages/providence-analytics/test-node/program/analyzers/find-customelements.test.js
@@ -0,0 +1,141 @@
+const { expect } = require('chai');
+const { providence } = require('../../../src/program/providence.js');
+const { QueryService } = require('../../../src/program/services/QueryService.js');
+const {
+ mockProject,
+ restoreMockedProjects,
+ getEntry,
+} = require('../../../test-helpers/mock-project-helpers.js');
+const {
+ mockWriteToJson,
+ restoreWriteToJson,
+} = require('../../../test-helpers/mock-report-service-helpers.js');
+const {
+ suppressNonCriticalLogs,
+ restoreSuppressNonCriticalLogs,
+} = require('../../../test-helpers/mock-log-service-helpers.js');
+
+const findCustomelementsQueryConfig = QueryService.getQueryConfigFromAnalyzer(
+ 'find-customelements',
+);
+const _providenceCfg = {
+ targetProjectPaths: ['/fictional/project'], // defined in mockProject
+};
+
+describe('Analyzer "find-customelements"', () => {
+ const queryResults = [];
+
+ const cacheDisabledInitialValue = QueryService.cacheDisabled;
+
+ before(() => {
+ QueryService.cacheDisabled = true;
+ });
+
+ after(() => {
+ QueryService.cacheDisabled = cacheDisabledInitialValue;
+ });
+
+ beforeEach(() => {
+ suppressNonCriticalLogs();
+ mockWriteToJson(queryResults);
+ });
+
+ afterEach(() => {
+ restoreSuppressNonCriticalLogs();
+ restoreMockedProjects();
+ restoreWriteToJson(queryResults);
+ });
+
+ it(`stores the tagName of a custom element`, async () => {
+ mockProject([`customElements.define('custom-el', class extends HTMLElement {});`]);
+ await providence(findCustomelementsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].tagName).to.equal('custom-el');
+ });
+
+ it(`allows different notations for defining a custom element`, async () => {
+ mockProject([
+ `customElements.define('custom-el1', class extends HTMLElement {});`,
+ `window.customElements.define('custom-el2', class extends HTMLElement {});`,
+ `(() => {
+ window.customElements.define('custom-el3', class extends HTMLElement {});
+ })();`,
+ ]);
+ await providence(findCustomelementsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ const secondEntry = getEntry(queryResult, 1);
+ const thirdEntry = getEntry(queryResult, 2);
+ expect(firstEntry.result[0].tagName).to.equal('custom-el1');
+ expect(secondEntry.result[0].tagName).to.equal('custom-el2');
+ expect(thirdEntry.result[0].tagName).to.equal('custom-el3');
+ });
+
+ it(`stores the rootFile of a custom element`, async () => {
+ mockProject({
+ './src/CustomEl.js': `export class CustomEl extends HTMLElement {}`,
+ './custom-el.js': `
+ import { CustomEl } from './src/CustomEl.js';
+ customElements.define('custom-el', CustomEl);
+ `,
+ });
+ await providence(findCustomelementsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].rootFile).to.eql({
+ file: './src/CustomEl.js',
+ specifier: 'CustomEl',
+ });
+ });
+
+ it(`stores "[inline]" constructors`, async () => {
+ mockProject([`customElements.define('custom-el', class extends HTMLElement {});`]);
+ await providence(findCustomelementsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].constructorIdentifier).to.equal('[inline]');
+ expect(firstEntry.result[0].rootFile.specifier).to.equal('[inline]');
+ });
+
+ it(`stores "[current]" rootFile`, async () => {
+ mockProject([`customElements.define('custom-el', class extends HTMLElement {});`]);
+ await providence(findCustomelementsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].rootFile.file).to.equal('[current]');
+ });
+
+ it(`stores the locally exported specifier in the rootFile `, async () => {
+ mockProject({
+ './src/CustomEl.js': `export class CustomEl extends HTMLElement {}`,
+ './custom-el.js': `
+ import { CustomEl } from './src/CustomEl.js';
+ customElements.define('custom-el', CustomEl);
+ `,
+ });
+ await providence(findCustomelementsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].constructorIdentifier).to.equal('CustomEl');
+ expect(firstEntry.result[0].rootFile.specifier).to.equal('CustomEl');
+ });
+
+ it(`finds all occurrences of custom elements`, async () => {
+ mockProject([
+ `
+ customElements.define('tag-1', class extends HTMLElement {});
+ customElements.define('tag-2', class extends HTMLElement {});
+ `,
+ `
+ customElements.define('tag-3', class extends HTMLElement {});
+ `,
+ ]);
+ await providence(findCustomelementsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ const secondEntry = getEntry(queryResult, 1);
+ expect(firstEntry.result.length).to.equal(2);
+ expect(secondEntry.result.length).to.equal(1);
+ });
+});
diff --git a/packages/providence-analytics/test-node/program/analyzers/find-exports.test.js b/packages/providence-analytics/test-node/program/analyzers/find-exports.test.js
new file mode 100644
index 000000000..98f9be9f5
--- /dev/null
+++ b/packages/providence-analytics/test-node/program/analyzers/find-exports.test.js
@@ -0,0 +1,254 @@
+const { expect } = require('chai');
+const { providence } = require('../../../src/program/providence.js');
+const { QueryService } = require('../../../src/program/services/QueryService.js');
+const {
+ mockProject,
+ restoreMockedProjects,
+ getEntry,
+ getEntries,
+} = require('../../../test-helpers/mock-project-helpers.js');
+const {
+ mockWriteToJson,
+ restoreWriteToJson,
+} = require('../../../test-helpers/mock-report-service-helpers.js');
+const {
+ suppressNonCriticalLogs,
+ restoreSuppressNonCriticalLogs,
+} = require('../../../test-helpers/mock-log-service-helpers.js');
+
+const findExportsQueryConfig = QueryService.getQueryConfigFromAnalyzer('find-exports');
+
+describe('Analyzer "find-exports"', () => {
+ const queryResults = [];
+ const _providenceCfg = {
+ targetProjectPaths: ['/fictional/project'], // defined in mockProject
+ };
+
+ const cacheDisabledInitialValue = QueryService.cacheDisabled;
+
+ before(() => {
+ QueryService.cacheDisabled = true;
+ });
+
+ after(() => {
+ QueryService.cacheDisabled = cacheDisabledInitialValue;
+ });
+
+ beforeEach(() => {
+ suppressNonCriticalLogs();
+ mockWriteToJson(queryResults);
+ });
+
+ afterEach(() => {
+ restoreSuppressNonCriticalLogs();
+ restoreWriteToJson(queryResults);
+ restoreMockedProjects();
+ });
+
+ describe('Export notations', () => {
+ it(`supports [export const x = 0] (named specifier)`, async () => {
+ mockProject([`export const x = 0`]);
+ await providence(findExportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
+ expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('x');
+ expect(firstEntry.result[0].source).to.be.null;
+ });
+
+ it(`supports [export default class X {}] (default export)`, async () => {
+ mockProject([`export default class X {}`]);
+ await providence(findExportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
+ expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('[default]');
+ expect(firstEntry.result[0].source).to.equal(undefined);
+ });
+
+ it(`supports [export { x } from 'my/source'] (re-export named specifier)`, async () => {
+ mockProject([`export { x } from 'my/source'`]);
+ await providence(findExportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
+ expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('x');
+ expect(firstEntry.result[0].source).to.equal('my/source');
+ });
+
+ it(`supports [export { x as y } from 'my/source'] (re-export renamed specifier)`, async () => {
+ mockProject([`export { x as y } from 'my/source'`]);
+ await providence(findExportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
+ expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('y');
+ expect(firstEntry.result[0].source).to.equal('my/source');
+ });
+
+ it(`stores meta info(local name) of renamed specifiers`, async () => {
+ mockProject([`export { x as y } from 'my/source'`]);
+ await providence(findExportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ // This info will be relevant later to identify 'transitive' relations
+ expect(firstEntry.result[0].localMap).to.eql([
+ {
+ local: 'x',
+ exported: 'y',
+ },
+ ]);
+ });
+
+ it(`supports [export { x, y } from 'my/source'] (multiple re-exported named specifiers)`, async () => {
+ mockProject([`export { x, y } from 'my/source'`]);
+ await providence(findExportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].exportSpecifiers.length).to.equal(2);
+ expect(firstEntry.result[0].exportSpecifiers).to.eql(['x', 'y']);
+ expect(firstEntry.result[0].source).to.equal('my/source');
+ });
+
+ it(`stores rootFileMap of an exported Identifier`, async () => {
+ mockProject({
+ './src/OriginalComp.js': `export class OriginalComp {}`,
+ './src/inbetween.js': `export { OriginalComp as InBetweenComp } from './OriginalComp.js'`,
+ './index.js': `export { InBetweenComp as MyComp } from './src/inbetween.js'`,
+ });
+ await providence(findExportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+
+ const firstEntry = getEntry(queryResult);
+ const secondEntry = getEntry(queryResult, 1);
+ const thirdEntry = getEntry(queryResult, 2);
+
+ expect(firstEntry.result[0].rootFileMap).to.eql([
+ {
+ currentFileSpecifier: 'MyComp', // this is the local name in the file we track from
+ rootFile: {
+ file: './src/OriginalComp.js', // the file containing declaration
+ specifier: 'OriginalComp', // the specifier that was exported in file
+ },
+ },
+ ]);
+ expect(secondEntry.result[0].rootFileMap).to.eql([
+ {
+ currentFileSpecifier: 'InBetweenComp',
+ rootFile: {
+ file: './src/OriginalComp.js',
+ specifier: 'OriginalComp',
+ },
+ },
+ ]);
+ expect(thirdEntry.result[0].rootFileMap).to.eql([
+ {
+ currentFileSpecifier: 'OriginalComp',
+ rootFile: {
+ file: '[current]',
+ specifier: 'OriginalComp',
+ },
+ },
+ ]);
+ });
+
+ // TODO: myabe in the future: This experimental syntax requires enabling the parser plugin: 'exportDefaultFrom'
+ it.skip(`stores rootFileMap of an exported Identifier`, async () => {
+ mockProject({
+ './src/reexport.js': `
+ // a direct default import
+ import RefDefault from 'exporting-ref-project';
+
+ export RefDefault;
+ `,
+ './index.js': `
+ export { ExtendRefDefault } from './src/reexport.js';
+ `,
+ });
+ await providence(findExportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+
+ expect(firstEntry.result[0].rootFileMap).to.eql([
+ {
+ currentFileSpecifier: 'ExtendRefDefault',
+ rootFile: {
+ file: 'exporting-ref-project',
+ specifier: '[default]',
+ },
+ },
+ ]);
+ });
+ });
+
+ describe('Export variable types', () => {
+ it(`classes`, async () => {
+ mockProject([`export class X {}`]);
+ await providence(findExportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
+ expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('X');
+ expect(firstEntry.result[0].source).to.be.null;
+ });
+
+ it(`functions`, async () => {
+ mockProject([`export function y() {}`]);
+ await providence(findExportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
+ expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('y');
+ expect(firstEntry.result[0].source).to.be.null;
+ });
+
+ // ...etc?
+ // ...TODO: create custom hooks to store meta info about types etc.
+ });
+
+ describe('Default post processing', () => {
+ // onlyInternalSources: false,
+ // keepOriginalSourcePaths: false,
+ // filterSpecifier: null,
+ });
+
+ describe('Options', () => {
+ // TODO: Move to dashboard
+ it.skip(`"metaConfig.categoryConfig"`, async () => {
+ mockProject(
+ [
+ `export const foo = null`, // firstEntry
+ `export const bar = null`, // secondEntry
+ `export const baz = null`, // thirdEntry
+ ],
+ {
+ projectName: 'my-project',
+ filePaths: ['./foo.js', './packages/bar/test/bar.test.js', './temp/baz.js'],
+ },
+ );
+
+ const findExportsCategoryQueryObj = QueryService.getQueryConfigFromAnalyzer('find-exports', {
+ metaConfig: {
+ categoryConfig: [
+ {
+ project: 'my-project',
+ categories: {
+ fooCategory: localFilePath => localFilePath.startsWith('./foo'),
+ barCategory: localFilePath => localFilePath.startsWith('./packages/bar'),
+ testCategory: localFilePath => localFilePath.includes('/test/'),
+ },
+ },
+ ],
+ },
+ });
+
+ await providence(findExportsCategoryQueryObj, _providenceCfg);
+ const queryResult = queryResults[0];
+ const [firstEntry, secondEntry, thirdEntry] = getEntries(queryResult);
+ expect(firstEntry.meta.categories).to.eql(['fooCategory']);
+ // not mutually exclusive...
+ expect(secondEntry.meta.categories).to.eql(['barCategory', 'testCategory']);
+ expect(thirdEntry.meta.categories).to.eql([]);
+ });
+ });
+});
diff --git a/packages/providence-analytics/test-node/program/analyzers/find-imports.test.js b/packages/providence-analytics/test-node/program/analyzers/find-imports.test.js
new file mode 100644
index 000000000..c696beb72
--- /dev/null
+++ b/packages/providence-analytics/test-node/program/analyzers/find-imports.test.js
@@ -0,0 +1,347 @@
+const { expect } = require('chai');
+const { providence } = require('../../../src/program/providence.js');
+const { QueryService } = require('../../../src/program/services/QueryService.js');
+const {
+ mockProject,
+ restoreMockedProjects,
+ getEntry,
+} = require('../../../test-helpers/mock-project-helpers.js');
+const {
+ mockWriteToJson,
+ restoreWriteToJson,
+} = require('../../../test-helpers/mock-report-service-helpers.js');
+const {
+ suppressNonCriticalLogs,
+ restoreSuppressNonCriticalLogs,
+} = require('../../../test-helpers/mock-log-service-helpers.js');
+
+const findImportsQueryConfig = QueryService.getQueryConfigFromAnalyzer('find-imports');
+const _providenceCfg = {
+ targetProjectPaths: ['/fictional/project'], // defined in mockProject
+};
+
+describe('Analyzer "find-imports"', () => {
+ const queryResults = [];
+
+ const cacheDisabledInitialValue = QueryService.cacheDisabled;
+
+ before(() => {
+ QueryService.cacheDisabled = true;
+ });
+
+ after(() => {
+ QueryService.cacheDisabled = cacheDisabledInitialValue;
+ });
+
+ beforeEach(() => {
+ suppressNonCriticalLogs();
+ mockWriteToJson(queryResults);
+ });
+
+ afterEach(() => {
+ restoreSuppressNonCriticalLogs();
+ restoreMockedProjects();
+ restoreWriteToJson(queryResults);
+ });
+
+ describe('Import notations', () => {
+ it(`supports [import 'imported/source'] (no specifiers)`, async () => {
+ mockProject([`import 'imported/source'`]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers).to.eql(['[file]']);
+ expect(firstEntry.result[0].source).to.equal('imported/source');
+ });
+
+ it(`supports [import x from 'imported/source'] (default specifier)`, async () => {
+ mockProject([`import x from 'imported/source'`]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers[0]).to.equal('[default]');
+ expect(firstEntry.result[0].source).to.equal('imported/source');
+ });
+
+ it(`supports [import { x } from 'imported/source'] (named specifier)`, async () => {
+ mockProject([`import { x } from 'imported/source'`]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers[0]).to.equal('x');
+ expect(firstEntry.result[0].importSpecifiers[1]).to.equal(undefined);
+ expect(firstEntry.result[0].source).to.equal('imported/source');
+ });
+
+ it(`supports [import { x, y } from 'imported/source'] (multiple named specifiers)`, async () => {
+ mockProject([`import { x, y } from 'imported/source'`]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers[0]).to.equal('x');
+ expect(firstEntry.result[0].importSpecifiers[1]).to.equal('y');
+ expect(firstEntry.result[0].importSpecifiers[2]).to.equal(undefined);
+ expect(firstEntry.result[0].source).to.equal('imported/source');
+ });
+
+ it(`supports [import x, { y, z } from 'imported/source'] (default and named specifiers)`, async () => {
+ mockProject([`import x, { y, z } from 'imported/source'`]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers[0]).to.equal('[default]');
+ expect(firstEntry.result[0].importSpecifiers[1]).to.equal('y');
+ expect(firstEntry.result[0].importSpecifiers[2]).to.equal('z');
+ expect(firstEntry.result[0].source).to.equal('imported/source');
+ });
+
+ it(`supports [import { x as y } from 'imported/source'] (renamed specifiers)`, async () => {
+ mockProject([`import { x as y } from 'imported/source'`]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers[0]).to.equal('x');
+ });
+
+ it(`supports [import * as all from 'imported/source'] (namespace specifiers)`, async () => {
+ mockProject([`import * as all from 'imported/source'`]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers[0]).to.equal('[*]');
+ });
+
+ describe('Reexports', () => {
+ it(`supports [export { x } from 'imported/source'] (reexported named specifiers)`, async () => {
+ mockProject([`export { x } from 'imported/source'`]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers[0]).to.equal('x');
+ });
+
+ it(`supports [export { x as y } from 'imported/source'] (reexported renamed specifiers)`, async () => {
+ mockProject([`export { x as y } from 'imported/source'`]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers[0]).to.equal('x');
+ });
+
+ // maybe in the future... needs experimental babel flag "exportDefaultFrom"
+ it.skip(`supports [export x from 'imported/source'] (reexported default specifiers)`, async () => {
+ mockProject([`export x from 'imported/source'`]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers[0]).to.equal('x');
+ });
+
+ it(`supports [export * as x from 'imported/source'] (reexported namespace specifiers)`, async () => {
+ mockProject([`export * as x from 'imported/source'`]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers[0]).to.equal('[*]');
+ });
+ });
+
+ // Currently only supported for find-exports. For now not needed...
+ it.skip(`stores meta info(local name) of renamed specifiers`, async () => {
+ mockProject([`import { x as y } from 'imported/source'`]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ // This info will be relevant later to identify transitive relations
+ expect(firstEntry.result[0].localMap[0]).to.eql({
+ local: 'y',
+ imported: 'x',
+ });
+ });
+
+ it(`supports [import('my/source')] (dynamic imports)`, async () => {
+ mockProject([`import('my/source')`]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers[0]).to.equal('[default]');
+ // TODO: somehow mark as dynamic??
+ expect(firstEntry.result[0].source).to.equal('my/source');
+ });
+
+ it(`supports [import(pathReference)] (dynamic imports with variable source)`, async () => {
+ mockProject([
+ `
+ const pathReference = 'my/source';
+ import(pathReference);
+ `,
+ ]);
+ await providence(findImportsQueryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers[0]).to.equal('[default]');
+ // TODO: somehow mark as dynamic??
+ expect(firstEntry.result[0].source).to.equal('[variable]');
+ });
+
+ describe('Filter out false positives', () => {
+ it(`doesn't support [object.import('my/source')] (import method members)`, async () => {
+ mockProject([`object.import('my/source')`]);
+ await providence(findImportsQueryConfig, {
+ targetProjectPaths: ['/fictional/project'], // defined in mockProject
+ });
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry).to.equal(undefined);
+ });
+ });
+
+ /**
+ * Not in scope:
+ * - dynamic imports containing variables
+ * - tracking of specifier usage for default (dynamic or not) imports
+ */
+ });
+
+ describe('Default post processing', () => {
+ it('only stores external sources', async () => {
+ mockProject([
+ `
+ import '@external/source';
+ import 'external/source';
+ import './internal/source';
+ import '../internal/source';
+ import '../../internal/source';
+ `,
+ ]);
+ await providence(findImportsQueryConfig, { ..._providenceCfg });
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers.length).to.equal(1);
+ expect(firstEntry.result[0].source).to.equal('@external/source');
+ expect(firstEntry.result[1].source).to.equal('external/source');
+ expect(firstEntry.result[2]).to.equal(undefined);
+ });
+
+ it('normalizes source paths', async () => {
+ const queryConfig = QueryService.getQueryConfigFromAnalyzer('find-imports', {
+ keepInternalSources: true,
+ });
+ mockProject({
+ './internal/file-imports.js': `
+ import '@external/source';
+ import 'external/source';
+ import './source/x'; // auto resolve filename
+ import '../'; // auto resolve root
+ `,
+ './internal/source/x.js': '',
+ './index.js': '',
+ });
+ await providence(queryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers.length).to.equal(1);
+ expect(firstEntry.result[0].normalizedSource).to.equal('@external/source');
+ // expect(firstEntry.result[0].fullSource).to.equal('@external/source');
+ expect(firstEntry.result[1].normalizedSource).to.equal('external/source');
+ // expect(firstEntry.result[1].fullSource).to.equal('external/source');
+ expect(firstEntry.result[2].normalizedSource).to.equal('./source/x.js');
+ // expect(firstEntry.result[2].fullSource).to.equal('./internal/source/x.js');
+ expect(firstEntry.result[3].normalizedSource).to.equal('../index.js');
+ // expect(firstEntry.result[3].fullSource).to.equal('./index.js');
+ expect(firstEntry.result[4]).to.equal(undefined);
+ });
+ });
+
+ describe('Options', () => {
+ it('"keepInternalSources"', async () => {
+ const queryConfig = QueryService.getQueryConfigFromAnalyzer('find-imports', {
+ keepInternalSources: true,
+ });
+ mockProject([
+ `
+ import '@external/source';
+ import 'external/source';
+ import './internal/source';
+ import '../internal/source';
+ import '../../internal/source';
+ `,
+ ]);
+ await providence(queryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+
+ const firstEntry = getEntry(queryResult);
+ expect(firstEntry.result[0].importSpecifiers.length).to.equal(1);
+ expect(firstEntry.result[0].source).to.equal('@external/source');
+ expect(firstEntry.result[1].source).to.equal('external/source');
+ expect(firstEntry.result[2].source).to.equal('./internal/source');
+ expect(firstEntry.result[3].source).to.equal('../internal/source');
+ expect(firstEntry.result[4].source).to.equal('../../internal/source');
+ expect(firstEntry.result[5]).to.equal(undefined);
+ });
+
+ // Post processors for whole result
+ it('"keepOriginalSourceExtensions"', async () => {
+ const queryConfig = QueryService.getQueryConfigFromAnalyzer('find-imports', {
+ keepOriginalSourceExtensions: true,
+ });
+ mockProject([`import '@external/source.js'`, `import '@external/source';`]);
+ await providence(queryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+
+ const firstEntry = getEntry(queryResult);
+ const secondEntry = getEntry(queryResult, 1);
+
+ expect(firstEntry.result[0].normalizedSource).to.equal('@external/source.js');
+ expect(secondEntry.result[0].normalizedSource).to.equal('@external/source');
+ });
+
+ // TODO: currently disabled. Might become default later (increased readability of json reports)
+ // but only without loss of information and once depending analyzers (match-imports and
+ // match-subclasses) are made compatible.
+ it.skip('"sortBySpecifier"', async () => {
+ const queryConfig = QueryService.getQueryConfigFromAnalyzer('find-imports', {
+ sortBySpecifier: true,
+ });
+ mockProject(
+ [
+ `import { x, y } from '@external/source.js'`,
+ `import { x, y, z } from '@external/source.js'`,
+ ],
+ { filePaths: ['./file1.js', './file2.js'] },
+ );
+ await providence(queryConfig, _providenceCfg);
+ const queryResult = queryResults[0];
+
+ /**
+ * Output will be in the format of:
+ *
+ * "queryOutput": [
+ * {
+ * "specifier": "LitElement",
+ * "source": "lion-based-ui/core",
+ * "id": "LitElement::lion-based-ui/core",
+ * "dependents": [
+ * "my-app-using-lion-based-ui/src/x.js",
+ * "my-app-using-lion-based-ui/src/y/z.js", *
+ * ...
+ */
+
+ expect(queryResult.queryOutput[0].specifier).to.equal('x');
+ // Should be normalized source...?
+ expect(queryResult.queryOutput[0].source).to.equal('@external/source.js');
+ expect(queryResult.queryOutput[0].id).to.equal('x::@external/source.js');
+ expect(queryResult.queryOutput[0].dependents).to.eql([
+ 'fictional-project/file1.js',
+ 'fictional-project/file2.js',
+ ]);
+ });
+ });
+
+ // TODO: put this in the generic providence/analyzer part
+ describe.skip('With