Skip to content

Commit

Permalink
Add jsonExt reporter to output JSON with row/col of exports/types i…
Browse files Browse the repository at this point in the history
…ssues (#288)
  • Loading branch information
webpro committed Oct 22, 2023
1 parent 92c4a80 commit 7e483d4
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 2 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ $ npx knip --help
-n, --no-progress Don't show dynamic progress updates (automatically enabled in CI environments)
--preprocessor Preprocess the results before providing it to the reporter(s), can be repeated
--preprocessor-options Pass extra options to the preprocessor (as JSON string, see --reporter-options example)
--reporter Select reporter: symbols, compact, codeowners, json, can be repeated (default: symbols)
--reporter Select reporter: symbols, compact, codeowners, json, jsonExt, can be repeated (default: symbols)
--reporter-options Pass extra options to the reporter (as JSON string, see example)
--no-config-hints Suppress configuration hints
--no-exit-code Always exit with code zero (0)
Expand Down
2 changes: 2 additions & 0 deletions src/reporters/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import codeowners from './codeowners.js';
import compact from './compact.js';
import json from './json.js';
import jsonExt from './jsonExt.js';
import symbols from './symbols.js';

export default {
symbols,
compact,
codeowners,
json,
jsonExt,
};
101 changes: 101 additions & 0 deletions src/reporters/jsonExt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { OwnershipEngine } from '@snyk/github-codeowners/dist/lib/ownership/index.js';
import { isFile } from '../util/fs.js';
import { relative, resolve } from '../util/path.js';
import type { Report, ReporterOptions, IssueSet, IssueRecords, SymbolIssueType, Issue } from '../types/issues.js';
import type { Entries } from 'type-fest';

type ExtraReporterOptions = {
codeowners?: string;
};

type Item = { name: string; pos?: number; line?: number; col?: number };

type Row = {
file: string;
owners: Array<{ name: string }>;
files?: boolean;
dependencies?: Array<{ name: string }>;
devDependencies?: Array<{ name: string }>;
optionalPeerDependencies?: Array<{ name: string }>;
unlisted?: Array<{ name: string }>;
binaries?: Array<{ name: string }>;
unresolved?: Array<{ name: string }>;
exports?: Array<Item>;
types?: Array<Item>;
duplicates?: Array<Item[]>;
enumMembers?: Record<string, Array<Item>>;
classMembers?: Record<string, Array<Item>>;
};

const mergeTypes = (type: SymbolIssueType) =>
type === 'exports' || type === 'nsExports' ? 'exports' : type === 'types' || type === 'nsTypes' ? 'types' : type;

export default async ({ report, issues, options }: ReporterOptions) => {
let opts: ExtraReporterOptions = {};
try {
opts = options ? JSON.parse(options) : opts;
} catch (error) {
console.error(error);
}

const json: Record<string, Row> = {};
const codeownersFilePath = resolve(opts.codeowners ?? '.github/CODEOWNERS');
const codeownersEngine = isFile(codeownersFilePath) && OwnershipEngine.FromCodeownersFile(codeownersFilePath);

const flatten = (issues: IssueRecords): Issue[] => Object.values(issues).flatMap(Object.values);

const initRow = (filePath: string) => {
const file = relative(filePath);
const row: Row = {
file,
...(codeownersEngine && { owners: codeownersEngine.calcFileOwnership(file) }),
...(report.files && { files: false }),
...(report.dependencies && { dependencies: [] }),
...(report.devDependencies && { devDependencies: [] }),
...(report.optionalPeerDependencies && { optionalPeerDependencies: [] }),
...(report.unlisted && { unlisted: [] }),
...(report.binaries && { binaries: [] }),
...(report.unresolved && { unresolved: [] }),
...((report.exports || report.nsExports) && { exports: [] }),
...((report.types || report.nsTypes) && { types: [] }),
...(report.enumMembers && { enumMembers: {} }),
...(report.classMembers && { classMembers: {} }),
...(report.duplicates && { duplicates: [] }),
};
return row;
};

for (const [reportType, isReportType] of Object.entries(report) as Entries<Report>) {
if (isReportType) {
if (reportType === 'files') {
Array.from(issues[reportType] as IssueSet).forEach(filePath => {
json[filePath] = json[filePath] ?? initRow(filePath);
json[filePath][reportType] = true;
});
} else {
const type = mergeTypes(reportType);
flatten(issues[reportType] as IssueRecords).forEach(issue => {
const { filePath, symbol, symbols, parentSymbol } = issue;
json[filePath] = json[filePath] ?? initRow(filePath);
if (type === 'duplicates') {
symbols && json[filePath][type]?.push(symbols.map(symbol => ({ name: symbol })));
} else if (type === 'enumMembers' || type === 'classMembers') {
const item = json[filePath][type];
if (parentSymbol && item) {
item[parentSymbol] = item[parentSymbol] ?? [];
item[parentSymbol].push({ name: issue.symbol, line: issue.line, col: issue.col, pos: issue.pos });
}
} else {
if (type === 'exports' || type === 'types') {
json[filePath][type]?.push({ name: issue.symbol, line: issue.line, col: issue.col, pos: issue.pos });
} else {
json[filePath][type]?.push({ name: symbol });
}
}
});
}
}
}

console.log(JSON.stringify(Object.values(json)));
};
2 changes: 1 addition & 1 deletion src/util/cli-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Options:
-n, --no-progress Don't show dynamic progress updates (automatically enabled in CI environments)
--preprocessor Preprocess the results before providing it to the reporter(s), can be repeated
--preprocessor-options Pass extra options to the preprocessor (as JSON string, see --reporter-options example)
--reporter Select reporter: symbols, compact, codeowners, json, can be repeated (default: symbols)
--reporter Select reporter: symbols, compact, codeowners, json, jsonExt, can be repeated (default: symbols)
--reporter-options Pass extra options to the reporter (as JSON string, see example)
--no-config-hints Suppress configuration hints
--no-exit-code Always exit with code zero (0)
Expand Down
131 changes: 131 additions & 0 deletions test/cli-reporter-jsonExt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import assert from 'node:assert/strict';
import { EOL } from 'node:os';
import test from 'node:test';
import { resolve } from '../src/util/path.js';
import { execFactory } from './helpers/execKnip.js';

const cwd = resolve('fixtures/exports');

const exec = execFactory(cwd);

test('knip --reporter jsonext', () => {
const json = [
{
file: 'default.ts',
files: false,
dependencies: [],
devDependencies: [],
optionalPeerDependencies: [],
unlisted: [],
binaries: [],
unresolved: [],
exports: [{ name: 'NamedExport', line: 1, col: 14, pos: 13 }],
types: [],
enumMembers: {},
classMembers: {},
duplicates: [],
},
{
file: 'dynamic-import.ts',
files: false,
dependencies: [],
devDependencies: [],
optionalPeerDependencies: [],
unlisted: [],
binaries: [],
unresolved: [],
exports: [{ name: 'unusedZero', line: 3, col: 14, pos: 39 }],
types: [],
enumMembers: {},
classMembers: {},
duplicates: [],
},
{
file: 'my-mix.ts',
files: false,
dependencies: [],
devDependencies: [],
optionalPeerDependencies: [],
unlisted: [],
binaries: [],
unresolved: [],
exports: [{ name: 'unusedInMix', line: 1, col: 14, pos: 13 }],
types: [],
enumMembers: {},
classMembers: {},
duplicates: [],
},
{
file: 'my-module.ts',
files: false,
dependencies: [],
devDependencies: [],
optionalPeerDependencies: [],
unlisted: [],
binaries: [],
unresolved: [],
exports: [
{ name: 'unusedNumber', line: 14, col: 14, pos: 562 },
{ name: 'unusedFunction', line: 15, col: 14, pos: 593 },
{ name: 'default', line: 21, col: 8, pos: 727 },
],
types: [{ name: 'MyAnyType', line: 19, col: 13, pos: 702 }],
enumMembers: {},
classMembers: {},
duplicates: [[{ name: 'exportedResult' }, { name: 'default' }]],
},
{
file: 'named-exports.ts',
files: false,
dependencies: [],
devDependencies: [],
optionalPeerDependencies: [],
unlisted: [],
binaries: [],
unresolved: [],
exports: [
{ name: 'renamedExport', line: 6, col: 30, pos: 179 },
{ name: 'namedExport', line: 7, col: 15, pos: 215 },
],
types: [],
enumMembers: {},
classMembers: {},
duplicates: [],
},
{
file: 'my-namespace.ts',
files: false,
dependencies: [],
devDependencies: [],
optionalPeerDependencies: [],
unlisted: [],
binaries: [],
unresolved: [],
exports: [{ name: 'nsUnusedKey', line: 3, col: 14, pos: 84 }],
types: [{ name: 'MyNamespace', line: 5, col: 18, pos: 119 }],
enumMembers: {},
classMembers: {},
duplicates: [],
},
{
file: 'types.ts',
files: false,
dependencies: [],
devDependencies: [],
optionalPeerDependencies: [],
unlisted: [],
binaries: [],
unresolved: [],
exports: [],
types: [
{ name: 'MyEnum', line: 3, col: 13, pos: 71 },
{ name: 'MyType', line: 8, col: 14, pos: 145 },
],
enumMembers: {},
classMembers: {},
duplicates: [],
},
];

assert.equal(exec('knip --reporter jsonExt'), JSON.stringify(json) + EOL);
});

0 comments on commit 7e483d4

Please sign in to comment.