Skip to content

Commit

Permalink
Adding utils to read and extract source fragments (#531)
Browse files Browse the repository at this point in the history
  • Loading branch information
scalvert authored Oct 30, 2022
1 parent 9bbaa72 commit 072ffac
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 5 deletions.
56 changes: 52 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Those utilities are:

<dl>
<dt><a href="#buildTodoDatum">buildTodoDatum(lintResult, lintMessage, todoConfig)</a> ⇒</dt>
<dd><p>Adapts a <a href="https://github.com/lint-todo/utils/blob/master/src/types/lint.ts#L31">LintResult</a> to a <a href="https://github.com/lint-todo/utils/blob/master/src/types/todos.ts#L46">TodoData</a>. FilePaths are absolute
<dd><p>Adapts a <a href="https://github.com/lint-todo/utils/blob/master/src/types/lint.ts#L31">LintResult</a> to a <a href="https://github.com/lint-todo/utils/blob/master/src/types/todo.ts#L61">TodoData</a>. FilePaths are absolute
when received from a lint result, so they&#39;re converted to relative paths for stability in
serializing the contents to disc.</p>
</dd>
Expand Down Expand Up @@ -68,7 +68,7 @@ have a todo lint violation.</p>
<dt><a href="#applyTodoChanges">applyTodoChanges(baseDir, add, remove, shouldLock)</a></dt>
<dd><p>Applies todo changes, either adding or removing, based on batches from <code>getTodoBatches</code>.</p>
</dd>
<dt><a href="#compactTodoStorageFile">compactTodoStorageFile(baseDir, options)</a> ⇒</dt>
<dt><a href="#compactTodoStorageFile">compactTodoStorageFile(baseDir)</a> ⇒</dt>
<dd><p>Compacts the .lint-todo storage file.</p>
</dd>
<dt><a href="#getTodoConfig">getTodoConfig(baseDir, engine, customDaysToDecay)</a> ⇒</dt>
Expand All @@ -92,6 +92,15 @@ have a todo lint violation.</p>
<dt><a href="#format">format(date)</a> ⇒</dt>
<dd><p>Formats the date in short form, eg. 2021-01-01</p>
</dd>
<dt><a href="#buildRange">buildRange(line, column, endLine, endColumn)</a> ⇒</dt>
<dd><p>Converts node positional numbers into a Range object.</p>
</dd>
<dt><a href="#readSource">readSource(filePath)</a> ⇒</dt>
<dd><p>Reads a source file, optionally caching it if it&#39;s already been read.</p>
</dd>
<dt><a href="#getSourceForRange">getSourceForRange(source, range)</a> ⇒</dt>
<dd><p>Extracts a source fragment from a file&#39;s contents based on the provided Range.</p>
</dd>
</dl>

<a name="buildTodoDatum"></a>
Expand Down Expand Up @@ -308,7 +317,7 @@ Applies todo changes, either adding or removing, based on batches from `getTodoB

<a name="compactTodoStorageFile"></a>

## compactTodoStorageFile(baseDir, options) ⇒
## compactTodoStorageFile(baseDir) ⇒
Compacts the .lint-todo storage file.

**Kind**: global function
Expand All @@ -317,7 +326,6 @@ Compacts the .lint-todo storage file.
| Param | Description |
| --- | --- |
| baseDir | The base directory that contains the .lint-todo storage file. |
| options | An object containing read options. |

<a name="getTodoConfig"></a>

Expand Down Expand Up @@ -450,5 +458,45 @@ Formats the date in short form, eg. 2021-01-01
| --- | --- |
| date | The date to format |

<a name="buildRange"></a>

## buildRange(line, column, endLine, endColumn) ⇒
Converts node positional numbers into a Range object.

**Kind**: global function
**Returns**: A range object.

| Param | Description |
| --- | --- |
| line | The source start line. |
| column | The source start column. |
| endLine | The source end line. |
| endColumn | The source end column. |

<a name="readSource"></a>

## readSource(filePath) ⇒
Reads a source file, optionally caching it if it's already been read.

**Kind**: global function
**Returns**: The file contents.

| Param | Description |
| --- | --- |
| filePath | The path to the source file. |

<a name="getSourceForRange"></a>

## getSourceForRange(source, range) ⇒
Extracts a source fragment from a file's contents based on the provided Range.

**Kind**: global function
**Returns**: The source fragment.

| Param | Description |
| --- | --- |
| source | The file contents. |
| range | A Range object representing the range to extract from the file contents. |


<!--DOCS_END-->
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"build": "tsc --build",
"build:watch": "tsc --watch",
"clean": "tsc --build --clean",
"docs:update": "npm run build && readme-api-generator lib/builders.js lib/io.js lib/todo-config.js lib/get-severity.js lib/date-utils.js",
"docs:update": "npm run build && readme-api-generator lib/builders.js lib/io.js lib/todo-config.js lib/get-severity.js lib/date-utils.js lib/source.js",
"lint": "eslint . --ext .ts",
"prepare": "npm run build",
"test": "npm-run-all lint test:*",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ export {
export { getTodoConfig, validateConfig } from './todo-config';
export { getSeverity } from './get-severity';
export { differenceInDays, format, getDatePart, isExpired } from './date-utils';
export { buildRange, readSource, getSourceForRange } from './source';

export * from './types';
93 changes: 93 additions & 0 deletions src/source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { existsSync, readFileSync } from 'fs';
import { Range } from './types';

const LINES_PATTERN = /(.*?(?:\r\n?|\n|$))/gm;
export const _sourceCache = new Map<string, string>();

/**
* Converts node positional numbers into a Range object.
*
* @param line - The source start line.
* @param column - The source start column.
* @param endLine - The source end line.
* @param endColumn - The source end column.
* @returns A range object.
*/
export function buildRange(
line: number,
column: number,
endLine?: number,
endColumn?: number
): Range {
return {
start: {
line: line,
column: column,
},
end: {
line: endLine ?? line,
column: endColumn ?? column,
},
};
}

/**
* Reads a source file, optionally caching it if it's already been read.
*
* @param filePath - The path to the source file.
* @returns The file contents.
*/
export function readSource(filePath: string | undefined): string {
if (!filePath) {
return '';
}

if (existsSync(filePath) && !_sourceCache.has(filePath)) {
const source = readFileSync(filePath, { encoding: 'utf8' });

_sourceCache.set(filePath, source);
}

return _sourceCache.get(filePath) || '';
}

/**
* Extracts a source fragment from a file's contents based on the provided Range.
*
* @param source - The file contents.
* @param range - A Range object representing the range to extract from the file contents.
* @returns The source fragment.
*/
export function getSourceForRange(source: string, range: Range): string {
if (!source) {
return '';
}

const sourceLines = source.match(LINES_PATTERN) || [];
const firstLine = range.start.line - 1;
const lastLine = range.end.line - 1;
let currentLine = firstLine - 1;
const firstColumn = range.start.column - 1;
const lastColumn = range.end.column - 1;
const src = [];
let line;

while (currentLine < lastLine) {
currentLine++;
line = sourceLines[currentLine];

if (currentLine === firstLine) {
if (firstLine === lastLine) {
src.push(line.slice(firstColumn, lastColumn));
} else {
src.push(line.slice(firstColumn));
}
} else if (currentLine === lastLine) {
src.push(line.slice(0, lastColumn));
} else {
src.push(line);
}
}

return src.join('');
}
36 changes: 36 additions & 0 deletions tests/source-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, it, expect } from 'vitest';
import { buildRange, getSourceForRange } from '../src/source';

describe('source', () => {
describe('getSourceForRange', () => {
it('returns empty string for empty source', () => {
expect(getSourceForRange('', buildRange(0, 0))).toEqual('');
});

it('returns correct full single line fragment', () => {
const source = "const someLongVariableDeclaration = 'This is a message!!'";
const range = buildRange(1, 1, 1, 58);

expect(getSourceForRange(source, range)).toEqual(source);
});

it('returns correct single line sub-fragment', () => {
const source = "const someLongVariableDeclaration = 'This is a message!!'";
const range = buildRange(1, 1, 1, 34);

expect(getSourceForRange(source, range)).toEqual('const someLongVariableDeclaration');
});

it('returns correct multi line sub-fragment', () => {
const source = `function addOne(i) {
if (i != NaN) {
return i++;
}
return;
}`;

expect(getSourceForRange(source, buildRange(1, 10, 1, 16))).toEqual('addOne');
expect(getSourceForRange(source, buildRange(2, 7, 2, 15))).toEqual('i != NaN');
});
});
});

0 comments on commit 072ffac

Please sign in to comment.