Skip to content

Commit ecc72b4

Browse files
committed
refactor: simplify parser APIs and fix StringUnescaper bugs (v2.0 sync)
- Remove deprecated constructor params from ConstExprParser (unescapeStrings, quoteAwareConstExprString) and PhpDocParser (requireWhitespaceBeforeDescription, preserveTypeAliasesWithInvalidTypes, textBetweenTagsBelongsToDescription) - Always unescape strings in ConstExprParser, always preserve type aliases with invalid types - Fix StringUnescaper single-quote regex: match \' not \. for PHP single-quoted string escaping - Fix StringUnescaper double-quote regex: properly escape backslash before quote in parseEscapeSequences - Remove unused eslint-disable directive in PhpDocParser - Update tests for simplified APIs
1 parent 0ed8d7e commit ecc72b4

6 files changed

Lines changed: 44 additions & 78 deletions

File tree

src/phpdoc-parser/parser/const-expr-parser.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,13 @@ export class ConstExprParser {
2323
* @param usedAttributes is an object that may have 'lines' and 'indexes' properties
2424
*/
2525
constructor(
26-
private unescapeStrings: boolean = false,
2726
usedAttributes: { lines?: boolean; indexes?: boolean } = {},
2827
) {
2928
this.useLinesAttributes = usedAttributes.lines ?? false;
3029
this.useIndexAttributes = usedAttributes.indexes ?? false;
3130
}
3231

33-
public parse(tokens: TokenIterator, trimStrings = false): ConstExprNode {
32+
public parse(tokens: TokenIterator): ConstExprNode {
3433
const startLine = tokens.currentTokenLine();
3534
const startIndex = tokens.currentTokenIndex();
3635
if (tokens.isCurrentTokenType(Lexer.TOKEN_FLOAT)) {
@@ -63,15 +62,8 @@ export class ConstExprParser {
6362
Lexer.TOKEN_DOUBLE_QUOTED_STRING,
6463
)
6564
) {
66-
let value = tokens.currentTokenValue();
65+
const value = StringUnescaper.unescapeString(tokens.currentTokenValue());
6766
const type = tokens.currentTokenType();
68-
if (trimStrings) {
69-
if (this.unescapeStrings) {
70-
value = StringUnescaper.unescapeString(value);
71-
} else {
72-
value = value.substring(1, value.length - 1);
73-
}
74-
}
7567
tokens.next();
7668

7769
return this.enrichWithAttributes(

src/phpdoc-parser/parser/php-doc-parser.ts

Lines changed: 28 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,7 @@ export class PhpDocParser {
6363
constructor(
6464
public typeParser: TypeParser,
6565
public constantExprParser: ConstExprParser,
66-
public requireWhitespaceBeforeDescription: boolean = false,
67-
public preserveTypeAliasesWithInvalidTypes: boolean = false,
68-
usedAttributes: { lines: boolean; indexes: boolean } = {
69-
lines: false,
70-
indexes: false,
71-
},
72-
public parseDoctrineAnnotations: boolean = false,
73-
private textBetweenTagsBelongsToDescription: boolean = false,
66+
usedAttributes: { lines?: boolean; indexes?: boolean } = {},
7467
) {
7568
this.useLinesAttributes = usedAttributes.lines ?? false;
7669
this.useIndexAttributes = usedAttributes.indexes ?? false;
@@ -177,21 +170,11 @@ export class PhpDocParser {
177170
private parseText(tokens: TokenIterator): PhpDocTextNode {
178171
let text = '';
179172

180-
let endTokens = [
181-
Lexer.TOKEN_PHPDOC_EOL,
182-
Lexer.TOKEN_CLOSE_PHPDOC,
183-
Lexer.TOKEN_END,
184-
];
185-
if (this.textBetweenTagsBelongsToDescription) {
186-
endTokens = [Lexer.TOKEN_CLOSE_PHPDOC, Lexer.TOKEN_END];
187-
}
173+
const endTokens = [Lexer.TOKEN_CLOSE_PHPDOC, Lexer.TOKEN_END];
188174

189175
let savepoint = false;
190176

191-
while (
192-
this.textBetweenTagsBelongsToDescription ||
193-
!tokens.isCurrentTokenType(Lexer.TOKEN_PHPDOC_EOL)
194-
) {
177+
while (true) {
195178
const tmpText =
196179
tokens.getSkippedHorizontalWhiteSpaceIfAny() +
197180
tokens.joinUntil(Lexer.TOKEN_PHPDOC_EOL, ...endTokens);
@@ -201,14 +184,12 @@ export class PhpDocParser {
201184
break;
202185
}
203186

204-
if (this.textBetweenTagsBelongsToDescription) {
205-
if (!savepoint) {
206-
tokens.pushSavePoint();
207-
savepoint = true;
208-
} else if (tmpText !== '') {
209-
tokens.dropSavePoint();
210-
tokens.pushSavePoint();
211-
}
187+
if (!savepoint) {
188+
tokens.pushSavePoint();
189+
savepoint = true;
190+
} else if (tmpText !== '') {
191+
tokens.dropSavePoint();
192+
tokens.pushSavePoint();
212193
}
213194

214195
tokens.pushSavePoint();
@@ -742,34 +723,29 @@ export class PhpDocParser {
742723
// support psalm-type syntax
743724
tokens.tryConsumeTokenType(Lexer.TOKEN_EQUAL);
744725

745-
if (this.preserveTypeAliasesWithInvalidTypes) {
746-
const startLine = tokens.currentTokenLine();
747-
const startIndex = tokens.currentTokenIndex();
726+
const startLine = tokens.currentTokenLine();
727+
const startIndex = tokens.currentTokenIndex();
748728

749-
try {
750-
const type = this.typeParser.parse(tokens);
751-
if (!tokens.isCurrentTokenType(Lexer.TOKEN_CLOSE_PHPDOC)) {
752-
if (!tokens.isCurrentTokenType(Lexer.TOKEN_PHPDOC_EOL)) {
753-
throw new Error('Expected end of line');
754-
}
729+
try {
730+
const type = this.typeParser.parse(tokens);
731+
if (!tokens.isCurrentTokenType(Lexer.TOKEN_CLOSE_PHPDOC)) {
732+
if (!tokens.isCurrentTokenType(Lexer.TOKEN_PHPDOC_EOL)) {
733+
throw new Error('Expected end of line');
755734
}
756-
return new TypeAliasTagValueNode(alias, type);
757-
} catch (e) {
758-
this.parseOptionalDescription(tokens);
759-
return new TypeAliasTagValueNode(
760-
alias,
761-
this.enrichWithAttributes(
762-
tokens,
763-
new InvalidTypeNode(e as ParserException),
764-
startLine,
765-
startIndex,
766-
),
767-
);
768735
}
736+
return new TypeAliasTagValueNode(alias, type);
737+
} catch (e) {
738+
this.parseOptionalDescription(tokens);
739+
return new TypeAliasTagValueNode(
740+
alias,
741+
this.enrichWithAttributes(
742+
tokens,
743+
new InvalidTypeNode(e as ParserException),
744+
startLine,
745+
startIndex,
746+
),
747+
);
769748
}
770-
771-
const type = this.typeParser.parse(tokens);
772-
return new TypeAliasTagValueNode(alias, type);
773749
}
774750

775751
private parseTypeAliasImportTagValue(
@@ -941,7 +917,6 @@ export class PhpDocParser {
941917
}
942918

943919
if (
944-
this.requireWhitespaceBeforeDescription &&
945920
!tokens.isCurrentTokenType(
946921
Lexer.TOKEN_PHPDOC_EOL,
947922
Lexer.TOKEN_CLOSE_PHPDOC,

src/phpdoc-parser/parser/string-unescaper.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ export class StringUnescaper {
1313
const quote = input[0];
1414

1515
if (quote === "'") {
16-
// eslint-disable-next-line no-useless-escape
17-
return input.slice(1, input.length - 1).replaceAll(/\\([\\\.])/g, '$1');
16+
return input.slice(1, input.length - 1).replaceAll(/\\([\\'])/g, '$1');
1817
}
1918

2019
return this.parseEscapeSequences(input.slice(1, input.length - 1), '"');
@@ -23,7 +22,7 @@ export class StringUnescaper {
2322
// Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L90-L130
2423
private static parseEscapeSequences(input: string, quote: string): string {
2524
// eslint-disable-next-line no-param-reassign
26-
input = input.replaceAll(new RegExp(`\\${quote}`, 'g'), quote);
25+
input = input.replaceAll(new RegExp(`\\\\${quote}`, 'g'), quote);
2726

2827
return input.replaceAll(
2928
/\\([\\nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u\{([0-9a-fA-F]+)\})/g,

src/phpdoc-parser/parser/type-parser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ export class TypeParser {
307307
}
308308

309309
try {
310-
const constExpr = this.constExprParser.parse(tokens, true);
310+
const constExpr = this.constExprParser.parse(tokens);
311311
if (constExpr instanceof ConstExprArrayNode) {
312312
throw exception;
313313
}
@@ -766,7 +766,7 @@ export class TypeParser {
766766
}
767767

768768
try {
769-
const constExpr = this.constExprParser.parse(tokens, true);
769+
const constExpr = this.constExprParser.parse(tokens);
770770

771771
if (constExpr instanceof ConstExprArrayNode) {
772772
throw exception;

tests/parser/const-expr-node.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,11 @@ const floatNodeParseData = [
8585
] as TestFixtureDataItem[];
8686

8787
const stringNodeParseData = [
88-
// String
89-
['"foo"', new ConstExprStringNode('"foo"', ConstExprStringNode.DOUBLE_QUOTED)],
90-
['"Foo \\n\\"\\r Bar"', new ConstExprStringNode('"Foo \\n\\"\\r Bar"', ConstExprStringNode.DOUBLE_QUOTED)],
91-
["'bar'", new ConstExprStringNode("'bar'", ConstExprStringNode.SINGLE_QUOTED)],
92-
["'Foo \\' Bar'", new ConstExprStringNode("'Foo \\' Bar'", ConstExprStringNode.SINGLE_QUOTED)],
88+
// String - values are always unescaped in v2.0
89+
['"foo"', new ConstExprStringNode('foo', ConstExprStringNode.DOUBLE_QUOTED)],
90+
['"Foo \\n\\"\\r Bar"', new ConstExprStringNode('Foo \n"\r Bar', ConstExprStringNode.DOUBLE_QUOTED)],
91+
["'bar'", new ConstExprStringNode('bar', ConstExprStringNode.SINGLE_QUOTED)],
92+
["'Foo \\' Bar'", new ConstExprStringNode("Foo ' Bar", ConstExprStringNode.SINGLE_QUOTED)],
9393
] as TestFixtureDataItem[];
9494

9595
const arrayNodeParseData = [
@@ -224,7 +224,7 @@ describe('ConstExprParser', () => {
224224
const visitor = new NodeCollectingVisitor();
225225
const traverser = new NodeTraverser([visitor]);
226226

227-
parser = new ConstExprParser(true, {
227+
parser = new ConstExprParser({
228228
lines: true,
229229
indexes: true,
230230
});

tests/parser/upstream-v2-features.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ describe('Upstream v2.0+ Features', () => {
114114

115115
it('should parse single-quoted string in const expr', () => {
116116
const lexer = new Lexer();
117-
const parser = new ConstExprParser(true);
117+
const parser = new ConstExprParser();
118118
const tokens = new TokenIterator(lexer.tokenize("'hello'"));
119-
const node = parser.parse(tokens, true);
119+
const node = parser.parse(tokens);
120120
expect(node).toBeInstanceOf(ConstExprStringNode);
121121
const strNode = node as ConstExprStringNode;
122122
expect(strNode.value).toBe('hello');
@@ -125,9 +125,9 @@ describe('Upstream v2.0+ Features', () => {
125125

126126
it('should parse double-quoted string in const expr', () => {
127127
const lexer = new Lexer();
128-
const parser = new ConstExprParser(true);
128+
const parser = new ConstExprParser();
129129
const tokens = new TokenIterator(lexer.tokenize('"world"'));
130-
const node = parser.parse(tokens, true);
130+
const node = parser.parse(tokens);
131131
expect(node).toBeInstanceOf(ConstExprStringNode);
132132
const strNode = node as ConstExprStringNode;
133133
expect(strNode.value).toBe('world');

0 commit comments

Comments
 (0)