Parse new-style format substitutions parameters

Extract superclass of percent and new-style substitution chunks
This commit is contained in:
Valentina Kiryushkina
2017-05-15 14:37:51 +03:00
parent c54dbd1dbe
commit d9eb2c3e69
6 changed files with 401 additions and 161 deletions
@@ -40,11 +40,11 @@ public class PySubstitutionChunkReference extends PsiReferenceBase<PyStringLiter
private final boolean myIsPercent;
public PySubstitutionChunkReference(@NotNull final PyStringLiteralExpression element,
@NotNull final PyStringFormatParser.SubstitutionChunk chunk, final int position, boolean isPercent) {
@NotNull final PyStringFormatParser.SubstitutionChunk chunk, final int position) {
super(element, getKeywordRange(element, chunk));
myChunk = chunk;
myPosition = position;
myIsPercent = isPercent;
myIsPercent = chunk instanceof PyStringFormatParser.PercentSubstitutionChunk;
}
@Nullable
@@ -40,23 +40,23 @@ public class PythonFormattedStringReferenceProvider extends PsiReferenceProvider
private static PySubstitutionChunkReference[] getReferencesFromFormatString(@NotNull final PyStringLiteralExpression element) {
final List<PyStringFormatParser.SubstitutionChunk> chunks = PyStringFormatParser.filterSubstitutions(
PyStringFormatParser.parseNewStyleFormat(element.getText()));
return getReferencesFromChunks(element, chunks, false);
return getReferencesFromChunks(element, chunks);
}
private static PySubstitutionChunkReference[] getReferencesFromPercentString(@NotNull final PyStringLiteralExpression element) {
final List<PyStringFormatParser.SubstitutionChunk>
chunks = PyStringFormatParser.filterSubstitutions(PyStringFormatParser.parsePercentFormat(element.getText()));
return getReferencesFromChunks(element, chunks, true);
return getReferencesFromChunks(element, chunks);
}
@NotNull
public static PySubstitutionChunkReference[] getReferencesFromChunks(@NotNull final PyStringLiteralExpression element,
@NotNull final List<PyStringFormatParser.SubstitutionChunk> chunks,
boolean isPercent) {
@NotNull final List<PyStringFormatParser.SubstitutionChunk> chunks) {
final PySubstitutionChunkReference[] result = new PySubstitutionChunkReference[chunks.size()];
for (int i = 0; i < chunks.size(); i++) {
final PyStringFormatParser.SubstitutionChunk chunk = chunks.get(i);
result[i] = new PySubstitutionChunkReference(element, chunk, i, isPercent);
result[i] = new PySubstitutionChunkReference(element, chunk, i);
}
return result;
}
@@ -66,7 +66,7 @@ public class PyStringFormatInspection extends PyInspection {
public static class Visitor extends PyInspectionVisitor {
private static class Inspection {
private static final ImmutableMap<Character, String> FORMAT_CONVERSIONS = ImmutableMap.<Character, String>builder()
private static final ImmutableMap<Character, String> PERCENT_FORMAT_CONVERSIONS = ImmutableMap.<Character, String>builder()
.put('d', "int or long or float")
.put('i', "int or long or float")
.put('o', "int or long or float")
@@ -364,7 +364,7 @@ public class PyStringFormatInspection extends PyInspection {
}
}
private void inspectFormat(@NotNull final PyStringLiteralExpression formatExpression) {
private void inspectPercentFormat(@NotNull final PyStringLiteralExpression formatExpression) {
final String value = formatExpression.getStringValue();
final List<PyStringFormatParser.SubstitutionChunk> chunks = filterSubstitutions(parsePercentFormat(value));
@@ -374,40 +374,42 @@ public class PyStringFormatInspection extends PyInspection {
// if use mapping keys
final boolean mapping = chunks.size() > 0 && chunks.get(0).getMappingKey() != null;
for (int i = 0; i < chunks.size(); ++i) {
PyStringFormatParser.SubstitutionChunk chunk = chunks.get(i);
// Mapping key
String mappingKey = Integer.toString(i + 1);
if (mapping) {
if (chunk.getMappingKey() == null || chunk.isUnclosedMapping()) {
registerProblem(formatExpression, PyBundle.message("INSP.too.few.keys"));
break;
PyStringFormatParser.PercentSubstitutionChunk chunk = as(chunks.get(i), PyStringFormatParser.PercentSubstitutionChunk.class);
if (chunk != null) {
// Mapping key
String mappingKey = Integer.toString(i + 1);
if (mapping) {
if (chunk.getMappingKey() == null || chunk.isUnclosedMapping()) {
registerProblem(formatExpression, PyBundle.message("INSP.too.few.keys"));
break;
}
mappingKey = chunk.getMappingKey();
myUsedMappingKeys.put(mappingKey, false);
}
mappingKey = chunk.getMappingKey();
myUsedMappingKeys.put(mappingKey, false);
}
// Minimum field width
inspectWidth(formatExpression, chunk.getWidth());
// Minimum field width
inspectWidth(formatExpression, chunk.getWidth());
// Precision
inspectWidth(formatExpression, chunk.getPrecision());
// Precision
inspectWidth(formatExpression, chunk.getPrecision());
// Format specifier
final char conversionType = chunk.getConversionType();
if (conversionType == 'b') {
final LanguageLevel languageLevel = LanguageLevel.forElement(formatExpression);
if (languageLevel.isOlderThan(LanguageLevel.PYTHON35) || !isBytesLiteral(formatExpression, myTypeEvalContext)) {
registerProblem(formatExpression, "Unsupported format character 'b'");
return;
// Format specifier
final char conversionType = chunk.getConversionType();
if (conversionType == 'b') {
final LanguageLevel languageLevel = LanguageLevel.forElement(formatExpression);
if (languageLevel.isOlderThan(LanguageLevel.PYTHON35) || !isBytesLiteral(formatExpression, myTypeEvalContext)) {
registerProblem(formatExpression, "Unsupported format character 'b'");
return;
}
}
if (PERCENT_FORMAT_CONVERSIONS.containsKey(conversionType)) {
myFormatSpec.put(mappingKey, PERCENT_FORMAT_CONVERSIONS.get(conversionType));
continue;
}
registerProblem(formatExpression, PyBundle.message("INSP.no.format.specifier.char"), new PyAddSpecifierToFormatQuickFix());
return;
}
if (FORMAT_CONVERSIONS.containsKey(conversionType)) {
myFormatSpec.put(mappingKey, FORMAT_CONVERSIONS.get(conversionType));
continue;
}
registerProblem(formatExpression, PyBundle.message("INSP.no.format.specifier.char"), new PyAddSpecifierToFormatQuickFix());
return;
}
}
@@ -472,7 +474,7 @@ public class PyStringFormatInspection extends PyInspection {
if (node.getLeftExpression() instanceof PyStringLiteralExpression && node.isOperator("%")) {
final Inspection inspection = new Inspection(this, myTypeEvalContext);
final PyStringLiteralExpression literalExpression = (PyStringLiteralExpression)node.getLeftExpression();
inspection.inspectFormat(literalExpression);
inspection.inspectPercentFormat(literalExpression);
if (inspection.isProblem()) {
return;
}
@@ -69,83 +69,19 @@ public class PyStringFormatParser {
public static class SubstitutionChunk extends FormatStringChunk {
@Nullable private String myMappingKey;
@Nullable private String myConversionFlags;
@Nullable private String myWidth;
@Nullable private String myPrecision;
@Nullable private Integer myPosition;
@Nullable private Integer myAutoPosition;
private char myLengthModifier;
private char myConversionType;
private boolean myUnclosedMapping;
public SubstitutionChunk(int startIndex) {
super(startIndex, startIndex);
public SubstitutionChunk(int startIndex, int endIndex) {
super(startIndex, endIndex);
}
protected void setEndIndex(int endIndex) {
myEndIndex = endIndex;
}
public char getConversionType() {
return myConversionType;
}
private void setConversionType(char conversionType) {
myConversionType = conversionType;
}
@Nullable
public String getMappingKey() {
return myMappingKey;
}
protected void setMappingKey(@Nullable String mappingKey) {
myMappingKey = mappingKey;
}
@Nullable
public String getConversionFlags() {
return myConversionFlags;
}
private void setConversionFlags(@Nullable String conversionFlags) {
myConversionFlags = conversionFlags;
}
@Nullable
public String getWidth() {
return myWidth;
}
private void setWidth(@Nullable String width) {
myWidth = width;
}
@Nullable
public String getPrecision() {
return myPrecision;
}
private void setPrecision(@Nullable String precision) {
myPrecision = precision;
}
public char getLengthModifier() {
return myLengthModifier;
}
private void setLengthModifier(char lengthModifier) {
myLengthModifier = lengthModifier;
}
public boolean isUnclosedMapping() {
return myUnclosedMapping;
}
private void setUnclosedMapping(boolean unclosedMapping) {
myUnclosedMapping = unclosedMapping;
}
@Nullable
public Integer getPosition() {
return myPosition;
@@ -171,6 +107,169 @@ public class PyStringFormatParser {
protected void setAutoPosition(@Nullable Integer autoPosition) {
myAutoPosition = autoPosition;
}
@Nullable
public String getPrecision() {
return myPrecision;
}
public void setPrecision(@Nullable String precision) {
myPrecision = precision;
}
@Nullable
public String getWidth() {
return myWidth;
}
public void setWidth(@Nullable String width) {
myWidth = width;
}
@Nullable
public String getMappingKey() {
return myMappingKey;
}
protected void setMappingKey(@Nullable String mappingKey) {
myMappingKey = mappingKey;
}
}
public static class PercentSubstitutionChunk extends SubstitutionChunk {
@Nullable private String myConversionFlags;
private char myLengthModifier;
private char myConversionType;
private boolean myUnclosedMapping;
public PercentSubstitutionChunk(int startIndex) {
super(startIndex, startIndex);
}
public char getConversionType() {
return myConversionType;
}
private void setConversionType(char conversionType) {
myConversionType = conversionType;
}
@Nullable
public String getConversionFlags() {
return myConversionFlags;
}
private void setConversionFlags(@Nullable String conversionFlags) {
myConversionFlags = conversionFlags;
}
public char getLengthModifier() {
return myLengthModifier;
}
private void setLengthModifier(char lengthModifier) {
myLengthModifier = lengthModifier;
}
public boolean isUnclosedMapping() {
return myUnclosedMapping;
}
private void setUnclosedMapping(boolean unclosedMapping) {
myUnclosedMapping = unclosedMapping;
}
}
public static class NewStyleSubstitutionChunk extends SubstitutionChunk {
@Nullable private String myMappingKey;
@Nullable private String myConversion;
@Nullable private String myWidth;
@Nullable private String myPrecision;
@Nullable private Integer myPosition;
private char myConversionType;
private boolean signOption;
private boolean zeroPadding;
private boolean alternateForm;
private boolean thousandsSeparator;
public NewStyleSubstitutionChunk(int startIndex) {
super(startIndex, startIndex);
}
@Nullable
public String getMappingKey() {
return myMappingKey;
}
public void setMappingKey(@Nullable String mappingKey) {
myMappingKey = mappingKey;
}
@Nullable
public String getConversion() {
return myConversion;
}
public void setConversion(@Nullable String conversion) {
myConversion = conversion;
}
@Nullable
public String getWidth() {
return myWidth;
}
public void setWidth(@Nullable String width) {
myWidth = width;
}
@Nullable
public String getPrecision() {
return myPrecision;
}
public void setPrecision(@Nullable String precision) {
myPrecision = precision;
}
public boolean hasSignOption() {
return signOption;
}
public void setSignOption(boolean signOption) {
this.signOption = signOption;
}
public char getConversionType() {
return myConversionType;
}
public void setConversionType(char conversionType) {
myConversionType = conversionType;
}
public boolean useAlternateForm() {
return alternateForm;
}
public void setAlternateForm(boolean alternateForm) {
this.alternateForm = alternateForm;
}
public boolean hasZeroPadding() {
return zeroPadding;
}
public void setZeroPadding(boolean zeroPadding) {
this.zeroPadding = zeroPadding;
}
public boolean hasThousandsSeparator() {
return thousandsSeparator;
}
public void setThousandsSeparator(boolean thousandsSeparator) {
this.thousandsSeparator = thousandsSeparator;
}
}
@NotNull private final String myLiteral;
@@ -178,11 +277,21 @@ public class PyStringFormatParser {
private int myPos;
private int mySubstitutionsCount = 0;
// % strings
private static final String CONVERSION_FLAGS = "#0- +";
private static final String DIGITS = "0123456789";
private static final String LENGTH_MODIFIERS = "hlL";
private static final String VALID_CONVERSION_TYPES = "diouxXeEfFgGcrsb";
// new style strings
private static final String ALIGN_SYMBOLS = "<>=^";
private static final String SIGN_SYMBOLS = "+- ";
private static final String NEW_STYLE_CONVERSION_TYPES = "bcdeEfFgGnosxX%";
private static final String CONVERSIONS = "rsa";
private static final char ALTERNATE_FORM_SYMBOL = '#';
private static final char ZERO_PADDING_SYMBOL = '0';
private PyStringFormatParser(@NotNull String literal) {
myLiteral = literal;
}
@@ -194,42 +303,7 @@ public class PyStringFormatParser {
@NotNull
public static List<FormatStringChunk> parseNewStyleFormat(@NotNull String s) {
final List<FormatStringChunk> results = new ArrayList<>();
final Matcher matcher = NEW_STYLE_FORMAT_TOKENS.matcher(s);
int autoPositionedFieldsCount = 0;
while (matcher.find()) {
final String group = matcher.group();
final int start = matcher.start();
final int end = matcher.end();
if ("{{".equals(group) || "}}".equals(group)) {
results.add(new ConstantChunk(start, end));
}
else if (group.startsWith("{") && group.endsWith("}")) {
final SubstitutionChunk chunk = new SubstitutionChunk(start);
chunk.setEndIndex(end);
final int nameStart = start + 1;
final int nameEnd = StringUtil.indexOfAny(s, "!:.[}", nameStart, end);
if (nameEnd > 0 && nameStart < nameEnd) {
final String name = s.substring(nameStart, nameEnd);
try {
final int number = Integer.parseInt(name);
chunk.setPosition(number);
} catch (NumberFormatException e) {
chunk.setMappingKey(name);
}
}
else {
chunk.setAutoPosition(autoPositionedFieldsCount);
autoPositionedFieldsCount++;
}
// TODO: Parse substitution details
results.add(chunk);
}
else {
results.add(new ConstantChunk(start, end));
}
}
return results;
return new PyStringFormatParser(s).parseNewStyle();
}
@NotNull
@@ -253,9 +327,107 @@ public class PyStringFormatParser {
return myResult;
}
private List<FormatStringChunk> parseNewStyle() {
final List<FormatStringChunk> results = new ArrayList<FormatStringChunk>();
final Matcher matcher = NEW_STYLE_FORMAT_TOKENS.matcher(myLiteral);
int autoPositionedFieldsCount = 0;
while (matcher.find()) {
final String group = matcher.group();
myPos = matcher.start();
final int end = matcher.end();
if ("{{".equals(group) || "}}".equals(group)) {
results.add(new ConstantChunk(myPos, end));
}
else if (group.startsWith("{") && group.endsWith("}")) {
autoPositionedFieldsCount = parseNewStyleSubstitution(results, end, autoPositionedFieldsCount);
}
else {
results.add(new ConstantChunk(myPos, end));
}
}
return results;
}
private int parseNewStyleSubstitution(List<FormatStringChunk> results, int end, int autoPositionedFieldsCount) {
final NewStyleSubstitutionChunk chunk = new NewStyleSubstitutionChunk(myPos);
chunk.setEndIndex(end);
// skip "{"
myPos++;
// name
final int nameEnd = StringUtil.indexOfAny(myLiteral, "!:.[}", myPos, end);
if (nameEnd > 0 && myPos < nameEnd) {
final String name = myLiteral.substring(myPos, nameEnd);
try {
final int number = Integer.parseInt(name);
chunk.setPosition(number);
} catch (NumberFormatException e) {
chunk.setMappingKey(name);
}
}
else {
chunk.setAutoPosition(autoPositionedFieldsCount);
autoPositionedFieldsCount++;
}
// conversion
myPos = Math.max(myPos, StringUtil.indexOf(myLiteral, '!', myPos, end) + 1);
final int conversionEnd = StringUtil.indexOfAny(myLiteral, ":}", myPos, end);
if (conversionEnd - myPos == 1) {
final String conversion = myLiteral.substring(myPos, conversionEnd);
if (StringUtil.containsAnyChar(conversion, CONVERSIONS)) {
chunk.setConversion(conversion);
myPos = conversionEnd + 1;
}
}
// parse format spec
// [[fill]align][sign][#][0][width][,][.precision][type]
if (isAt(':')) {
myPos++;
//skip align options
myPos = Math.max(myPos, StringUtil.indexOfAny(myLiteral, ALIGN_SYMBOLS, myPos, end) + 1);
if (isAtSet(SIGN_SYMBOLS)) {
chunk.setSignOption(true);
myPos++;
}
if (isAt(ALTERNATE_FORM_SYMBOL)) {
chunk.setAlternateForm(true);
myPos++;
}
if (isAt(ZERO_PADDING_SYMBOL)) {
chunk.setZeroPadding(true);
myPos++;
}
chunk.setWidth(parseWhileCharacterInSet(DIGITS));
if (isAt(',')) {
myPos++;
chunk.setThousandsSeparator(true);
}
if (isAt('.')) {
myPos++;
chunk.setPrecision(parseWhileCharacterInSet(DIGITS));
}
if (isAtSet(NEW_STYLE_CONVERSION_TYPES)) {
chunk.setConversionType(myLiteral.charAt(myPos));
}
}
results.add(chunk);
return autoPositionedFieldsCount;
}
private void parseSubstitution() {
assert myLiteral.charAt(myPos) == '%';
SubstitutionChunk chunk = new SubstitutionChunk(myPos);
PercentSubstitutionChunk chunk = new PercentSubstitutionChunk(myPos);
myResult.add(chunk);
myPos++;
if (isAt('(')) {
@@ -79,20 +79,22 @@ public class PyAddSpecifierToFormatQuickFix implements LocalQuickFix {
int shift = 2;
for (int i = 0; i < chunks.size(); i++) {
final PyStringFormatParser.SubstitutionChunk chunk = chunks.get(i);
if (elements.length <= i) return;
final PyType type = context.getType(elements[i]);
final char conversionType = chunk.getConversionType();
if (conversionType == '\u0000') {
final int insertOffset = offset + chunk.getStartIndex() + shift;
if (insertOffset > leftExpression.getTextRange().getEndOffset()) return;
if (PyTypeChecker.match(strType, type, context)) {
document.insertString(insertOffset, "s");
shift += 1;
}
if (PyTypeChecker.match(intType, type, context) || PyTypeChecker.match(floatType, type, context)) {
document.insertString(insertOffset, "d");
shift += 1;
final PyStringFormatParser.PercentSubstitutionChunk chunk = PyUtil.as(chunks.get(i), PyStringFormatParser.PercentSubstitutionChunk.class);
if (chunk != null) {
if (elements.length <= i) return;
final PyType type = context.getType(elements[i]);
final char conversionType = chunk.getConversionType();
if (conversionType == '\u0000') {
final int insertOffset = offset + chunk.getStartIndex() + shift;
if (insertOffset > leftExpression.getTextRange().getEndOffset()) return;
if (PyTypeChecker.match(strType, type, context)) {
document.insertString(insertOffset, "s");
shift += 1;
}
if (PyTypeChecker.match(intType, type, context) || PyTypeChecker.match(floatType, type, context)) {
document.insertString(insertOffset, "d");
shift += 1;
}
}
}
}
@@ -53,7 +53,7 @@ public class PyStringFormatParserTest extends TestCase {
public void testFormat() {
List<FormatStringChunk> chunks = parsePercentFormat("%s");
assertEquals(1, chunks.size());
SubstitutionChunk chunk = (SubstitutionChunk)chunks.get(0);
PercentSubstitutionChunk chunk = (PercentSubstitutionChunk)chunks.get(0);
assertEquals(0, chunk.getStartIndex());
assertEquals(2, chunk.getEndIndex());
assertEquals('s', chunk.getConversionType());
@@ -68,7 +68,7 @@ public class PyStringFormatParserTest extends TestCase {
public void testMappingKey() {
List<FormatStringChunk> chunks = parsePercentFormat("%(language)s");
assertEquals(1, chunks.size());
SubstitutionChunk chunk = (SubstitutionChunk)chunks.get(0);
PercentSubstitutionChunk chunk = (PercentSubstitutionChunk)chunks.get(0);
assertEquals("language", chunk.getMappingKey());
assertEquals('s', chunk.getConversionType());
}
@@ -76,7 +76,7 @@ public class PyStringFormatParserTest extends TestCase {
public void testConversionFlags() {
List<FormatStringChunk> chunks = parsePercentFormat("%#0d");
assertEquals(1, chunks.size());
SubstitutionChunk chunk = (SubstitutionChunk)chunks.get(0);
PercentSubstitutionChunk chunk = (PercentSubstitutionChunk)chunks.get(0);
assertEquals("#0", chunk.getConversionFlags());
}
@@ -97,21 +97,21 @@ public class PyStringFormatParserTest extends TestCase {
public void testLengthModifier() {
List<FormatStringChunk> chunks = parsePercentFormat("%ld");
assertEquals(1, chunks.size());
SubstitutionChunk chunk = (SubstitutionChunk)chunks.get(0);
PercentSubstitutionChunk chunk = (PercentSubstitutionChunk)chunks.get(0);
assertEquals('l', chunk.getLengthModifier());
}
public void testDoubleAsterisk() {
List<FormatStringChunk> chunks = parsePercentFormat("%**d");
assertEquals(2, chunks.size());
SubstitutionChunk chunk = (SubstitutionChunk)chunks.get(0);
PercentSubstitutionChunk chunk = (PercentSubstitutionChunk)chunks.get(0);
assertEquals(2, chunk.getEndIndex());
assertEquals('\0', chunk.getConversionType());
}
public void testUnclosedMapping() {
List<FormatStringChunk> chunks = parsePercentFormat("%(name1s");
SubstitutionChunk chunk = (SubstitutionChunk)chunks.get(0);
PercentSubstitutionChunk chunk = (PercentSubstitutionChunk)chunks.get(0);
assertEquals("name1s", chunk.getMappingKey());
assertTrue(chunk.isUnclosedMapping());
}
@@ -174,6 +174,70 @@ public class PyStringFormatParserTest extends TestCase {
assertEquals(TextRange.create(7, 9), chunks.get(2).getTextRange());
}
public void testNewStyleSign() {
final List<FormatStringChunk> chunks = parseNewStyleFormat("{:+}");
assertEquals(1, chunks.size());
assertEquals(TextRange.create(0, 4), chunks.get(0).getTextRange());
assertTrue(((NewStyleSubstitutionChunk)chunks.get(0)).hasSignOption());
}
public void testNewStyleAlternateForm() {
final List<FormatStringChunk> chunks = parseNewStyleFormat("{:#}");
assertEquals(1, chunks.size());
assertEquals(TextRange.create(0, 4), chunks.get(0).getTextRange());
assertTrue(((NewStyleSubstitutionChunk)chunks.get(0)).useAlternateForm());
}
public void testNewStyleZeroPadded() {
final List<FormatStringChunk> chunks = parseNewStyleFormat("{:0}");
assertEquals(1, chunks.size());
assertEquals(TextRange.create(0, 4), chunks.get(0).getTextRange());
assertTrue(((NewStyleSubstitutionChunk)chunks.get(0)).hasZeroPadding());
}
public void testNewStyleWidth() {
final List<FormatStringChunk> chunks = parseNewStyleFormat("{:10}");
assertEquals(1, chunks.size());
assertEquals(TextRange.create(0, 5), chunks.get(0).getTextRange());
assertEquals("10", ((NewStyleSubstitutionChunk)chunks.get(0)).getWidth());
}
public void testNewStyleZeroPaddingWidth() {
final List<FormatStringChunk> chunks = parseNewStyleFormat("{:010}");
assertEquals(1, chunks.size());
assertEquals(TextRange.create(0, 6), chunks.get(0).getTextRange());
assertTrue(((NewStyleSubstitutionChunk)chunks.get(0)).hasZeroPadding());
assertEquals("10", ((NewStyleSubstitutionChunk)chunks.get(0)).getWidth());
}
public void testNewStyleThousandSeparator() {
final List<FormatStringChunk> chunks = parseNewStyleFormat("{:,}");
assertEquals(1, chunks.size());
assertEquals(TextRange.create(0, 4), chunks.get(0).getTextRange());
assertTrue(((NewStyleSubstitutionChunk)chunks.get(0)).hasThousandsSeparator());
}
public void testNewStylePrecision() {
final List<FormatStringChunk> chunks = parseNewStyleFormat("{:.2}");
assertEquals(1, chunks.size());
assertEquals(TextRange.create(0, 5), chunks.get(0).getTextRange());
assertEquals("2", ((NewStyleSubstitutionChunk)chunks.get(0)).getPrecision());
}
public void testNewStyleConversionType() {
final List<FormatStringChunk> chunks = parseNewStyleFormat("{:d}");
assertEquals(1, chunks.size());
assertEquals(TextRange.create(0, 4), chunks.get(0).getTextRange());
assertEquals('d', ((NewStyleSubstitutionChunk)chunks.get(0)).getConversionType());
}
public void testNewStyleConversion() {
final List<FormatStringChunk> chunks = parseNewStyleFormat("{!s}");
assertEquals(1, chunks.size());
assertEquals(TextRange.create(0, 4), chunks.get(0).getTextRange());
assertEquals("s", ((NewStyleSubstitutionChunk)chunks.get(0)).getConversion());
}
public void testNewStyleNestedFields() {
final Field field = doParseAndGetFirstField("u'{foo:{bar} {baz}}'");
assertEquals("foo", field.getFirstName());