Skip to content

Commit

Permalink
I think this is right?
Browse files Browse the repository at this point in the history
  • Loading branch information
aomarks committed Sep 8, 2024
1 parent ab26149 commit a312261
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 145 deletions.
188 changes: 58 additions & 130 deletions src/analysis/dependency-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ import type {Diagnostic, Range, Result} from '../error.js';
export function parseDependency(
dependency: string,
): Result<ParsedDependency, Diagnostic> {
if (dependency.includes('#')) {
return new NewDependencyParser(dependency).parse();
} else {
return new DependencyParser(dependency).parse();
}
return new DependencyParser(dependency).parse();
}

export interface ParsedDependency {
Expand Down Expand Up @@ -51,169 +47,81 @@ class DependencyParser {
this.#len = str.length;
}

#peek(offset = 0) {
#peek(offset = 0): string | undefined {
return this.#str[this.#pos + offset];
}

#skip(num = 1) {
#skip(num = 1): void {
this.#pos += num;
}

#consume(num = 1) {
#consume(num = 1): string {
const substr = this.#str.slice(this.#pos, this.#pos + num);
this.#pos += num;
return substr;
}

#done(): boolean {
return this.#pos >= this.#len;
}

#lookAhead(substr: string, offset = 0): boolean {
return this.#str.includes(substr, this.#pos + offset);
}

parse(): Result<ParsedDependency, Diagnostic> {
if (this.#peek() === PERIOD) {
const pkg = this.#parsePackage();
const script = this.#parseScript();
return this.#parseBackwardsCompatible();
}

const first = this.#parseParts();
if (this.#done()) {
return {
ok: true,
value: {package: pkg, script},
value: {package: [], script: first},
};
} else {
if (this.#peek() === BACKSLASH && this.#peek(1) === PERIOD) {
this.#skip();
}
const script = this.#parseScript();
}
this.#skip();
const second = this.#parseParts();
if (!this.#done()) {
return {
ok: true,
value: {package: [], script},
value: {package: [], script: []},
};
}
return {
ok: true,
value: {package: first, script: second},
};
}

#parsePackage(): Part[] {
let buffer = '';
const pkg: Part[] = [];
while (this.#pos < this.#len) {
if (this.#peek() === BACKSLASH && this.#peek(1) === COLON) {
buffer += COLON;
this.#skip(2);
} else if (this.#peek() === COLON) {
break;
} else if (this.#peek() === BACKSLASH && this.#peek(1) === LT) {
buffer += LT;
this.#skip(2);
} else if (this.#peek() === LT) {
if (buffer.length > 0) {
pkg.push({kind: 'literal', value: buffer});
}
buffer = '';
pkg.push(this.#parseVariable());
} else {
buffer += this.#consume();
}
}
if (buffer.length > 0) {
pkg.push({kind: 'literal', value: buffer});
}
return pkg;
}

#parseScript(): Part[] {
if (this.#peek() === COLON) {
this.#skip();
}
let buffer = '';
const script: Part[] = [];
while (this.#pos < this.#len) {
if (this.#peek() === BACKSLASH && this.#peek(1) === LT) {
buffer += LT;
this.#skip(2);
} else if (this.#peek() === LT) {
if (buffer.length > 0) {
script.push({kind: 'literal', value: buffer});
}
buffer = '';
script.push(this.#parseVariable());
} else {
buffer += this.#consume();
}
}
if (buffer.length > 0) {
script.push({kind: 'literal', value: buffer});
}
return script;
}

#parseVariable(): VariablePart {
this.#skip();
let value = '';
while (this.#pos < this.#len) {
if (this.#peek() === BACKSLASH && this.#peek(1) === GT) {
value += GT;
this.#skip(2);
} else if (this.#peek() === GT) {
this.#skip();
return {kind: 'variable', value};
} else {
value += this.#consume();
}
}
return {kind: 'variable', value: 'ERROR'};
}
}

class NewDependencyParser {
readonly #str: string;
readonly #len: number;
#pos = 0;

constructor(str: string) {
this.#str = str;
this.#len = str.length;
}

#peek(offset = 0): string | undefined {
return this.#str[this.#pos + offset];
}

