Skip to content

Commit

Permalink
chore(midnight-smoker): JSONBlamer nits, tests
Browse files Browse the repository at this point in the history
  • Loading branch information
boneskull committed Oct 7, 2024
1 parent 1a3f6b4 commit 076c8d2
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 35 deletions.
34 changes: 20 additions & 14 deletions packages/midnight-smoker/src/rule/json-blamer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,11 @@ import {JSONLocation} from './json-location';

/**
* A {@link Node} which can contain one or more {@link ValueNode ValueNodes}.
*
* @see {@link https://github.com/humanwhocodes/momoa/pull/134}
*/
type ContainerNode = DocumentNode | ElementNode | MemberNode;

export type GetContextOptions = {
/**
* Number of lines of context to show before the highlighted line
*/
before?: number;
};

/**
* The result of a successful call to {@link JSONBlamer.find}.
*/
Expand Down Expand Up @@ -72,6 +67,11 @@ type ValueNode = ArrayNode | ObjectNode;

const DEFAULT_BEFORE_LINES = 3;

/**
* A class which creates a "source context string" for a JSON file, which
* contains a few lines of source, line numbers, and a highlighted key/value
* pair somewhere within.
*/
export class JSONBlamer {
/**
* Root {@link DocumentNode} of the AST
Expand All @@ -94,6 +94,14 @@ export class JSONBlamer {
public readonly beforeLines: number = DEFAULT_BEFORE_LINES,
) {}

public static create(
rawJson: string,
jsonPath: string,
beforeLines?: number,
) {
return new JSONBlamer(rawJson, jsonPath, beforeLines);
}

/**
* Applies ANSI syntax highlighting to {@link JSONBlamer.json}.
*
Expand Down Expand Up @@ -156,7 +164,7 @@ export class JSONBlamer {
}, root);

if (found) {
const loc = new JSONLocation(
const loc = JSONLocation.create(
this.jsonPath,
found.loc.start,
found.loc.end,
Expand Down Expand Up @@ -190,7 +198,9 @@ export class JSONBlamer {
* {@link JSONBlamer.find}
* @returns A nice thing to print to the console
*/
@memoize((result: BlameInfo) => `${result.filepath}:${result.keypath}`)
@memoize(
(result: BlameInfo) => result && `${result.filepath}:${result.keypath}`,
)
public getContext(blameInfo: BlameInfo): string {
if (!blameInfo) {
throw new InvalidArgError('blameInfo is required', {
Expand Down Expand Up @@ -234,11 +244,7 @@ export class JSONBlamer {
.slice(blameInfo.loc.start.line - 1, blameInfo.loc.end.line)
.map(stripAnsi);
const maxCol = max(strippedLines.map(stringWidth));
ok(
maxCol,
// typeof maxCol !== 'undefined',
'Unexpected empty array of highlighted lines. This is a bug',
);
ok(maxCol, 'Unexpected empty array of highlighted lines. This is a bug');
contextLines = [
...lines.slice(startLine, blameInfo.loc.start.line - 1),
...strippedLines.map((line, idx) => {
Expand Down
16 changes: 16 additions & 0 deletions packages/midnight-smoker/src/rule/json-location.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
/**
* Provides {@link JSONLocation}, which is a wrapper around a `LocationRange` and
* includes a filepath.
*/

import {type Location} from '@humanwhocodes/momoa';

/**
* The only interesting thing here is {@link JSONLocation.toString}, which prints
* a path, start line, and column delimited by `:`, which terminals may
* recognize to open the file in an IDE and jump to the location.
*
* This works in iTerm2 + VSCode, anyhow.
*/
export class JSONLocation {
constructor(
public readonly filepath: string,
public readonly start: Location,
public readonly end: Location,
) {}

public static create(filepath: string, start: Location, end: Location) {
return new JSONLocation(filepath, start, end);
}

public toString() {
return `${this.filepath}:${this.start.line}:${this.start.column}`;
}
Expand Down
79 changes: 58 additions & 21 deletions packages/midnight-smoker/test/unit/json-blamer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {ErrorCode} from '#error/codes';
import {JSONBlamer} from '#rule/json-blamer';
import {type BlameInfo, JSONBlamer} from '#rule/json-blamer';
import {JSONLocation} from '#rule/json-location';
import {NL} from '#util/format';
import {type Location} from '@humanwhocodes/momoa';
import unexpected from 'unexpected';
import unexpectedSinon from 'unexpected-sinon';

Expand Down Expand Up @@ -155,36 +158,70 @@ describe('midnight-smoker', function () {

describe('getContext()', function () {
describe('when called without a BlameInfo', function () {
it('should reject', async function () {
it('should throw', function () {
const result = jsonBlamer.find('papa.smurf');
await expect(
jsonBlamer.getContext(result!),
'to be rejected with error satisfying',
{
code: ErrorCode.InvalidArgError,
},
);
expect(() => jsonBlamer.getContext(result!), 'to throw error', {
code: ErrorCode.InvalidArgError,
});
});
});

describe('when called with an invalid BlameInfo', function () {
it('should fail with an AssertionError', async function () {
await expect(
jsonBlamer.getContext({
loc: {
end: {
line: 0,
},
start: {
line: 0,
it('should fail with an AssertionError', function () {
expect(
() =>
jsonBlamer.getContext({
loc: {
end: {
line: 0,
},
start: {
line: 0,
},
},
},
} as any),
'to be rejected with error satisfying',
} as any),
'to throw error',
{code: ErrorCode.AssertionError},
);
});
});

describe('when BlameInfo represents a range', function () {
let info: BlameInfo;

beforeEach(function () {
const start: Location = {
column: 1,
line: 1,
offset: 0,
};
const end: Location = {
column: 10,
line: 1,
offset: 0,
};
info = {
filepath: 'some.json',
keypath: 'foo.bar',
loc: JSONLocation.create('some.json', start, end),
value: 'baz',
};
});

it('should return a formatted source context string (no color)', function () {
const context = jsonBlamer.getContext(info);
// note the silly string formatting here
expect(
`${NL}${context}${NL}`,
'to equal',
`
— some.json ——————————————✂
1: {"foo": {"bar": "baz"}}
——————————————————————————✂
`,
);
});
});
});
});
});
Expand Down

0 comments on commit 076c8d2

Please sign in to comment.