@@ -700,35 +700,213 @@ def _dart_plugin_class(ctx, package_root, name):
700700 # Parenthesised because buildifier cannot parse a bare conditional here.
701701 return dart_class , (dart_file if dart_file else "{}.dart" .format (name ))
702702
703- def _namespace (build_gradle_text ):
704- """Read the `namespace` AGP assigns the module, from build.gradle.
705-
706- AGP 7 deprecated the manifest's `package=` and AGP 8 removed it, so a plugin
707- written against a current AGP declares its package name here instead and
708- ships a bare `<manifest />`. This is the authoritative source of the two:
709- where both exist AGP errors on a disagreement rather than reconciling them.
710-
711- Matched line by line rather than by substring, so that `testNamespace` and a
712- coordinate mentioning the word are not mistaken for it. `namespace 'x'`
713- (Groovy) and `namespace = "x"` (Kotlin, and the Groovy assignment form) are
714- both accepted.
703+ def _namespace (build_gradle_text , source_path , strict = True ):
704+ """Read the static `namespace` AGP assigns the module.
705+
706+ This is deliberately a small lexer rather than a substring search. Gradle
707+ has both Groovy and Kotlin syntax in the wild, and the word also appears in
708+ comments, strings, and identifiers such as `testNamespace`. A declaration
709+ that is present but not a literal is an error in the standard generator:
710+ treating it as absent would incorrectly fall through to a manifest that AGP
711+ itself does not use. Package recipes set `strict = False` because their
712+ custom implementation may know how to handle a dynamic namespace.
715713 """
716- for line in _strip_buildscript (build_gradle_text ).splitlines ():
717- stripped = line .strip ()
718- if not stripped .startswith ("namespace" ):
714+ text = _strip_buildscript (build_gradle_text )
715+ i = 0
716+ for _ in range (len (text )):
717+ if i >= len (text ):
718+ break
719+ c = text [i ]
720+ if text [i :i + 2 ] == "//" :
721+ newline = text .find ("\n " , i + 2 )
722+ i = len (text ) if newline == - 1 else newline + 1
719723 continue
720- rest = stripped [len ("namespace" ):].lstrip ()
721- if rest .startswith ("=" ):
722- rest = rest [1 :].lstrip ()
723- if not rest or rest [0 ] not in "'\" " :
724+ if text [i :i + 2 ] == "/*" :
725+ end = text .find ("*/" , i + 2 )
726+ i = len (text ) if end == - 1 else end + 2
724727 continue
725- quote = rest [0 ]
726- end = rest .find (quote , 1 )
727- if end == - 1 :
728+ if c in ["'" , "\" " ]:
729+ i = _namespace_string_end (text , i )
730+ continue
731+ if not (c .isalpha () or c == "_" ):
732+ i += 1
733+ continue
734+
735+ start = i
736+ i += 1
737+ for _ in range (len (text )):
738+ if i >= len (text ) or not (text [i ].isalnum () or text [i ] == "_" ):
739+ break
740+ i += 1
741+ if text [start :i ] != "namespace" :
728742 continue
729- return rest [1 :end ]
743+
744+ # Only a DSL statement can introduce the module namespace. In
745+ # particular, do not interpret `foo.namespace` or a namespace named as
746+ # an argument to an unrelated call.
747+ previous = start - 1
748+ at_line_start = previous < 0
749+ for _ in range (len (text )):
750+ if previous < 0 :
751+ at_line_start = True
752+ break
753+ if not text [previous ].isspace ():
754+ break
755+ if text [previous ] == "\n " :
756+ at_line_start = True
757+ break
758+ previous -= 1
759+ if not at_line_start and (previous < 0 or text [previous ] not in ["{" , "}" , ";" ]):
760+ continue
761+
762+ value , expression = _parse_namespace_declaration (text , i )
763+ if value != None :
764+ return value
765+ if strict :
766+ fail ("Unsupported namespace expression in {}: {}" .format (
767+ source_path ,
768+ expression if expression else "<missing expression>" ,
769+ ))
770+ return None
730771 return None
731772
773+ def _namespace_string_end (text , start ):
774+ """Return the end of a quoted Gradle string, or the end of the text."""
775+ quote = text [start ]
776+ delimiter = quote * 3 if text [start :start + 3 ] == quote * 3 else quote
777+ i = start + len (delimiter )
778+ for _ in range (len (text )):
779+ if i >= len (text ):
780+ break
781+ if text [i :i + len (delimiter )] == delimiter :
782+ return i + len (delimiter )
783+ if text [i ] == "\\ " :
784+ i += 2
785+ continue
786+ i += 1
787+ return len (text )
788+
789+ def _namespace_tail (text , start ):
790+ """Return a declaration tail, including a continued literal when needed."""
791+ line_end = text .find ("\n " , start )
792+ if line_end == - 1 :
793+ line_end = len (text )
794+ first_line = text [start :line_end ]
795+ comment = first_line .find ("//" )
796+ first_code = first_line if comment == - 1 else first_line [:comment ]
797+ continuation = (
798+ first_code .rstrip ().endswith ("=" ) or
799+ first_code .rstrip ().endswith ("(" )
800+ )
801+ if not continuation :
802+ return first_line if comment == - 1 else first_line [:comment ]
803+
804+ # Assignment and call forms may put their literal on the next line. Keep
805+ # scanning through a call's balanced parentheses, but stop an assignment
806+ # after its first complete line expression. The loop is bounded by the
807+ # input length so malformed Gradle cannot make repository evaluation hang.
808+ depth = 0
809+ seen_value = False
810+ pieces = []
811+ segment_start = start
812+ i = start
813+ for _ in range (len (text )):
814+ if i >= len (text ):
815+ pieces .append (text [segment_start :])
816+ return "" .join (pieces )
817+ if text [i ] in ["'" , "\" " ]:
818+ i = _namespace_string_end (text , i )
819+ seen_value = True
820+ continue
821+ if text [i :i + 2 ] == "//" :
822+ pieces .append (text [segment_start :i ])
823+ newline = text .find ("\n " , i + 2 )
824+ if newline == - 1 :
825+ return "" .join (pieces )
826+ segment_start = newline
827+ i = newline
828+ continue
829+ c = text [i ]
830+ if c == "(" :
831+ depth += 1
832+ elif c == ")" :
833+ if depth > 0 :
834+ depth -= 1
835+ elif c == "\n " :
836+ if depth == 0 and seen_value :
837+ pieces .append (text [segment_start :i ])
838+ return "" .join (pieces )
839+ elif not c .isspace () and c != "=" :
840+ seen_value = True
841+ i += 1
842+ pieces .append (text [segment_start :])
843+ return "" .join (pieces )
844+
845+ def _namespace_literal (expression ):
846+ """Return a literal's value, or None when `expression` is not one."""
847+ expression = expression .strip ()
848+ if len (expression ) < 2 or expression [0 ] not in ["'" , "\" " ]:
849+ return None
850+ if expression .startswith (expression [0 ] * 3 ):
851+ return None
852+ end = _namespace_string_end (expression , 0 )
853+ if end == len (expression ) and expression [- 1 ] != expression [0 ]:
854+ return None
855+ if expression [end :].strip ():
856+ return None
857+ value = expression [1 :end - 1 ]
858+ if "$" in value or "\\ " in value :
859+ return None
860+ return value
861+
862+ def _namespace_structural_tail (tail ):
863+ """Remove syntax closing an inline guard and return the remaining text."""
864+ tail = tail .strip ()
865+ for _ in range (len (tail )):
866+ if not tail or tail [- 1 ] not in ["}" , ";" ]:
867+ break
868+ tail = tail [:- 1 ].rstrip ()
869+ return tail
870+
871+ def _parse_namespace_declaration (text , after_name ):
872+ """Return (literal value, unsupported expression) for one declaration."""
873+ tail = _namespace_tail (text , after_name )
874+ stripped = tail .lstrip ()
875+ if stripped .startswith ("=" ):
876+ expression = _namespace_structural_tail (stripped [1 :])
877+ return _namespace_literal (expression ), expression
878+
879+ if stripped .startswith ("(" ):
880+ depth = 0
881+ end = - 1
882+ i = 0
883+ for _ in range (len (stripped )):
884+ if i >= len (stripped ):
885+ break
886+ if stripped [i ] in ["'" , "\" " ]:
887+ i = _namespace_string_end (stripped , i )
888+ continue
889+ if stripped [i ] == "(" :
890+ depth += 1
891+ elif stripped [i ] == ")" :
892+ depth -= 1
893+ if depth == 0 :
894+ end = i
895+ break
896+ i += 1
897+ if end == - 1 :
898+ expression = _namespace_structural_tail (stripped [1 :])
899+ return None , expression
900+ expression = stripped [1 :end ].strip ()
901+ trailing = _namespace_structural_tail (stripped [end + 1 :])
902+ if trailing :
903+ expression = "{} {}" .format (expression , trailing ).strip ()
904+ return None , expression
905+ return _namespace_literal (expression ), expression
906+
907+ expression = _namespace_structural_tail (stripped )
908+ return _namespace_literal (expression ), expression
909+
732910def _manifest_package (ctx , manifest_path ):
733911 """Read the `package=` attribute from a library manifest.
734912
@@ -1066,7 +1244,7 @@ def _flutter_plugins_impl(ctx):
10661244 _list_files (root , "android/src/main/kotlin" , [".kt" ]) +
10671245 _list_files (root , "android/src/main/java" , [".kt" ])
10681246 )
1069- package = _namespace (ctx .read (build_gradle ))
1247+ package = _namespace (ctx .read (build_gradle ), str ( build_gradle ), strict = False )
10701248 if not package :
10711249 manifest_path = root .get_child ("android/src/main/AndroidManifest.xml" )
10721250 if manifest_path .exists :
@@ -1175,8 +1353,7 @@ def _flutter_plugins_impl(ctx):
11751353 # manifest was enough for the demo app's plugins and is not enough in
11761354 # the wild: four of smooth_app's thirty carry a bare `<manifest />` and
11771355 # name themselves in build.gradle, while qr_code_scanner is the mirror
1178- # case, predating `namespace` entirely.
1179- package = _namespace (ctx .read (build_gradle ))
1356+ package = _namespace (ctx .read (build_gradle ), str (build_gradle ))
11801357 if not package :
11811358 package = _manifest_package (ctx , root .get_child ("android/src/main/AndroidManifest.xml" ))
11821359 if not package :
0 commit comments