@@ -1353,33 +1353,73 @@ object Parsers {
13531353 * The amount of whitespace to remove is determined by the indentation
13541354 * of the last line (which should contain only whitespace before the
13551355 * closing delimiter).
1356+ *
1357+ * @param str The string content to dedent
1358+ * @param offset The source offset where the string literal begins
1359+ * @return The dedented string, or str if errors were reported
13561360 */
1357- private def dedentString (str : String ): String = {
1361+ private def dedentString (str : String , offset : Offset ): String = {
13581362 if (str.isEmpty) return str
13591363
13601364 // Find the last line (should be just whitespace before closing delimiter)
13611365 val lastNewlineIdx = str.lastIndexOf('\n ' )
1362- assert(
1363- lastNewlineIdx >= 0 ,
1364- " Dedented string literal must contain at least two newlines"
1365- )
1366+ if (lastNewlineIdx < 0 ) {
1367+ syntaxError(
1368+ em " dedented string literal must start with newline after opening quotes " ,
1369+ offset
1370+ )
1371+ return str
1372+ }
13661373
13671374 val closingIndent = str.substring(lastNewlineIdx + 1 )
1368- assert(
1369- closingIndent.forall(_.isWhitespace),
1370- " Last line of a dedented string literal must contain only whitespace followed by the closing delimiter"
1371- )
1375+ if (! closingIndent.forall(_.isWhitespace)) {
1376+ syntaxError(
1377+ em " last line of dedented string literal must contain only whitespace before closing delimiter " ,
1378+ offset
1379+ )
1380+ return str
1381+ }
1382+
1383+ // Check for mixed tabs and spaces in closing indent
1384+ val hasTabs = closingIndent.contains('\t ' )
1385+ val hasSpaces = closingIndent.contains(' ' )
1386+ if (hasTabs && hasSpaces) {
1387+ syntaxError(
1388+ em " dedented string literal cannot mix tabs and spaces in indentation " ,
1389+ offset
1390+ )
1391+ return str
1392+ }
1393+
13721394 // Split into lines
13731395 val lines = str.linesIterator.toSeq
13741396
13751397 // Process all lines except the last (which is just the closing indentation)
1398+ var lineOffset = offset
13761399 val dedented = lines.dropRight(1 ).map { line =>
1377- if (line.startsWith(closingIndent)) line.substring(closingIndent.length)
1378- else if (line.trim.isEmpty) " " // Empty or whitespace-only lines
1379- else assert(
1380- false ,
1381- s " line \" $line\" in dedented string must be either empty or be further indented than the closing delimiter "
1382- )
1400+ val result =
1401+ if (line.startsWith(closingIndent)) line.substring(closingIndent.length)
1402+ else if (line.trim.isEmpty) " " // Empty or whitespace-only lines
1403+ else {
1404+ // Check if this line has mixed tabs/spaces that don't match closing indent
1405+ val lineIndent = line.takeWhile(_.isWhitespace)
1406+ val lineHasTabs = lineIndent.contains('\t ' )
1407+ val lineHasSpaces = lineIndent.contains(' ' )
1408+ if ((hasTabs && lineHasSpaces && ! lineHasTabs) || (hasSpaces && lineHasTabs && ! lineHasSpaces)) {
1409+ syntaxError(
1410+ em " dedented string literal cannot mix tabs and spaces in indentation " ,
1411+ offset
1412+ )
1413+ } else {
1414+ syntaxError(
1415+ em " line in dedented string literal must be indented at least as much as the closing delimiter " ,
1416+ lineOffset
1417+ )
1418+ }
1419+ line
1420+ }
1421+ lineOffset += line.length + 1 // +1 for the newline
1422+ result
13831423 }
13841424
13851425 // Drop the first line if it's empty (the newline after opening delimiter)
@@ -1424,7 +1464,7 @@ object Parsers {
14241464 // For non-interpolated dedented strings, check if the token starts with '''
14251465 val str = in.strVal
14261466 if (token == STRINGLIT && ! inStringInterpolation && isDedentedStringLiteral(negOffset)) {
1427- dedentString(str)
1467+ dedentString(str, negOffset )
14281468 } else str
14291469 case TRUE => true
14301470 case FALSE => false
0 commit comments