#skip(num = 1): void {
this.#pos += num;
}

#consume(num = 1): string {
const substr = this.#str.slice(this.#pos, this.#pos + num);
this.#pos += num;
return substr;
}

#done(): boolean {
return this.#pos >= this.#len;
}

parse(): Result<ParsedDependency, Diagnostic> {
const packageOrScript = this.#parseParts();
#parseBackwardsCompatible(): Result<ParsedDependency, Diagnostic> {
const pkg = this.#parseParts(true);
if (this.#done()) {
return {
ok: true,
value: {package: [], script: packageOrScript},
value: {package: pkg, script: []},
};
}
if (this.#peek() !== HASH) {
throw new Error('no!');
}
this.#skip(); // Skip the hash
// Assume it's either a "#" or a ":".
this.#skip();
const script = this.#parseParts();
if (!this.#done()) {
throw new Error('NO!');
return {
ok: true,
value: {package: [], script: []},
};
}
return {
ok: true,
value: {package: packageOrScript, script},
value: {package: pkg, script},
};
}

#parseParts(): Part[] {
#parseParts(isPathPosition = false): Part[] {
let buffer = '';
const parts: Part[] = [];
while (this.#pos < this.#len) {
if (this.#peek() === HASH) {
break;
}
if (this.#peek() === BACKSLASH && this.#peek(1) === HASH) {
buffer += HASH;
this.#skip(2);
Expand All @@ -226,6 +134,26 @@ class NewDependencyParser {
}
buffer = '';
parts.push(this.#parseVariable());
} else if (this.#peek() === HASH) {
break;
} else if (
isPathPosition &&
this.#peek() === COLON &&
!this.#lookAhead('#', 1)
) {
// This case provides backwards compatibility for the syntax before "#"
// was adopted. The rule here covers cases like:
//
// ./foo:bar
//
// Which we would today recommend writing as:
//
// ./foo#bar
//
// But which, without this case, would be wrongly interpreted as:
//
// { package: undefined , script: "./foo:bar" }
break;
} else {
buffer += this.#consume();
}
Expand Down
23 changes: 8 additions & 15 deletions src/test/analysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,21 +415,21 @@ const cases = [
':bar',
{
package: [],
script: [{kind: 'literal', value: 'bar'}],
script: [{kind: 'literal', value: ':bar'}],
},
],
[
'./foo\\:bar:baz',
{
package: [{kind: 'literal', value: './foo:bar'}],
script: [{kind: 'literal', value: 'baz'}],
package: [{kind: 'literal', value: './foo\\'}],
script: [{kind: 'literal', value: 'bar:baz'}],
},
],
[
'./foo\\:bar:baz:qux',
{
package: [{kind: 'literal', value: './foo:bar'}],
script: [{kind: 'literal', value: 'baz:qux'}],
package: [{kind: 'literal', value: './foo\\'}],
script: [{kind: 'literal', value: 'bar:baz:qux'}],
},
],
['./foo', {package: [{kind: 'literal', value: './foo'}], script: []}],
Expand All @@ -439,7 +439,7 @@ const cases = [
{package: [{kind: 'literal', value: './foo\\/bar'}], script: []},
],
['', {package: [], script: []}],
[':', {package: [], script: []}],
[':', {package: [], script: [{kind: 'literal', value: ':'}]}],
[
'../foo:bar',
{
Expand Down Expand Up @@ -522,7 +522,8 @@ const cases = [
'\\./packages/foo:bar',
{
package: [],
script: [{kind: 'literal', value: './packages/foo:bar'}],
// TODO(aomarks) Are you sure about this?
script: [{kind: 'literal', value: '\\./packages/foo:bar'}],
},
],

Expand Down Expand Up @@ -581,14 +582,6 @@ const cases = [
},
],

[
'build',
{
package: [],
script: [{kind: 'literal', value: 'build:tsc'}],
},
],

[
'\\<workspaces>:\\<this>',
{
Expand Down

0 comments on commit a312261

Please sign in to comment.