Skip to content

Commit

Permalink
Handle source map referencing EOL character (#136)
Browse files Browse the repository at this point in the history
- Detect EOL character for the file. It fixes wrong splitting for source maps generated on Windows environments.
- When checking column boundary verify that the last referenced column is not  EOL.
- Generate source map referencing EOL characters (typescript.ts)
- Fix 'InvalidMappingLine' check. Getting not existing value from array returns `undefined`, not `null`.
- Add test for 'InvalidMappingColumn' error. Generate `invalid-map-column.js` and `invalid-map-line.js`.
- Fix tests for errors
- Use `/` as invalid filename - works on Linux/Windows
  • Loading branch information
nikolay-borzov committed Nov 12, 2019
1 parent 2f8535b commit 254a5d9
Show file tree
Hide file tree
Showing 10 changed files with 500 additions and 402 deletions.
2 changes: 2 additions & 0 deletions src/app-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export class AppError extends Error {

constructor(errorContext: ErrorContext, error?: NodeJS.ErrnoException) {
super();
// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
Object.setPrototypeOf(this, AppError.prototype);

const message = getErrorMessage(errorContext);

Expand Down
76 changes: 54 additions & 22 deletions src/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function exploreBundle(

const sourceMapData = await loadSourceMap(code, map);

const sizes = computeFileSizeMapOptimized(sourceMapData);
const sizes = computeFileSizes(sourceMapData);

const files = adjustSourcePaths(sizes.files, options);

Expand Down Expand Up @@ -85,10 +85,44 @@ async function loadSourceMap(codeFile: File, sourceMapFile?: File): Promise<Sour
};
}

function detectEOL(content: string): string {
return content.includes('\r\n') ? '\r\n' : '\n';
}

interface ComputeFileSizesContext {
generatedLine: number;
generatedColumn: number;
line: string;
eol: string;
}

function checkInvalidMappingColumn({
generatedLine,
generatedColumn,
line,
eol,
}: ComputeFileSizesContext): void {
const maxColumnIndex = line.length - 1;

// Columns are 0-based
// Ignore case when source map references EOL character (e.g. https://github.com/microsoft/TypeScript/issues/34695)
if (generatedColumn > maxColumnIndex && `${line}${eol}`.lastIndexOf(eol) !== generatedColumn) {
throw new AppError({
code: 'InvalidMappingColumn',
generatedLine,
generatedColumn,
maxColumn: line.length,
});
}
}

/** Calculate the number of bytes contributed by each source file */
function computeFileSizeMapOptimized(sourceMapData: SourceMapData): FileSizes {
function computeFileSizes(sourceMapData: SourceMapData): FileSizes {
const { consumer, codeFileContent } = sourceMapData;
const lines = codeFileContent.split('\n');
const eol = detectEOL(codeFileContent);
// Assume only one EOL is used
const lines = codeFileContent.split(eol);

const files: FileSizeMap = {};
let mappedBytes = 0;

Expand All @@ -97,38 +131,36 @@ function computeFileSizeMapOptimized(sourceMapData: SourceMapData): FileSizes {
consumer.eachMapping(({ source, generatedLine, generatedColumn, lastGeneratedColumn }) => {
// Lines are 1-based
const line = lines[generatedLine - 1];
if (line === null) {

if (line === undefined) {
throw new AppError({
code: 'InvalidMappingLine',
generatedLine: generatedLine,
generatedLine,
maxLine: lines.length,
});
}

// Columns are 0-based
if (generatedColumn >= line.length) {
throw new AppError({
code: 'InvalidMappingColumn',
generatedLine: generatedLine,
generatedColumn: generatedColumn,
maxColumn: line.length,
});
}
checkInvalidMappingColumn({
generatedLine,
generatedColumn,
line,
eol,
});

let mappingLength = 0;
if (lastGeneratedColumn !== null) {
if (lastGeneratedColumn >= line.length) {
throw new AppError({
code: 'InvalidMappingColumn',
generatedLine: generatedLine,
generatedColumn: lastGeneratedColumn,
maxColumn: line.length,
});
}
checkInvalidMappingColumn({
generatedLine,
generatedColumn: lastGeneratedColumn,
line,
eol,
});

mappingLength = lastGeneratedColumn - generatedColumn + 1;
} else {
mappingLength = line.length - generatedColumn;
}

files[source] = (files[source] || 0) + mappingLength;
mappedBytes += mappingLength;
});
Expand Down
29 changes: 20 additions & 9 deletions tests/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ describe('api', () => {
bundlesAndFileTokens: { code: 'data/no-map.js' },
expectedErrorCode: 'NoSourceMap',
},
{
name: 'should throw if source map reference column beyond generated last column in line',
bundlesAndFileTokens: 'data/invalid-map-column.js',
expectedErrorCode: 'InvalidMappingColumn',
},
];

bundleErrorTests.forEach(function({
Expand All @@ -180,12 +185,10 @@ describe('api', () => {
expectedErrorCode,
}) {
it(name, async function() {
try {
await explore(bundlesAndFileTokens, options);
} catch (result) {
await expect(explore(bundlesAndFileTokens, options)).to.be.rejected.then(result => {
const error = result.errors[0];
expect(error.code).to.equal(expectedErrorCode);
}
});
});
});

Expand All @@ -198,18 +201,17 @@ describe('api', () => {
{
name: 'should throw when cannot save html to file',
bundlesAndFileTokens: 'data/inline-map.js',
options: { output: { format: 'html', filename: '?' } },
// `/` supposed to be invalid filename on both Linux and Windows
options: { output: { format: 'html', filename: '/' } },
expectedErrorCode: 'CannotSaveFile',
},
];

appErrorTests.forEach(function({ name, bundlesAndFileTokens, options, expectedErrorCode }) {
it(name, async function() {
try {
await explore(bundlesAndFileTokens, options);
} catch (error) {
await expect(explore(bundlesAndFileTokens, options)).to.be.rejected.then(error => {
expect(error.code).to.equal(expectedErrorCode);
}
});
});
});

Expand Down Expand Up @@ -289,6 +291,15 @@ describe('api', () => {
code: 'data/inline-map.js',
map: undefined,
},
{
code: 'data/invalid-map-column.js',
map: undefined,
},
{
code: 'data/invalid-map-line.js',
map: undefined,
},
{ code: 'data/map-reference-eol.js', map: undefined },
{
code: 'data/no-map.js',
map: 'data/no-map.js.map',
Expand Down
3 changes: 3 additions & 0 deletions tests/data/invalid-map-column.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions tests/data/invalid-map-line.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions tests/data/map-reference-eol.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions tests/generate-data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# How to generate

## `invalid-map-column.js`

1. Create `src/invalid-map-column.ts` with content
```
console.log('goodbye cruel world');
console.log('goodbye man');
```
2. Run `tsc --inlineSourceMap src/invalid-map-column.ts --outFile ../data/invalid-map-column.js`
3. Save source map comment from `../data/invalid-map-column.js`
4. Replace `src/invalid-map-column.ts` content with
```
console.log('hello happy world');
console.log('hello man');
```
5. Run `.2` again
6. Replace source map comment of `../data/invalid-map-column.js` with one from `.3`

## `invalid-map-line.js`

1. Create `src/invalid-map-line.ts` with content
```
console.log('hello happy world');
console.log('hello man');
```
2. Run `tsc --inlineSourceMap src/invalid-map-line.ts --outFile ../data/invalid-map-line.js`
3. Save source map comment from `../data/invalid-map-line.js`
4. Replace `src/invalid-map-line.ts` content with
```
console.log('hello happy world');
```
5. Run `.2` again
6. Replace source map comment of `../data/invalid-map-line.js` with one from `.3`

0 comments on commit 254a5d9

Please sign in to comment.